事務(wù)專題(一)——數(shù)據(jù)庫事務(wù)

事務(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ā)性能越弱:

隔離級別和并發(fā)性能的關(guān)系圖

事務(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):

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 專業(yè)考題類型管理運(yùn)行工作負(fù)責(zé)人一般作業(yè)考題內(nèi)容選項(xiàng)A選項(xiàng)B選項(xiàng)C選項(xiàng)D選項(xiàng)E選項(xiàng)F正確答案 變電單選GYSZ本規(guī)程...
    小白兔去釣魚閱讀 10,507評論 0 13
  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 32,289評論 2 89
  • 你虛無縹緲的來, 如春風(fēng)拂面秋月撩人般不露痕跡, 助人上馬的契機(jī),悔恨一生的根…… 悄然而來,拂袖而去,了無蹤跡。...
    絲潤萬物閱讀 767評論 0 49
  • 當(dāng)大彩開心的時候,她說: 春風(fēng)得意馬蹄疾,一日看盡長安花。 我說: 哈哈哈哈哈哈哈哈哈哈…… 當(dāng)大彩傷心的時候,她...
    胡鑠閱讀 225評論 0 0
  • 引入相關(guān)maven依賴 <modelVersion>4.0.0</modelVersion> <groupI...
    囂張碼農(nóng)閱讀 284評論 1 1

友情鏈接更多精彩內(nèi)容