淺談MySQL InnoDB鎖

基於MySQL 5.6.16

SQL92標準-事務級別:

  • 序列化:排它鎖

53977698.png

  • 可重複讀:讀寫鎖,讀讀並行,寫排他;因爲讀鎖和寫鎖都是記錄數,沒法鎖定不存在的記錄,因此沒法阻止插入,會出現幻讀。

54226896.png

  • 讀已提交:讀寫鎖,讀讀並行,讀寫並行(寫讀不能並行);事務1讀的時候,事務2能夠寫,事務2提交事務釋放鎖以後,事務1再讀,就會出現不可重複。

54347871.png

session1 session2
begin tx
select name from user where id = 7 -
- begin tx
- update user set name = 'chen' where id = 7
- commit tx
select name from user where id = 7 // 數據不一致 -
commit tx -
  • 讀未提交:寫鎖,讀讀並行,讀寫並行,寫讀並行;事務1讀的時候,事務2能夠寫,事務2還未提交事務,事務1還能夠再讀,就會出現髒讀。

54687993.png

session1 session2
begin tx -
select name from user where id = 7 -
- begin tx
- update user set name = 'chen' where id = 7
select name from user where id = 7 // 髒數據 -
commit tx -
- commit tx

以上爲已過期的處理事務的方式(92年被批准的標準),列出來是爲了引出共享鎖排它鎖的概念!html

鎖:共享鎖、排它鎖、意向共享鎖、意向排它鎖

select * from user where name = 'lin' for update

以上SQL是否有加鎖,對那些記錄加鎖?mysql

  • 當name爲主鍵時,鎖的是聚簇索引對應的記錄。
  • 當name爲惟一索引時,鎖的是惟一索引對應的記錄、聚簇索引對應的記錄。
  • 當name爲普通索引時,鎖的是普通索引對應的記錄和間隙聚簇索引對應的記錄。
  • 當name不是索引時,鎖的是整個表(每一條記錄上加記錄鎖和間隙鎖,實際上MySQL有作了優化,再加完鎖只有會排查並釋放掉不符合條件的記錄上的鎖,後面會講到)。
意向鎖是什麼?
  • 當name不是索引時,須要鎖住全部的記錄,那是否是要一條條記錄檢查是否有加鎖?這樣判斷的效率很是低。
  • 意向鎖是表級別的,當對記錄加鎖時,同時會在表上加上對應的意向鎖(共享鎖 -> 意向共享鎖,排它鎖 -> 意向排它鎖)。
  • 以上SQL,在加排他鎖時,發現表上若是已經有了意向鎖,就會被阻塞。
- 共享鎖(S) 排它鎖(X) 意向共享鎖(IS) 意向排它鎖(IX)
共享鎖(S) 兼容 不兼容 兼容 不兼容
排它鎖(X) 不兼容 不兼容 不兼容 不兼容
意向共享鎖(IS) 兼容 不兼容 兼容 兼容
意向排它鎖(IX) 不兼容 不兼容 兼容 兼容

還有其餘的「自增鎖」、「insert時候的隱式鎖」,本文不會說明,詳細可參考:解決死鎖之路 - 常見 SQL 語句的加鎖分析git

MVCC(Multi-Version Concurrent Control)

select * from user where name = 'lin'

以上是否有加鎖?
讀已提交可重複讀級別下,查詢使用了MVCC方式,是不加鎖的。
InnoDB 每一個彙集索引都有 4 個隱藏字段,分別是行ID(DB_ROW_ID,隱含的自增ID,若是數據表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引),最近更改的事務ID(DB_TRX_ID,每條記錄有獨立的事務ID,數據修改並提交成功的同時,會將事務ID修改爲當前事務ID,新ID確定會大於舊ID),undo Log 的指針(回滾指針DB_ROLL_PTR,記錄刪除的時候全局事務ID號,指向這條記錄在undo log上的回滾數據),刪除標記(記錄頭信息有專門的bit標誌,用來表示當前記錄是否已經被刪除,當刪除時,不會當即刪除,而是打標記,而後異步刪除)。
數據庫每次對數據進行更新操做時,會將修改前的數據保存到undo log中,經過undo log能夠實現事務回滾,而且能夠根據undo log回溯到某個特定的版本的數據,實現MVCCundo log分爲insertupdatedeleteupdate操做被歸成一類。
70810500.png
其中DB_ROLL_PTR長度爲7個字節(56個字節),數據結構以下:github

