關於事務
我們在數據庫中需要執行一個系列的操作的時候,要保證這個系列執行的連續性和完整性,要么整個系列的執行都成功,要么就全部失敗(只要有一個步驟失敗,其他均回滾到之前的狀態),
保證不會存在一部分成功一部分失敗的情況。這就是我們事務的職責。下面舉個分蘋果的例子:
A同學有3個蘋果,B同學有2個蘋果,如果A同學給一個蘋果給B同學,那么A同學只剩下2個蘋果,而B同學有了3個。步驟如下
1 update tname set apples=apples-1 where name = "A"; 2 update tname set apples=apples+1 where name = "B";
當然,這是理想情況下的結果。有可能會出錯:A同學減去了一個蘋果,然后執行B同學的加蘋果的時候,系統宕掉了,B同學沒有加成功,A同學的庫存中無厘頭少了一個蘋果。
這時候我們就需要事務支持了,有事務的情況下,就有兩種狀態,要么都成功,要么都失敗。
操作成功:A同學減去一個蘋果,B同學增加一個蘋果,最終A 2個,B 3個。
操作失敗:A同學依舊是3個蘋果,B同學依舊是2個蘋果。
事務有如下特性(參考官方描述):
2、事務處理可以用來維護數據庫的完整性,保證成批的 SQL 語句要么全部執行,要么全部不執行。
3、事務用來管理 insert,update,delete 語句。
事務的四個特性(ACID)
一般來說,衡量事務必須滿足四個特性:ACID,即 原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、持久性(Durability)。
原子性(Atomicity):一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
一致性(Consistency):在事務開始之前和事務結束以后,數據庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及后續數據庫可以自發性地完成預定的工作。
隔離性(Isolation):數據庫允許多個並發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務並發執行時由於交叉執行而導致數據的不一致。事務隔離分為不同級別,包括讀未提交(read uncommitted)、讀提交(read committed)、可重復讀(repeatable read)和串行化(Serializable),下面會詳細說明。
持久性(Durability):事務處理結束后,對數據的修改就是永久的,會持久化到硬盤上,即便系統故障也不會丟失。
顯示和隱式事務
事務分為顯示事務和隱式事務,
隱式事務:事務自動開啟、提交或回滾,比如insert、update、delete語句,事務的開啟、提交或回滾由mysql內部自動控制的
顯示事務:事務需要手動開啟、提交或回滾,由開發者自己控制。
自動提交
MySQL中事務默認是隱式事務(即自動提交(autocommit)模式為 ON),執行insert、update、delete操作的時候,數據庫自動開啟事務、提交或回滾事務。如下所示:
1 mysql> show variables like 'autocommit'; 2 +---------------+-------+ 3 | Variable_name | Value | 4 +---------------+-------+ 5 | autocommit | ON | 6 +---------------+-------+ 7 1 row in set
在自動提交模式下,如果沒有start transaction顯式地開始一個事務,那么每個sql語句都會被當做一個事務執行提交操作。
通過如下方式,可以關閉autocommit;需要注意的是,autocommit參數是針對連接的,在一個連接中修改了參數,不會對其他連接產生影響。如果你新開一個命令窗口,會恢復到默認值。
1 mysql> set autocommit = 0; 2 Query OK, 0 rows affected 3 4 mysql> show variables like 'autocommit'; 5 +---------------+-------+ 6 | Variable_name | Value | 7 +---------------+-------+ 8 | autocommit | OFF | 9 +---------------+-------+ 10 1 row in set
如果關閉了autocommit,則所有的sql語句都在一個事務中,直到執行了commit或rollback,該事務結束,同時開始了另外一個事務。
特殊操作
在MySQL中,存在一些特殊的命令,如果在事務中執行了這些命令,會馬上強制執行commit提交事務;比如DDL語句(create table/drop table/alter table)。
不過,常用的select、insert、update和delete命令,都不會強制提交事務。
手動操作事務的兩種方式
改變默認事務提交策略
SET AUTOCOMMIT=0 禁止自動提交
SET AUTOCOMMIT=1 開啟自動提交
這個跟上面的腳本大概一致,先設置為禁止自動提交事務,這樣執行的修改並沒有真正的到數據庫中,等commit 或者 rollback來最終確認,再手動進行事務操作。
1 --1、設置不自動提交事務 2 set autocommit=0; 3 --2、手動進行事務操作 4 commit;|rollback;
提交示例:
1 mysql> set autocommit = 0; 2 Query OK, 0 rows affected 3 4 mysql> insert into classes values(5,'初三五班'); 5 Query OK, 1 row affected 6 7 mysql> commit; --提交操作 8 Query OK, 0 rows affected 9 10 mysql> select * from classes; 11 +---------+-----------+ 12 | classid | classname | 13 +---------+-----------+ 14 | 1 | 初三一班 | 15 | 2 | 初三二班 | 16 | 3 | 初三三班 | 17 | 4 | 初三四班 | 18 | 5 | 初三五班 | 19 +---------+-----------+ 20 5 rows in set
回滾示例:
1 mysql> set autocommit = 0; 2 Query OK, 0 rows affected 3 4 mysql> insert into classes values(6,'初三六班'); 5 Query OK, 1 row affected 6 7 mysql> rollback; --回滾操作 8 Query OK, 0 rows affected 9 10 mysql> select * from classes; 11 +---------+-----------+ 12 | classid | classname | 13 +---------+-----------+ 14 | 1 | 初三一班 | 15 | 2 | 初三二班 | 16 | 3 | 初三三班 | 17 | 4 | 初三四班 | 18 | 5 | 初三五班 | 19 +---------+-----------+ 20 5 rows in set
常規啟用事務
START|BEGIN 開始一個事務
ROLLBACK 事務回滾
COMMIT 事務確認
這是典型的MySQL事務操作,其中start transaction標識事務開始,commit提交事務,將執行結果寫入到數據庫。如果sql語句執行出現問題,會調用rollback,回滾所有已經執行成功的sql語句。
當然,也可以在事務中直接使用rollback語句進行回滾。:
1 start transaction; --1、開啟事務 2 /* 1條或者n條待執行的語句 */ 3 commit;|rollback; --2、手動進行事務操作
提交示例:
1 mysql> start transaction; 2 Query OK, 0 rows affected 3 4 mysql> insert into classes values(6,'初三六班'); 5 Query OK, 1 row affected 6 7 mysql> commit; 8 Query OK, 0 rows affected 9 10 mysql> select * from classes; 11 12 +---------+-----------+ 13 | classid | classname | 14 +---------+-----------+ 15 | 1 | 初三一班 | 16 | 2 | 初三二班 | 17 | 3 | 初三三班 | 18 | 4 | 初三四班 | 19 | 5 | 初三五班 | 20 | 6 | 初三六班 | 21 +---------+-----------+ 22 6 rows in set
回滾示例:
1 mysql> start transaction; 2 Query OK, 0 rows affected 3 4 mysql> insert into classes values(7,'初三七班'); 5 Query OK, 1 row affected 6 7 mysql> rollback; 8 Query OK, 0 rows affected 9 10 mysql> select * from classes; 11 +---------+-----------+ 12 | classid | classname | 13 +---------+-----------+ 14 | 1 | 初三一班 | 15 | 2 | 初三二班 | 16 | 3 | 初三三班 | 17 | 4 | 初三四班 | 18 | 5 | 初三五班 | 19 | 6 | 初三六班 | 20 +---------+-----------+ 21 6 rows in set
事務保存點的使用
事務保存點(savepoint),指的是對事務執行過程中做位置保存(類似我們打游戲時的存盤點),如果你寫了一大堆的語句,但是有部分是你不想回滾的,想保留修改的狀態,但是部分是你想回滾的。
這時候使用savepoint是個不錯的方法。
1 mysql> start transaction; --開啟事務 2 Query OK, 0 rows affected 3 4 mysql> insert into classes values(7,'初三七班'); 5 Query OK, 1 row affected 6 7 mysql> savepoint point1; --注意:這邊設置了一個存盤點 8 Query OK, 0 rows affected 9 10 mysql> insert into classes values(8,'初三八班'); 11 Query OK, 1 row affected 12 13 mysql> rollback to point1; --記住這個語法,回滾到存盤點,存盤點之后的語句就丟棄了 14 Query OK, 0 rows affected 15 16 mysql> commit; 17 Query OK, 0 rows affected 18 19 mysql> select * from classes; --最后輸出,確實只有存盤點之前的成功了 20 +---------+-----------+ 21 | classid | classname | 22 +---------+-----------+ 23 | 1 | 初三一班 | 24 | 2 | 初三二班 | 25 | 3 | 初三三班 | 26 | 4 | 初三四班 | 27 | 5 | 初三五班 | 28 | 6 | 初三六班 | 29 | 7 | 初三七班 | 30 +---------+-----------+ 31 7 rows in set
這邊需要注意 savepoint 和 rollback to savepoint 的配合使用。
只讀事務的使用
表示在事務中執行的是一些只讀操作,如查詢,但是不會做insert、update、delete操作,數據庫內部對只讀事務可能會有一些性能上的優化。
1 start transaction read only;
再只讀操作的事務中進行增、刪、改操作會報錯,如下:
1 mysql> start transaction read only; 2 Query OK, 0 rows affected 3 4 mysql> select * from classes; 5 +---------+-----------+ 6 | classid | classname | 7 +---------+-----------+ 8 | 1 | 初三一班 | 9 | 2 | 初三二班 | 10 | 3 | 初三三班 | 11 | 4 | 初三四班 | 12 | 5 | 初三五班 | 13 | 6 | 初三六班 | 14 | 7 | 初三七班 | 15 +---------+-----------+ 16 7 rows in set 17 18 mysql> insert into classes values(8,'初三八班'); 19 1792 - Cannot execute statement in a READ ONLY transaction. --這邊報出異常
事務的臟讀、幻讀、不可重復讀
臟讀:讀取未提交數據
臟讀就是指當一個事務A正在訪問數據,並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務B也訪問這個數據,然后使用了這個臟數據。舉個例子
時間順序 | A事務 | B事務 |
T1 |
開始事務 | |
T2 |
開始事務 | |
T3 |
查詢A同學有2個蘋果 | |
T4 |
給A同學增加一個蘋果(未提交) |
|
T5 |
查詢A同學有3個蘋果(讀臟數據) | |
T6 | 添加蘋果操作出現錯誤,回滾回2個蘋果 | |
T7 | 提交事務 |
不可重復讀:前后多次讀取數據不一致
不可重復讀指的是在事務A中先后多次讀取同一個數據,讀取的結果不一樣,因為另外一個事務也訪問該同一數據,並且可能修改這個數據,這種現象稱為不可重復讀。
臟讀與不可重復讀的區別在於:前者讀到的是其他事務未提交的數據,后者讀到的是其他事務已提交的數據。
時間順序 | A事務 | B事務 |
T1 |
開始事務 | |
T2 |
開始事務 | |
T3 |
查詢A同學有2個蘋果 | |
T4 |
給A同學增加一個蘋果(未提交) |
|
T5 |
提交事務 | |
T6 | 查詢A同學有3個蘋果(不可重復讀) |
按照正確邏輯,事務B前后兩次讀取到的數據應該一致,這邊一次讀到的是2個,一次讀到的是3個。
幻讀:前后多次讀取,數據總量不一致
在事務A中按照某個條件先后兩次查詢數據庫,兩次查詢結果的條數不同,這種現象稱為幻讀。不可重復讀與幻讀的區別可以通俗的理解為:前者是數據變了,后者是數據的行數變了。
時間順序 | A事務 | B事務 |
T1 |
開始事務 | 開始事務 |
T2 |
第一次查詢庫存數據有2條 | |
T3 |
給A同學增加一個蘋果(增加了一條庫存數據) | |
T4 |
提交事務 |
|
T5 |
第二次查詢庫存數據有3條 |
按照正確邏輯,按照正確邏輯,事務B前后兩次讀取到的數據總量應該一致
不可重復讀和幻讀的區別
(1)不可重復讀是讀取了其他事務更改的數據,針對update操作
解決:使用行級鎖,鎖定該行,事務A多次讀取操作完成后才釋放該鎖,這個時候才允許其他事務更改剛才的數據。
(2)幻讀是讀取了其他事務新增的數據,針對insert與delete操作
解決:使用表級鎖,鎖定整張表,事務A多次讀取數據總量之后才釋放該鎖,這個時候才允許其他事務新增數據。
幻讀和不可重復讀都是指的一個事務范圍內的操作受到其他事務的影響了。只不過幻讀是重點在插入和刪除,不可重復讀重點在修改
事務的隔離級別
SQL標准中事務的隔離性(Isolation)定義了四種隔離級別,並規定了每種隔離級別下上述幾個(臟讀、不可重復讀、幻讀)問題是否存在。
一般來說,隔離級別越低,系統開銷越低,可支持的並發越高,但隔離性也越差。隔離級別與讀問題的關系如下:
隔離級別的分類
注意:幻讀只會在 可重復讀 級別中才會出現,其他級別下不存在。
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
讀未提交:Read Uncommitted |
✔ | ✔ | x |
讀已提交:Read Committed |
× | ✔ | x |
可重復讀:Repeatable Read |
× | x | ✔ |
串行化:Serializable |
× |
× | × |
查看|修改 隔離級別
查看當前的隔離級別,默認應該都是可重復讀(Repeatable Read):
1 mysql> show variables like 'transaction_isolation'; 2 +-----------------------+-----------------+ 3 | Variable_name | Value | 4 +-----------------------+-----------------+ 5 | transaction_isolation | REPEATABLE-READ | 6 +-----------------------+-----------------+ 7 1 row in set
修改隔離級別:
找到MySQL安裝目錄中的my.init文件,會看到當前的隔離級別:REPEATABLE-READ。
1 # 隔離級別設置,READ-UNCOMMITTED讀未提交,READ-COMMITTED讀已提交,REPEATABLE-READ可重復讀,SERIALIZABLE串行 2 transaction-isolation=REPEATABLE-READ
修改后重啟MySQL即可,下面的各項操作都是在修改了對應的隔離級別之后的操作。
READ-UNCOMMITTED:讀未提交
事物A和事物B,事物B未提交的數據,事物A可以讀取到,這里讀取到的數據叫做“臟數據”,這種隔離級別最低,一般理論上存在,數據庫隔離級別大都高於該級別。
舉個例子:學校新增加了7班和8班,教務主任進去錄入了8班的班級信息,但是該事務並未提交,而8班班主任正好去查看班級信息,發現是存在的,摩拳擦掌准備錄入這個班級的學生信息。可是教務主任發現班級信息寫錯了,應該先錄入7班,於是迅速回滾了該事務。
最后8班的信息消失了。出現上述情況,就是我們所說的臟讀 ,兩個並發的事務,“事務A:查詢班級信息”、“事務B:錄入班級信息”,事務A讀取了事務B尚未提交的數據。
數據基礎:
1 mysql> select * from classes; 2 +---------+-----------+ 3 | classid | classname | 4 +---------+-----------+ 5 | 1 | 初三一班 | 6 | 2 | 初三二班 | 7 | 3 | 初三三班 | 8 | 4 | 初三四班 | 9 | 5 | 初三五班 | 10 | 6 | 初三六班 | 11 | 7 | 初三七班 | 12 +---------+-----------+ 13 7 rows in set
時間順序 | 事務A | 事務B |
---|---|---|
T1 | start transaction; | |
T2 | select * from classes; | |
T3 | start transaction; | |
T4 | insert into classes values(8,'初三八班'); | |
T5 | select * from classes; | |
T6 | select * from classes; | |
T7 | rollback; | |
T8 | commit; |
說明:
事務A-T2:只有7條數據,事務A-T6:有8條數據,事務B-T6並未提交,此時事務A已經看到了事務B插入的數據,出現了臟讀。
事務A-T2:只有7條數據,T6-A:有8條數據,查詢到的結果不一樣,出現不可重復讀。
結論:讀未提交情況下,可以讀取到其他事務還未提交的數據,多次讀取結果不一樣,出現了臟讀、不可重復讀
READ-COMMITTED:讀已提交
事物A和事物B,事物B提交完的數據,事物A才能讀取到
這種隔離級別高於讀未提交:即對方事物提交之后的數據,我當前事物才能讀取到
這種隔離級別可以避免“臟數據” ,但會導致“不可重復讀取”
數據基礎跟上面一樣
時間順序 | 事務A | 事務B |
---|---|---|
T1 | start transaction; | |
T2 | select * from classes; | |
T3 | start transaction; | |
T4 | insert into classes values(8,'初三八班'); | |
T5 | select * from classes; | |
T6 | commit; | |
T7 | select * from classes; |
說明:
事務A-T5:只有7條數據,A看不到B新增的第8條數據,說明沒有臟讀。
事務A-T5:只有7條數據,事務A-T7:讀到了8條數據,此時事務B已經提交了事務,事務A讀取到了事務B提交的數據,說明可以讀取到已提交的數據。
事務A-T5 和 事務A-T7:兩次讀取的數據結果不一樣,說明不可重復讀。
結論:讀已提交情況下,無法讀取到其他事務還未提交的數據,可以讀取到其他事務已經提交的數據,多次讀取結果不一樣,未出現臟讀,出現了讀已提交、不可重復讀。
REPEATABLE-READ:可重復讀
可重復讀是MySQL默認事務隔離級別
事務A和事務B,事務B提交之后的數據,事務A讀取不到,即對方提交之后的數據,還是讀取不到
事務B是可重復讀取數據,隔離級別高於讀已提交,這種隔離級別可以避免“不可重復讀取”,達到可重復讀取,但是可能導致“幻讀”
1 mysql> select * from classes; 2 +---------+-----------+ 3 | classid | classname | 4 +---------+-----------+ 5 | 1 | 初三一班 | 6 | 2 | 初三二班 | 7 | 3 | 初三三班 | 8 | 4 | 初三四班 | 9 | 5 | 初三五班 | 10 | 6 | 初三六班 | 11 | 7 | 初三七班 | 12 | 8 | 初三八班 | 13 +---------+-----------+ 14 8 rows in set
時間順序 | 事務A | 事務B |
---|---|---|
T1 | start transaction; | |
T2 | start transaction; | |
T3 | insert into classes values(9,'初三九班'); | |
T4 | select * from classes; | |
T5 | commit; | |
T6 | select * from classes; | |
T7 | select * from classes; | |
T8 | commit; | |
T9 | select * from classes; |
說明:
事務A-T4、事務A-T7:讀到的是八條數據,事務B-T6:有數據,A事務下讀不到B事務產生的數據,說明沒有臟讀。
事務A-T7:讀到的是9條數據,這時事務B已經commit,事務A看不到事務B已提交的數據,A事務下兩次讀的結果一樣,說明可重復讀。
事務A-T9:讀到的是9條數據。
結論:可重復讀情況下,未出現臟讀,未讀取到其他事務已提交的數據,多次讀取結果一致,即可重復讀。
SERIALIZABLE:串行
SERIALIZABLE會讓並發的事務串行執行。事務A和事務B,事務A在操作數據庫時,事務B只能排隊等待,這種隔離級別很少使用,吞吐量太低,用戶體驗差
這種級別可以避免“幻讀”,每一次讀取的都是數據庫中真實存在數據,事務A與事務B串行,而不並發
數據基礎同上
時間 | 窗口A | 窗口B |
---|---|---|
T1 | start transaction; | |
T2 | select * from classes; | |
T3 | start transaction; | |
T4 | insert into classes values(9,'初三九班'); | |
T5 | select * from classes; | |
T6 | commit; | |
T7 | commit; |
按時間順序運行上面的命令,會發現事務B-T4這樣會被阻塞,直到事務A執行完畢T6步驟。
可以看出來,事務只能串行執行了。串行情況下不存在臟讀、不可重復讀、幻讀的問題了。
隔離級別選擇注意點
1、讀已提交(READ-COMMITTED)通常用的比較多,也是MySQL默認選項。
2、具體選擇哪種需要結合具體的業務來選擇,隔離級別越高,並發性也低,比如最高級別SERIALIZABLE
會讓事物串行執行,並發操作變成串行了,會導致系統性能直接降低
事務典型用法
1 DROP PROCEDURE IF EXISTS t_test; 2 DELIMITER // 3 CREATE PROCEDURE t_test() 4 BEGIN 5 DECLARE t_error INTEGER; 6 DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET t_error = 1; 7 START TRANSACTION; 8 insert into students(studentname,score,classid) VALUE('A',99,3); 9 insert into students(studentname,score,classid) VALUE('A','lala',3); --這邊執行錯誤,兩個語句都會被回滾 10 IF t_error = 1 THEN 11 ROLLBACK; -- 有錯誤回滾 12 ELSE 13 COMMIT; --沒錯誤提交 14 END IF; 15 END //DELIMITER; 16 CALL t_test();
這個是典型的用法,score是decimal類型,這兩個語句都會被回滾。
MVCC了解
RR解決臟讀、不可重復讀、幻讀等問題,使用的是MVCC(Multi-Version Concurrency Control)協議,即多版本的並發控制協議。
有多個請求來讀取表中的數據時可以不采取任何操作,但是多個請求里有讀請求,又有修改請求時必須有一種措施來進行並發控制。不然很有可能會造成不一致。
讀寫鎖,解決上述問題很簡單,只需用兩種鎖的組合來對讀寫請求進行控制即可,這兩種鎖被稱為:
1、共享鎖(shared lock),又叫做 讀鎖
讀鎖是可以共享的,或者說多個讀請求可以共享一把鎖讀數據,不會造成阻塞。
2、排他鎖(exclusive lock),又叫做 寫鎖
寫鎖會排斥其他所有獲取鎖的請求,一直阻塞,直到寫入完成釋放鎖。
通過讀寫鎖,可以做到讀讀可以並行,但是不能做到寫讀,寫寫並行。這邊了解一下,后續專門一篇來說明MVCC的原理和實現機制分析。
總結
1、認識ACID(原子性、一致性、隔離性、持久性)特性及其實現原理
2、了解事務的臟讀、幻讀、不可重復讀
3、了解事務的隔離級別以及原理