事務(wù)的基本要素
事務(wù)的四個基本要素:ACID
原子性(A):整個事務(wù)中的操作,要么全部完成,要么全部不完成(全部撤銷)
一致性(C):事務(wù)開始之前和結(jié)束之后,數(shù)據(jù)庫的完整性沒有遭到破壞
隔離性(I):在同一時間,只允許一個事務(wù)請求同一數(shù)據(jù)
持久性(D):事務(wù)完成以后,該事務(wù)對數(shù)據(jù)庫所做的操作持久化在數(shù)據(jù)庫中,并不會被回滾
原子性與一致性
在數(shù)據(jù)庫實(shí)現(xiàn)的場景中,一致性可以分為數(shù)據(jù)庫外部的一致性和數(shù)據(jù)庫內(nèi)部的一致性。前者由外部應(yīng)用的編碼來保證,即某個應(yīng)用在執(zhí)行轉(zhuǎn)帳的數(shù)據(jù)庫操作時,必須在同一個事務(wù)內(nèi)部調(diào)用對帳戶A和帳戶B的操作。如果在這個層次出現(xiàn)錯誤,這不是數(shù)據(jù)庫本身能夠解決的,也不屬于我們需要討論的范圍。后者由數(shù)據(jù)庫來保證,即在同一個事務(wù)內(nèi)部的一組操作必須全部執(zhí)行成功(或者全部失敗)。這就是事務(wù)處理的原子性。
為了實(shí)現(xiàn)原子性,需要通過日志:將所有對數(shù)據(jù)的更新操作都寫入日志,如果一個事務(wù)中的一部分操作已經(jīng)成功,但以后的操作,由于斷電/系統(tǒng)崩潰/其它的軟硬件錯誤而無法繼續(xù),則通過回溯日志,將已經(jīng)執(zhí)行成功的操作撤銷,從而達(dá)到“全部操作失敗”的目的。最常見的場景是,數(shù)據(jù)庫系統(tǒng)崩潰后重啟,此時數(shù)據(jù)庫處于不一致的狀態(tài),必須先執(zhí)行一個crash
recovery的過程:讀取日志進(jìn)行REDO(重演將所有已經(jīng)執(zhí)行成功但尚未寫入到磁盤的操作,保證持久性),再對所有到崩潰時尚未成功提交的事務(wù)進(jìn)行UNDO(撤銷所有執(zhí)行了一部分但尚未提交的操作,保證原子性)。crash
recovery結(jié)束后,數(shù)據(jù)庫恢復(fù)到一致性狀態(tài),可以繼續(xù)被使用。
其中一致性還可以分為:
- 強(qiáng)一致性:讀操作可以立即讀到提交的更新操作。
- 弱一致性:提交的更新操作,不一定立即會被讀操作讀到,此種情況會存在一個不一致窗口,指的是讀操作可以讀到最新值的一段時間。
- 最終一致性:是弱一致性的特例。事務(wù)更新一份數(shù)據(jù),最終一致性保證在沒有其他事務(wù)更新同樣的值的話,最終所有的事務(wù)都會讀到之前事務(wù)更新的最新值。如果沒有錯誤發(fā)生,不一致窗口的大小依賴于:通信延遲,系統(tǒng)負(fù)載等。
隔離性
本文重點(diǎn)討論事務(wù)的隔離性,隔離性是為了保證并發(fā)情況下數(shù)據(jù)庫的強(qiáng)一致性,下面探討一下并發(fā)情況下帶來的問題。
事務(wù)中經(jīng)常出現(xiàn)的并發(fā)問題
臟讀: 一個事務(wù)讀取了另一個事務(wù)操作但未提交的數(shù)據(jù)
比如A、B兩個事務(wù),都操作同一張表,A剛剛對數(shù)據(jù)進(jìn)行了操作(插入、修改等)但還沒有提交,這時B讀取到了A剛剛操作的數(shù)據(jù),因?yàn)锳有可能回滾,所以這部分?jǐn)?shù)據(jù)有可能只是臨時的、無效的,即臟數(shù)據(jù)。
不可重復(fù)讀:一個事務(wù)中的多個相同的查詢返回了不同數(shù)據(jù)
比如A、B兩個事務(wù),A中先后有兩次查詢相同數(shù)據(jù)的操作,第一次查詢完之后,B對相關(guān)數(shù)據(jù)進(jìn)行了修改,造成A事務(wù)第二次查詢出的數(shù)據(jù)與第一次不一致。
幻讀:事務(wù)并發(fā)執(zhí)行時,其中一個事務(wù)對另一個事務(wù)中操作的結(jié)果集的影響
比如A、B兩個事務(wù),事務(wù)A操作表中符合條件的若干行。事務(wù)B插入符合A操作條件的數(shù)據(jù)行,然后再提交。后來發(fā)現(xiàn)事務(wù)A并沒有如愿對“所有”符合條件的數(shù)據(jù)行做了修改。
請注意不可重復(fù)讀和幻讀的區(qū)別:不可重復(fù)讀和幻讀都是在另一個事務(wù)提交之后才操作,但不可重復(fù)讀只針對某一條特定數(shù)據(jù),而幻讀指的是整個操作結(jié)果集;下面提到的Repeatable read隔離級別可以防止臟讀、不可重復(fù)讀,但是不能防止幻讀,是因?yàn)樵摳綦x級別只應(yīng)用了行鎖鎖住了某一行特定數(shù)據(jù),而沒有用表鎖把整個表鎖住,而Serializable隔離級別就可以鎖住整個表,從而防止幻讀。
SQL規(guī)范定義的四個事務(wù)隔離級別
以上都是事務(wù)中經(jīng)常發(fā)生的問題,所以為了兼顧并發(fā)效率和異??刂疲琒QL規(guī)范定義了四個事務(wù)隔離級別:
Read uncommitted(讀未提交):如果設(shè)置了該隔離級別,則當(dāng)前事務(wù)可以讀取到其他事務(wù)已經(jīng)修改但還沒有提交的數(shù)據(jù)。這種隔離級別是最低的,會導(dǎo)致上面所說的臟讀
Read committed(讀已提交):如果設(shè)置了該隔離級別,當(dāng)前事務(wù)只可以讀取到其他事務(wù)已經(jīng)提交后的數(shù)據(jù),這種隔離級別可以防止臟讀,但是會導(dǎo)致不可重復(fù)讀和幻讀。這種隔離級別最效率較高,并且不可重復(fù)讀和幻讀在一般情況下是可以接受的,所以這種隔離級別最為常用
<span id="repeatableRead">Repeatable read(可重復(fù)讀)</span>:如果設(shè)置了該隔離級別,可以保證當(dāng)前事務(wù)中多次讀取特定記錄的結(jié)果相同??梢苑乐古K讀、不可重復(fù)讀,但是會導(dǎo)致幻讀
<span id="serializable">Serializable(串行化)</span>:如果設(shè)置了該隔離級別,所有的事務(wù)會放在一個隊(duì)列中執(zhí)行,當(dāng)前事務(wù)開啟后,其他事務(wù)將不能執(zhí)行,即同一個時間點(diǎn)只能有一個事務(wù)操作數(shù)據(jù)庫對象。這種隔離級別對于保證數(shù)據(jù)完整性的能力是最高的,但因?yàn)橥粫r刻只允許一個事務(wù)操作數(shù)據(jù)庫,所以大大降低了系統(tǒng)的并發(fā)能力
引用一張很經(jīng)典的表格:
| 事務(wù)隔離級別 | 是否存在臟讀 | 是否存在不可重復(fù)讀 | 是否存在幻讀 |
|---|---|---|---|
| Read uncommitted | √ | √ | √ |
| Read committed | × | √ | √ |
| Repeatable read | × | × | √ |
| Serializable | × | × | × |
并且隔離級別越高,并發(fā)性能越弱:

事務(wù)隔離級別測試
read uncommitted測試
打開一個客戶端A,并設(shè)置當(dāng)前事務(wù)模式為read uncommitted(未提交讀),查詢表account的初始值:
mysql> set session transaction isolation level read uncommitted;
mysql> select @@tx_isolation;
+--------------------+
| @@tx_isolation |
+--------------------+
| READ-UNCOMMITTED |
+--------------------+
1 row in set, 1 warning (0.01 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
在客戶端A的事務(wù)提交之前,打開另一個客戶端B,更新表account:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance=balance+1000 where id=1;
Query OK, 1 rows affected (0.01 sec)
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 2000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
這時,雖然客戶端B的事務(wù)還沒提交,打開客戶端A查詢數(shù)據(jù),發(fā)現(xiàn)已經(jīng)可以查詢到客戶端B更新的數(shù)據(jù):
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 2000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
客戶端B回滾,所有的操作都將會被撤銷:
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
客戶端A再次查詢:
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
所以一旦客戶端B的事務(wù)因?yàn)槟撤N原因回滾,所有的操作都將會被撤銷,那客戶端A查詢到的數(shù)據(jù)其實(shí)就是臟數(shù)據(jù)
read committed測試
針對上面的問題,把客戶端A的事務(wù)隔離級別設(shè)置為read committed,再重復(fù)上面的步驟,會發(fā)現(xiàn)B在事務(wù)結(jié)束之前,A并不能查詢到B所做的操作。
客戶端A設(shè)置事務(wù)隔離級別為read committed,然后開啟事務(wù):
mysql> set session transaction isolation level read committed;
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-COMMITTED |
+------------------+
1 row in set, 1 warning (0.01 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
客戶端B開啟事務(wù)并更新數(shù)據(jù):
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
mysql> update account set balance=balance+1000 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 2000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
客戶端A在客戶端B未提交事務(wù)前讀取數(shù)據(jù):
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
正如上文所說,這種隔離級別下可能導(dǎo)致前事務(wù)中多次讀取特定記錄的結(jié)果不相同,比如客戶端A事務(wù)隔離級別為read committed,在A的一個事務(wù)中,執(zhí)行兩次相同的查詢,在這兩次查詢的中間,客戶端B對數(shù)據(jù)進(jìn)行更改并提交事務(wù),那么會導(dǎo)致客戶端A的兩次查詢結(jié)果不一致,導(dǎo)致“不可重復(fù)讀”的麻煩。
客戶端B提交事務(wù):
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
客戶端A再次讀取數(shù)據(jù):
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 2000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
repeatable read測試
同理,如果設(shè)置了repeatable read隔離級別,就可以保證在當(dāng)前事務(wù)中多次執(zhí)行相同查詢的結(jié)果集相同,實(shí)現(xiàn)“可重復(fù)讀”。
客戶端A設(shè)置事務(wù)隔離級別為repeatable read并開啟事務(wù):
mysql> set session transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
客戶端B開啟事務(wù),修改數(shù)據(jù)并提交事務(wù):
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance=5000 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 5000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
客戶端A查詢,數(shù)據(jù)沒有發(fā)生變化:
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
可以發(fā)現(xiàn),在同一個事務(wù)A里,無論事務(wù)B是否提交,讀到的數(shù)據(jù)都是相同的。
但是,這種隔離級別會導(dǎo)致“幻讀”,比如客戶端A中事務(wù)操作表中符合條件的若干行,同時客戶端B中事務(wù)插入符合A操作條件的數(shù)據(jù)行,然后再提交。
客戶端A設(shè)置事務(wù)隔離級別為repeatable read并開啟事務(wù):
mysql> set session transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
+----+------+---------+
3 rows in set (0.00 sec)
客戶端B開啟事務(wù),插入數(shù)據(jù)并提交事務(wù):
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(4, 'd', 8888);
Query OK, 1 row affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
| 4 | d | 8888 |
+----+------+---------+
4 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
客戶端A更新全表數(shù)據(jù):
mysql> update account set balance=9999;
Query OK, 4 rows affected (0.00 sec)
Rows matched: 4 Changed: 4 Warnings: 0
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 9999 |
| 2 | b | 9999 |
| 3 | c | 9999 |
| 4 | d | 9999 |
+----+------+---------+
4 rows in set (0.00 sec)
奇怪,這樣操作并不會發(fā)生幻讀,那如果事務(wù)B在事務(wù)A更新之后插入數(shù)據(jù),會不會產(chǎn)生幻讀呢,我們再來做一個測試:
客戶端A設(shè)置事務(wù)隔離級別為repeatable read并開啟事務(wù),并在開啟事務(wù)后更新全表數(shù)據(jù):
mysql> set session transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
| 4 | d | 8888 |
+----+------+---------+
3 rows in set (0.00 sec)
mysql> update account set balance=9999;
Query OK, 4 rows affected (0.00 sec)
Rows matched: 4 Changed: 4 Warnings: 0
客戶端B開啟事務(wù),插入數(shù)據(jù):
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
| 4 | d | 8888 |
+----+------+---------+
4 rows in set (0.00 sec)
mysql> insert into account values(5, 'e', 5555);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction //阻塞超時
這個時候客戶端B阻塞超時,可以發(fā)現(xiàn),事務(wù)A正在執(zhí)行寫操作時,事務(wù)B是阻塞的(第一次測試事務(wù)B沒有阻塞是因?yàn)槭聞?wù)A沒有進(jìn)行寫操作)
為了不阻塞,在客戶端A提交事務(wù):
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
客戶端A提交事務(wù)后,客戶端B再次插入數(shù)據(jù):
mysql> insert into account values(5, 'e', 5555); //事務(wù)A已提交,沒有阻塞
Query OK, 1 row affected (2.92 sec)
mysql> select * from account; //因?yàn)槭聞?wù)B的默認(rèn)隔離級別是REPEATABLE-READ,即使事務(wù)A提交后也讀不到更新后的數(shù)據(jù)
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 1000 |
| 2 | b | 1200 |
| 3 | c | 6666 |
| 4 | d | 8888 |
| 5 | e | 5555 |
+----+------+---------+
5 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; //事務(wù)B提交后,就能讀到事務(wù)A更新的數(shù)據(jù)了
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 9999 |
| 2 | b | 9999 |
| 3 | c | 9999 |
| 4 | d | 9999 |
| 5 | e | 5555 |
+----+------+---------+
5 rows in set (0.00 sec)
客戶端A再次讀取數(shù)據(jù):
mysql> select * from account; //兩個事務(wù)都提交后客戶端A讀到的數(shù)據(jù),但這不算是幻讀,因?yàn)槭聞?wù)A已經(jīng)提交
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 1 | a | 9999 |
| 2 | b | 9999 |
| 3 | c | 9999 |
| 4 | d | 9999 |
| 5 | e | 5555 |
+----+------+---------+
5 rows in set (0.00 sec)
可以發(fā)現(xiàn),事務(wù)A正在執(zhí)行寫操作時,事務(wù)B是阻塞的(第一次測試事務(wù)B沒有阻塞是因?yàn)槭聞?wù)A沒有進(jìn)行寫操作),可見mysql的repeatable read隔離級別和Serializable是一樣的,不會產(chǎn)生幻讀!
serializable測試
筆者再次把事務(wù)隔離級別設(shè)置為serializable,實(shí)驗(yàn)結(jié)果與repeatable read的第二測試一樣。
再次證明,mysql的repeatable read隔離級別和Serializable是一樣的,不會產(chǎn)生幻讀!
參考文獻(xiàn):