UNIV_INLINE
void
trx_undo_decode_roll_ptr(
/*=====================*/
    roll_ptr_t roll_ptr, /*!< in: roll pointer */
    ibool* is_insert, /*!< out: TRUE if insert undo log */
    ulint* rseg_id, /*!< out: rollback segment id */
    ulint* page_no, /*!< out: page number */
    ulint* offset) /*!< out: offset of the undo
                    entry within page */
{
#if DATA_ROLL_PTR_LEN != 7
# error "DATA_ROLL_PTR_LEN != 7"
#endif
#if TRUE != 1
# error "TRUE != 1"
#endif
    ut_ad(roll_ptr < (1ULL << 56));
    *offset = (ulint) roll_ptr & 0xFFFF; //獲取低16位 爲OFFSET
    roll_ptr >>= 16; //右移16位
    *page_no = (ulint) roll_ptr & 0xFFFFFFFF;//獲取32位爲 page no
    roll_ptr >>= 32;//右移32位
    *rseg_id = (ulint) roll_ptr & 0x7F;//獲取7位爲segment id
    roll_ptr >>= 7;//右移7位
    *is_insert = (ibool) roll_ptr; // TRUE==1 ,最高位,標識修改或插入
}
字節位置 字節長度 做用
55 1 操做類型:1=INSERT,0=UPDATE
48-54 7 undolog segment id
16-47 32 undolog 頁編號
0-15 16 undolog 頁上的偏移量
數據操做
  • 查詢:返回的記錄須要知足兩個條件。sql

    1. 當前數據未被刪除,或者刪除事務ID大於當前事務ID。
    2. 事務ID小於當前事務ID(下面會講到可見性)。
  • 插入:該操做生產的undo log僅僅用於事務回滾,一旦事務提交,就會被刪除。
  • 刪除:將舊版本信息記錄在undo log中,將數據標誌爲刪除(Deleted bit設置爲1,而不是直接刪除記錄),設置事務ID爲當前事務ID。
  • 修改:分爲兩種狀況,update的列是不是主鍵列。數據庫

    1. 若是不是主鍵列,將舊版本信息記錄在undo log中,設置當前記錄的事務ID爲當前事務ID。
    2. 若是是主鍵列,update分兩部執行:安全

      1. 將數據標誌爲刪除(Deleted bit設置爲1),設置事務ID爲當前事務ID。
      2. 插入新的記錄,新記錄的事務ID爲當前事務ID。
      3. 若是有二級索引,二級索引也須要作相應的更新(二級索引中包含主鍵項)。
undo log清理

mysql有後臺purge進程來刪除無用的undo log,按順序從老到新定時掃描undo log,直到徹底清除或者遇到一個不能清除的undo log。purge進程有本身的read view(等同於進程開始時最老的活動事務以前的view,trx_sys->mvcc->clone_oldest_view),保證清除的數據對任何事務來講都是不可見的。session

數據可見性

MySQL在RC級別下經過MVCC解決了髒讀,在RR級別下經過MVCC方案解決了髒讀、不可重複讀。數據結構

  • RR是事務級快照、RC是語句級快照。
  • RR:在一個事務內同一快照讀執行任意次數,獲得的數據一致;且只能讀到第一次執行前已經提交的數據或本事務內更改的數據。併發

    • 在InnoDB中,建立一個新事務的時候,InnoDB會將當前系統中的活躍事務列表(trx_sys->trx_list)建立一個副本(read view),副本中保存的是系統當前不該該被本事務看到的其餘事務id列表。當用戶在這個事務中要讀取該行記錄的時候,InnoDB會將該行當前的版本號與該read view進行比較。
    • 設該行的當前事務id爲trx_id,read view中最先的事務id爲trx_id_min, 最遲的事務id爲trx_id_max(trx_id_max是當前全部已提交的事務中最大XID+1)。

      1. 若是trx_id < trx_id_min的話,那麼代表該行記錄所在的事務已經在本次新事務建立以前就提交了,因此該行記錄的當前值是可見的。
      2. 若是trx_id > trx_id_max的話,那麼代表該行記錄所在的事務在本次新事務建立以後纔開啓,因此該行記錄的當前值不可見
      3. 若是trx_id_min <= trx_id <= trx_id_max,遍歷read view,查找trx_id是否在read view列表中:

        • 若是trx_idread view列表中,此記錄的最後一次修改在read_view建立時還沒有commit不可見
        • 若是trx_id不在read view列表中,此記錄在read_view建立以前已經commit可見
      4. 若是該行數據不可見,則從該行記錄的回滾指針DB_ROLL_PTR指向的Undo Log中取出對應的數據,而後從新從第一步開始判斷
      5. 須要注意的是,新建事務(當前事務)與正在內存中commit 的事務不在活躍事務鏈表中。
  • RC:每次快照讀均會建立新的read view

