MySQL-事務中的一致性讀和鎖定讀的具體原理
前言
上一篇文章MySQL-InnoDB行鎖中,提到過一致性鎖定讀和一致性非鎖定讀,這篇文章會詳細分析一下在事務中時,具體是如何實現一致性的。
一致性讀原理
start transaction和begin語句,並不是立即開啟一個事務,事務是在第一條讀語句執行時才建立的。如果需要立即開啟事務,可以使用這個語句:start transaction with comsistent snapshot。
每一個事務都有一個唯一的事務id,在mysql系統中,這個事務id是唯一且遞增的。每一條資料庫記錄也有一個版本號,這個版本號記錄了修改記錄的事務id,如圖:
最新的版本是V4,修改它的事務id為25,依次往前為V3,事務id17,一直到V1,事務id為10。 資料庫中並不是真的有這些V1~V4的物理實體,是根據當前最新版本號和undolog往前計算出來每一個版本的。另外,資料庫記錄中除了儲存修改它的事務id以外,還會記錄這條修改是否已經提交。
在事務建立的一瞬間,當前事務會生成一個數組,儲存了當前時刻系統中所有的活躍事務id(未提交事務),按照從小到大順序排列,其中最小的id為低水位,最大的id為高水位。
那麼在讀操作和更新操作的時候,具體是如何使用這個版本號的呢?
我們知道,讀分為一致性鎖定讀和一致性非鎖定讀;更新操作,其實可以拆解為兩步,一步是一致性鎖定讀,一步是更新。我們只需要分析 一致性鎖定讀和一致性非鎖定讀就可以了。
- 如果是一致性非鎖定讀,能讀到的是低水位下的最近一個事務更新後的記錄。
- 如果是一致性鎖定讀,如果當前記錄被鎖定,需要等待鎖釋放;如果沒被鎖定,能讀到最新一個已提交記錄或者當前事務版本號對記錄的修改。
實驗驗證
準備一張表
create table t(id int,k int,primary key(id));
insert into t(id,k) values(1,1),(2,2),(3,3),(4,4);
事務的時間線圖如下
事務A:
mysql> start transaction with consistent snapshot; Query OK, 0 rows affected (0.00 sec) mysql> select * from t where id = 1; +----+------+ | id | k | +----+------+ | 1 | 1 | +----+------+ 1 row in set (0.00 sec)
事務B:
mysql> start transaction with consistent snapshot;
Query OK, 0 rows affected (0.00 sec)
mysql> update t set k=k+1 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from t where id = 1;
+----+------+
| id | k |
+----+------+
| 1 | 3 |
+----+------+
1 row in set (0.00 sec)
實驗結果分析
假設實驗開始前記錄的最新已提交版本事務id為90,事務A的id為99,事務B的id為100,事務C的id為101。
先分析B 在B查詢的時候,id=1記錄的最新版本為事務C更新的並且已經提交,事務B做的update操作,會被拆分成兩步:
- select * from t for update;
- update t set k=k+1;
第一步會在當前行上加X鎖,並且讀最新已提交的版本,雖然C記錄的事務id大於B,但是B會去讀取它,所以在第一步,B拿到了已經被事務C更新為2的資料。
第二步,事務B會在2的基礎上加一,把當前記錄更新為3,並且未提交,且事務版本號為事務B的100。
再分析A 在A查詢的時候,id=1記錄的最新版本為事務B更新的,並且未提交,所以事務A繼續往前找,直到找到事務id為90的已提交記錄讀取出來,所以事務A讀取到的為事務id=90更新的1。
場景實戰
併發減庫存的場景,目前庫存num=200,初始程式碼邏輯如下: select num from t where t > 0; update t set num = num -200;
有兩個併發的事務,事務A和事務B,在事務A執行到select語句後,事務B也執行到select,兩個事務都拿到了num=200,按照上面的語句繼續做更新操作,事務B結束後就會發現庫存num變成了負值,如何修改呢?
可以改成只寫一個update語句
update t set num = num - 200 where num >= 200
然後根據返回的影響行數做判斷,如果影響行數為0,說明庫存已經為0,需要做相關的後續業務處理。