Abstract
在Mysql如何實現隔離級別 - 可重複讀和讀提交 源碼分析中介紹了MySQL中如何實現讀提交和可重複讀主要是通過版本鏈+ReadView組成的MVCC實現的. 但是其中還有一個很重要的點就是MySQL如何用可重複讀解決幻讀問題的. 本文將演示(1)MySQL的可重複讀中沒有幻讀, PostGreSQL有的. (2) MySQL中的鎖.
幻讀現象
什麼是幻讀
不同的隔離級別可能產生不同的"不穩定現象".
比如髒讀-- 讀到未提交的數據. 不可重複讀 – 對單條記錄的數據在同一事務中不能重複讀取. 幻讀 – 對於同一事務中的同一語句發現前後返回的記錄條數不一樣. 那麼多出來的或者消失的行在使用者看來就是"Phantom Records". 像影子一樣憑空出現了. 它一般發生在select … from xx where 或者 update from xxx where 這種.
MySQL怎麼解決的?
在MySQL的可重複讀隔離級別中解決了幻讀現象, 在開始試驗之前, 我們需要了解下MySQL中事務併發控制相關的第3個組件----鎖. 其他兩個分別是: 版本鏈 (存儲多個版本數據); ReadView (看到應該看到的數據); 而鎖則防止插入或者修改會產生錯誤的數據.
MySQL中的鎖
MySQL的鎖 大體上分爲行鎖和表鎖. 而行鎖又分爲如下幾種:
- LOCK_REC_NOT_GAP : 單個行記錄上的鎖
- LOCK_GAP: 間隙鎖, 鎖定一個範圍但是不包括記錄本身. 主要用來解決幻讀情況
- LOCK_ORDINARY : 鎖定一個範圍並鎖定記錄本身. 就是這裏的next-key lock. 注意值代表的含義是(上一個indexrecord, 當前record]
MySQL演示
假設如下的表tt (a int primary key, b int); 然後在b上有非唯一索引.
數據如下:
如下同時開始兩個會話, 並且設置當前session隔離級別爲repeatable read進行試驗.
set session transaction isolation level repeatable read
Session1 | Session2 |
---|---|
begin; select * from tt where b = 30 for update; |
|
嘗試: insert into tt values (29, 29); – 失敗 block insert into tt values (29, 31); --失敗block insert into tt values (29, 18); --成功 insert into tt values (31, 35); – 成功 insert into tt values (29, 20); – 成功 insert into tt values (31, 34); --失敗 |
看出來了嗎? 前面2次插入都失敗了. 但是後面2次都成功了. 這裏跟b的值有關係. 注意到我們會話一的select好像以某種gap鎖的方式鎖住了b值爲(20, 34]的範圍. 注意集合開閉.
如何驗證??
在MySQL 8.0 中可以通過如下查詢查到鎖住的數據和類型:
SELECT * FROM performance_schema.data_locks\G;
輸出如下:
mysql> SELECT * FROM performance_schema.data_locks\G;
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140392970423376:1064:140393245168136
ENGINE_TRANSACTION_ID: 19512
THREAD_ID: 49
EVENT_ID: 83
OBJECT_SCHEMA: testdb
OBJECT_NAME: tt
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140393245168136
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140392970423376:7:5:5:140393236938776
ENGINE_TRANSACTION_ID: 19512
THREAD_ID: 49
EVENT_ID: 83
OBJECT_SCHEMA: testdb
OBJECT_NAME: tt
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: tt_b
OBJECT_INSTANCE_BEGIN: 140393236938776
LOCK_TYPE: RECORD
LOCK_MODE: X
LOCK_STATUS: GRANTED
LOCK_DATA: 30, 30
*************************** 3. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140392970423376:7:4:5:140393236939128
ENGINE_TRANSACTION_ID: 19512
THREAD_ID: 49
EVENT_ID: 83
OBJECT_SCHEMA: testdb
OBJECT_NAME: tt
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140393236939128
LOCK_TYPE: RECORD
LOCK_MODE: X,REC_NOT_GAP
LOCK_STATUS: GRANTED
LOCK_DATA: 30
*************************** 4. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140392970423376:7:5:7:140393236939480
ENGINE_TRANSACTION_ID: 19512
THREAD_ID: 49
EVENT_ID: 83
OBJECT_SCHEMA: testdb
OBJECT_NAME: tt
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: tt_b
OBJECT_INSTANCE_BEGIN: 140393236939480
LOCK_TYPE: RECORD
LOCK_MODE: X,GAP
LOCK_STATUS: GRANTED
LOCK_DATA: 34, 44
第1行是table tt的IX 表鎖(for update導致的意向排它鎖).
第2行, 記錄索引tt_b上值爲30的排它鎖.
第3行,主鍵上的值爲30的排它鎖. – 爲啥會有2,3兩個鎖? 是因爲MySQL中的索引機制. 輔助索引和主鍵索引其實是分開存儲的. 這個暫時不說了.
第4行是一個GAP鎖, 鎖住了b=34, a=44的記錄.
關於GAP鎖
MySQL的文檔 說的還是比較複雜的. 比如上面的例子中取決於b是主鍵? 還是 有唯一索引? 有索引但是不是unique的? 或者沒有索引. 其實鎖住的記錄都不同.
如果是主鍵索引select * from tt where a = 30 for update
輸出如下:
mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140392970423376:1064:140393245168136
ENGINE_TRANSACTION_ID: 19520
THREAD_ID: 49
EVENT_ID: 87
OBJECT_SCHEMA: testdb
OBJECT_NAME: tt
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140393245168136
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140392970423376:7:4:5:140393236938776
ENGINE_TRANSACTION_ID: 19520
THREAD_ID: 49
EVENT_ID: 87
OBJECT_SCHEMA: testdb
OBJECT_NAME: tt
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140393236938776
LOCK_TYPE: RECORD
LOCK_MODE: X,REC_NOT_GAP
LOCK_STATUS: GRANTED
LOCK_DATA: 30
插入值爲(29, 29); (31, 29);的okay.
如果b是unique索引(與索引一致.)
mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140392970423376:1064:140393245168136
ENGINE_TRANSACTION_ID: 19554
THREAD_ID: 49
EVENT_ID: 102
OBJECT_SCHEMA: testdb
OBJECT_NAME: tt
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140393245168136
LOCK_TYPE: TABLE
LOCK_MODE: IX
LOCK_STATUS: GRANTED
LOCK_DATA: NULL
*************************** 2. row ***************************
ENGINE: INNODB
ENGINE_LOCK_ID: 140392970423376:7:4:5:140393236938776
ENGINE_TRANSACTION_ID: 19554
THREAD_ID: 49
EVENT_ID: 102
OBJECT_SCHEMA: testdb
OBJECT_NAME: tt
PARTITION_NAME: NULL
SUBPARTITION_NAME: NULL
INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140393236938776
LOCK_TYPE: RECORD
LOCK_MODE: X,REC_NOT_GAP
LOCK_STATUS: GRANTED
LOCK_DATA: 30
2 rows in set (0.01 sec)
總結
簡單總結下: (1) 單純的select 不會加鎖. 但是select for update 和 update xx where 這種都會有對應的GAP鎖添加. (2) 爲啥select * from tt where b = 30 for update 會鎖住臨近的記錄呢? 我覺得這個可能是出於代碼一致性的考慮. 不論是select for update或者update where這種, 爲了防止幻讀現象產生, 所以周圍的記錄被鎖住了. 因爲b列上不是唯一索引, 那麼我們如何防止別人的update b=30呢? 直接防止別人修改完事.