讀數據時,要不要開啓「讀事務」

  • 若是你一次執行單條查詢語句,則沒有必要啓用事務支持,數據庫默認支持SQL執行期間的讀一致性;
  • 若是你一次執行多條查詢語句,例如統計查詢,報表查詢,在這種場景下,多條查詢SQL必須保證總體的讀一致性,不然,在前條SQL查詢以後,後條SQL查詢以前,數據被其餘用戶改變,則該次總體的統計查詢將會出現讀數據不一致的狀態,此時,應該啓用事務支持。
間隙鎖

RR隔離級別下,MySQL經過MVCC + next-key(記錄鎖 + 間隙鎖(gap鎖))解決了幻讀,gap只跟insert衝突,gap之間不衝突

  • 快照讀:簡單的select操做(select * from user where name = 'lin'),屬於快照讀,不加鎖。
  • 當前讀:全部的鎖定讀都是當前讀,也就是讀取當前記錄的最新版本,不會利用 undo log 讀取鏡像。

    • select * from table where ? lock in share mode;
    • select * from table where ? for update;
    • insert into table values (…);
    • update table set ? where ?;
    • delete from table where ?;
    • 在Serializable級別下,全部的讀都是當前讀。
select * from user where age = 10 for update;

當age爲普通索引時,age索引加鎖以下:

1 5 10 12 15

「10」上會加上排它鎖,(5, 10) 和 (10, 12)間會加上間隙鎖。

RR模式是否解決了幻讀?這一點還存在爭議,好比github上的這一個爭議:https://github.com/Yhzhtk/not...

session1 session2
begin tx
select * from user where id = 7 -
- begin tx
- inset into user(id) values(7)
- commit tx
select * from user where id = 7 for update -
commit tx -

session1的兩次select查詢結果不一致。
對於這個爭議,要看對幻讀的定義,「快照讀當前讀的結果不一致」屬不屬於幻讀的範圍。官網定義的「The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. 」,在我看來「same query」應該是表示徹底同樣的sql,因此要麼都是當前讀,要麼都是快照讀,若是按這個理解來看,RR級別下就解決了幻讀。

簡易例子

摘自MySQL 加鎖處理分析

delete from t1 where id = 10;

這個SQL會加什麼鎖?
回答這個問題,咱們須要知道如下前提:

  1. 當前事務隔離級別是什麼?
  2. id是主鍵?惟一索引?普通索引?非索引?
  3. 是否存在id值爲10的數據?
1. id是主鍵 + RC:

0.9010662300042598.png
結論:id是主鍵時,此SQL只須要在id=10這條記錄上加X鎖便可。

2. id是惟一索引 + RC:

0.6305007944652574.png
結論:若id列是unique列,其上有unique索引。那麼SQL須要加兩個X鎖,一個對應於id unique索引上的id = 10的記錄,另外一把鎖對應於聚簇索引上的[name=’d’,id=10]的記錄。

3. id是普通索引 + RC:

0.6218706292642482.png
結論:若id列上有非惟一索引,那麼對應的全部知足SQL查詢條件的記錄,都會被加鎖。同時,這些記錄在主鍵索引上的記錄,也會被加鎖。

4. id是非索引字段 + RC:

0.9744838857543574.png
因爲id列上沒有索引,所以只能走聚簇索引,進行所有掃描。從圖中能夠看到,知足刪除條件的記錄有兩條,可是,聚簇索引上全部的記錄,都被加上了X鎖。不管記錄是否知足條件,所有被加上X鎖。既不是加表鎖,也不是在知足條件的記錄上加記錄鎖。
有人可能會問?爲何不是隻在知足條件的記錄上加鎖呢?這是因爲MySQL的實現決定的。若是一個條件沒法經過索引快速過濾,那麼存儲引擎層面就會將全部記錄加鎖後返回,而後由MySQL Server層進行過濾。所以也就把全部的記錄,都鎖上了。
注:在實際的實現中,MySQL有一些改進(semi-consistent read),在MySQL Server過濾條件,發現不知足後,會調用unlock_row方法,把不知足條件的記錄放鎖 (違背了2PL的約束)。這樣作,保證了最後只會持有知足條件記錄上的鎖,可是每條記錄的加鎖操做仍是不能省略的。
結論:若id列上沒有索引,SQL會走聚簇索引的全掃描進行過濾,因爲過濾是由MySQL Server層面進行的。所以每條記錄,不管是否知足條件,都會被加上X鎖。可是,爲了效率考量,MySQL作了優化,對於不知足條件的記錄,會在判斷後放鎖,最終持有的,是知足條件的記錄上的鎖,可是不知足條件的記錄上的加鎖/放鎖動做不會省略。同時,優化也違背了2PL的約束。

5. id是主鍵 + RR:與第1個同樣。
6. id是惟一索引 + RR:與第2個同樣。
7. id是普通索引 + RR:

0.5139588195803471.png

  • 與第3個區別在於,這多了一個間隙鎖(GAP鎖),並且GAP鎖不是加在記錄上的,而是加載兩條記錄之間的位置。
  • Insert操做,如insert [10,aa],首先會定位到[6,c]與[10,b]間,而後在插入前,會檢查這個GAP是否已經被鎖上,若是被鎖上,則Insert不能插入記錄。

結論:Repeatable Read隔離級別下,id列上有一個非惟一索引,對應SQL:delete from t1 where id = 10; 首先,經過id索引定位到第一條知足查詢條件的記錄,加記錄上的X鎖,加GAP上的GAP鎖,而後加主鍵聚簇索引上的記錄X鎖,而後返回;而後讀取下一條,重複進行。直至進行到第一條不知足條件的記錄[11,f],此時,不須要加記錄X鎖,可是仍舊須要加GAP鎖,最後返回結束。

8. id是非索引字段 + RR:

0.2613144302545063.png
如圖,這是一個很恐怖的現象。首先,聚簇索引上的全部記錄,都被加上了X鎖。其次,聚簇索引每條記錄間的間隙(GAP),也同時被加上了GAP鎖。這個示例表,只有6條記錄,一共須要6個記錄鎖,7個GAP鎖。試想,若是表上有1000萬條記錄呢?
在這種狀況下,這個表上,除了不加鎖的快照度,其餘任何加鎖的併發SQL,均不能執行,不能更新,不能刪除,不能插入,全表被鎖死。
結論:在Repeatable Read隔離級別下,若是進行全表掃描的當前讀,那麼會鎖上表中的全部記錄,同時會鎖上聚簇索引內的全部GAP,杜絕全部的併發 更新/刪除/插入 操做。固然,跟組合四:[id無索引, Read Committed]相似,這個狀況下,也能夠開啓semi-consistent read,來緩解加鎖開銷與併發影響,可是semi-consistent read自己也會帶來其餘問題,不建議使用。
注:(我在:MySQL版本號5.6.16,RR級別,表數據量爲223條記錄的狀況下作測試「update t set a = 'b'」,其中a是非索引字段,結果爲:在事務結束前,會鎖住全部的記錄,能夠經過「select * from information_schema.innodb_locks」看到,lock_model爲「X」,lock_type爲「RECORD」)。
注:就是所謂的semi-consistent read。semi-consistent read開啓的狀況下,對於不知足查詢條件的記錄,MySQL會提早放鎖。針對上面的這個用例,就是除了記錄[d,10],[g,10]以外,全部的記錄鎖都會被釋放,同時不加GAP鎖。semi-consistent read如何觸發:要麼是read committed隔離級別;要麼是Repeatable Read隔離級別,同時設置了 innodb_locks_unsafe_for_binlog 參數。詳細見:MySQL+InnoDB semi-consitent read原理及實現分析

9. id是普通索引 + RR + 沒有id=5這條記錄:

結果:會鎖住小於5到大於5這段的間隙,例如:{一、三、七、九、10}數據中會鎖住(3, 7)這段間隙。

一條相對複雜的SQL:

0.2508793326529062.png
結論:在Repeatable Read隔離級別下,針對一個複雜的SQL,首先須要提取其where條件。Index Key肯定的範圍,須要加上GAP鎖;Index Filter過濾條件,視MySQL版本是否支持ICP,若支持ICP,則不知足Index Filter的記錄,不加X鎖,不然須要X鎖;Table Filter過濾條件,不管是否知足,都須要加X鎖。
注:一個SQL中的where條件如何拆分?具體的介紹,建議閱讀SQL中的where條件,在數據庫中提取與應用淺析

  1. Index key:pubtime > 1 and puptime < 20。此條件,用於肯定SQL在idx_t1_pu索引上的查詢範圍。Index key:pubtime > 1 and puptime < 20。此條件,用於肯定SQL在idx_t1_pu索引上的查詢範圍。
  2. Index Filter:userid = ‘hdc’ 。此條件,能夠在idx_t1_pu索引上進行過濾,但不屬於Index Key。Index Filter:userid = ‘hdc’ 。此條件,能夠在idx_t1_pu索引上進行過濾,但不屬於Index Key。
  3. Table Filter:comment is not NULL。此條件,在idx_t1_pu索引上沒法過濾,只能在聚簇索引上過濾。
  4. 從圖中能夠看出,在Repeatable Read隔離級別下,由Index Key所肯定的範圍,被加上了GAP鎖;Index Filter鎖給定的條件 (userid = ‘hdc’)什麼時候過濾,視MySQL的版本而定:

    • 在MySQL 5.6版本以前,不支持Index Condition Pushdown(ICP),所以Index Filter在MySQL Server層過濾,即全部符合pubtime > 1 and pubtime < 20的數據都經過回表查出來以後,才作userid = ’hdc‘過濾。
    • 在5.6後支持了Index Condition Pushdown,則在index上過濾。
    • 若不支持ICP,不知足Index Filter的記錄,也須要加上記錄X鎖,若支持ICP,則不知足Index Filter的記錄,無需加記錄X鎖 (圖中,用紅色箭頭標出的X鎖,是否要加,視是否支持ICP而定);而Table Filter對應的過濾條件,則在聚簇索引中讀取後,在MySQL Server層面過濾,所以聚簇索引上也須要X鎖。最後,選取出了一條知足條件的記錄[8,hdc,d,5,good],可是加鎖的數量,要遠遠大於知足條件的記錄數量。

進階

MySQL加鎖規則裏面,包含了五個原則。

  1. 加鎖的基本單位是next-key lock,next-key lock 是前開後閉區間。

    select * from t where c > 12 and c < 16 for update;
    c索引時,假設 c的值有「1,3,11,15,17,20」,則加的鎖爲:(11, 15],(15, 17],即在(11,15)和(15, 17)上加間隙鎖,15和17上加記錄鎖。
  2. 訪問到的對象纔會加鎖。

    select id from t where c = 15 lock in share mode;
    c爲索引,id爲主鍵,加讀鎖時, 覆蓋索引優化狀況下, 不會訪問主鍵索引, 所以若是要經過 lock in share mode 給行加鎖避免數據被修改, 那就須要繞過索引優化, 如 select 一個不在索引中的值。
    但若是改爲 for update , 則 mysql 認爲接下來會更新數據, 所以會將對應主鍵索引也一塊兒鎖了。
  3. 索引上的等值查詢,給惟一索引加鎖的時候,next-key lock 退化爲記錄鎖。

    當c是惟一索引或主鍵,假設 c的值有「1,3,11,15,17,20」。
    select * from t where c = 15 for update;
    只會在15上加記錄鎖。
    select * from t where c >= 15 and c < 17 for update;
    雖然查出來的結果跟 =15是同樣的,可是加的鎖卻不同。Mysql定位到第一個符合條件的數據 15查詢第一個符合條件的數據是,經過樹搜索的方式定位記錄,用的是「等值查詢」的方法),因爲在(11, 15]上是等值查詢,因此退化成記錄鎖,總體加鎖是:記錄鎖15和next-key (15, 17]
  4. 索引上的等值查詢,向右遍歷時且最後一個值不知足等值條件的時候,next-key lock 退化爲間隙鎖。

    當c是普通索引,假設 c的值有「1,3,11,15,17,20」。
    select * from t where c = 11 for update;
    引擎在找到 c=11這一條索引項後繼續向右遍歷到 c=15這一條, 此時鎖範圍是 (3, 11], (11, 15)
  5. 範圍查詢會訪問到不知足條件的第一個值爲止。

    select * from t where c >= 11 and c <= 15;
    當c是惟一索引,假設 c的值有「1,3,11,15,17,20」。從常理上看,c是惟一索引,不會出現重複的數據,因此加的鎖應該爲爲「11,(11, 15]」。
    可是mysql在這種場景上,處理方式跟普通索引同樣,會繼續向後找第一個不知足條件的記錄,最終加的鎖是「11,(11, 15],(15, 17]」
示例

RR級別下,列id有如下幾條數據

id
5
10
15
20
25
30
id是主鍵
條件 說明
id = 1 因爲不存在1的值,會在第一個大於1的值上加next-key,根據規則4,等值查詢會退化成間隙鎖 (-∞, 5)
id < 5 根據規則5,向後查詢第一個不符合條件的值,在找到的值上加next-key (-∞, 5]
id = 5 根據規則3,惟一索引/主鍵上等值查詢,退化成記錄鎖 5
id <= 5 根據規則五,即便id是主鍵,也會繼續向後查找 (-∞, 5](5, 10]
id > 5 and id < 10 (5, 10]
id >= 5 and id < 10 規則三,惟一索引上等值查找,退化成記錄鎖 5, (5, 10]
id >= 5 and id <= 10 5, (5, 10], (10, 15]
id = 8 8記錄不存在,同第一個條件 (5, 10)
id = 10 10
id > 25 and id < 30 (25, 30]
id > 25 and id <= 30 數據末尾,會對正無窮大加鎖 (25, 30], (30, +∞]
id >= 30 數據末尾,會對正無窮大加鎖 30, (30, +∞]
id是普通索引
條件 說明
id = 10 普通索引跟惟一索引不同的點在於,普通索引是存在重複的可能性,<br/>因此即便等值查詢,也是按next-key加鎖,<br/>根據規則4,繼續向後查詢時,退化成間隙鎖 (5, 10], (10, 15)
id = 12 12記錄不存在 (10, 15)
id > 10 and id <= 15 (10, 15], (15, 20]
id >= 10 and id <= 15 (5, 10], (10, 15], (15, 20]
id不是索引
條件 說明
id =15 因爲id不是索引,在沒有開啓semi-consistent read狀況下會鎖住所有數據(RR級別默認不開啓) (-∞, 5], (5, 10], (10, 15], (15, 20], (20, 25], (25, 30], (30, +∞]
limit

RR級別下,普通索引id有如下幾條數據

id
5
10
10
10
15
30
delete from t where id = 10

鎖的是(5, 10], (10, 10], (10, 10], (10, 15)

delete from t where id = 10 limit 2

鎖的是(5, 10], (10, 10]。
在刪除數據的時候儘可能加 limit。這樣不只能夠控制刪除數據的條數,讓操做更安全,還能夠減少加鎖的範圍。

order by

索引搜索就是:找到第一個值,而後向左或向右遍歷。

  • order by 是用最小的值來找第一個。
  • order by desc 是用最大的值來找第一個。

RR級別下

id
5
10
15
20
25
30
select * from t where id >= 15 and id <= 20 order by id desc for update

若是id是主鍵索引,因爲order by id desc,因此從id <= 20開始等值查詢第一個符合id=20條件的數據,查找到20以後,按道理根據規則3,會退化成記錄鎖,但測試發現,還多鎖了個間隙鎖,這一點目前還沒在哪一個資料上找到對這個的說明,繼續向左查詢,找到最後符合id >= 15的數據15,根據規則5還會繼續查找到第一個不符合條件的數據10,因此最終的加鎖是(5, 10], (10, 15], (15, 20], (20, 25)
若是id是普通索引,以上SQL查找到20以後,根據規則4,會退化成間隙鎖,繼續向左查詢,找到最後符合id >= 15的數據15,根據規則5還會繼續查找到第一個不符合條件的數據10,因此最終的加鎖是(5, 10], (10, 15], (15, 20], (20,25)

select * from t where id >= 15 and id < 22 order by id desc for update

若是id是主鍵索引,因爲id=22不存在,找到(20, 25)這個間隙,因爲MySQL定位第一個值用的是等值查找,根據規則4,會遍歷到25且退化成間隙鎖,因此最終的加鎖是(5, 10], (10, 15], (15, 20], (20,25)

in

RR級別下,普通索引id有如下幾條數據

id
5
10
10
10
15
30
select * from t where id in (5, 10, 15) for update

MySQL是先加鎖id=5,在繼續id=10id=15,一個個加鎖上去的。

session1 session2
begin tx begin tx
select * from t where id in (5, 10, 15) for update select * from t where id in (5, 10, 15) order by id desc for update
commit tx commit tx

order by id desc致使session2是按順序從15, 10, 5加鎖,session1和session2加鎖順序相反,致使這兩條SQL可能會發生死鎖。

動態間隙鎖

RR級別下,主鍵索引id有如下幾條數據

id
5
10
15
20
25
30
select * from t where id  > 15 and id <= 20 for update

以上SQL的鎖是(15, 20], (20, 25],若是這時候記錄15被刪除(delete from t where id = 15),以上SQL的鎖會動態變成(10, 20], (20, 25],

其餘例子

RR級別下:
數據:

idx
2
5
9
6
14
15

事務:

session1 session2
begin tx begin tx
select * from user where idx = 3 for update select * from user where idx < 3 for update
commit tx commit tx

以上兩個事物互相不干擾,session1的鎖範圍是(2, 5),session2的鎖範圍是(2, 5],間隙鎖只會阻塞插入操做

session1 session2
begin tx begin tx
delete from user where idx = 7 delete from user where idx = 8
insert into user(idx) values (7) insert into user(idx) values (8)
commit tx commit tx

因爲idx不存在值爲7和8的記錄,session1和session2都持有(5, 9)間隙鎖,鎖只有在事務提以後的時候纔會釋放,此時出現兩個事務互相等待對方持有的間隙鎖而沒法插入,出現死鎖。
避免更新或者刪除不存在的記錄,容易致使死鎖問題。

Serializable級別做用

既然RR級別下已經不會出現幻讀,那爲何還須要Serializable:
防止數據丟失(被覆蓋):

session1 session2
begin tx
select name from user where id = 7 -
- begin tx
- update user set name = 'chen' where id = 7
- commit tx
update user set name = 'lin' where id = 7 -
commit tx -

防止出現幻覺(RR級別,id爲主鍵):

session1 session2
begin tx
select * from user where id = 7 // 發現數據不存在 -
- begin tx
- inset into user(id) values(7)
- commit tx
inset into user(id) values(7) // 報錯,主鍵衝突 -
select * from user where id = 7 // 仍然發現數據不存在 -
commit tx -
參考:
InnoDB多版本(MVCC)實現簡要分析
MySQL 加鎖處理分析
MySQL · 引擎特性 · InnoDB undo log 漫遊
SQL中的where條件,在數據庫中提取與應用淺析
MySQL 在 RC 隔離級別下是如何實現讀不阻塞的?
查看Mysql正在執行的事務、鎖、等待
數據庫分析手記 —— InnoDB鎖機制分析
爲何我只改一行的語句,鎖這麼多?
關於 MySQL 中 InnoDB 行鎖的理解及案例
相關文章
相關標籤/搜索