InnoDB 併發插入,如何使用自增鎖和意向鎖

併發的任務對同一個臨界資源進行操作,如果不採取措施,可能導致不一致,故必須進行併發控制(Concurrency Control)。InnoDB 存儲引擎中使用的多種併發控制策略,按照鎖的粒度劃分,可以分成行鎖和表鎖。

1. 併發控制

併發控制保證數據一致性的常見手段有:鎖(Locking)數據多版本(Multi Versioning)。樂觀鎖和悲觀鎖其實都是併發控制的機制,同時它們在原理上就有着本質的差別;

  • 樂觀鎖是一種思想,它其實並不是一種真正的『鎖』,它會先嚐試對資源進行修改,在寫回時判斷資源是否進行了改變,如果沒有發生改變就會寫回,否則就會進行重試,在整個的執行過程中其實都沒有對數據庫進行加鎖
  • 悲觀鎖就是一種真正的鎖了,它會在獲取資源前對資源進行加鎖,確保同一時刻只有有限的線程能夠訪問該資源,其他想要嘗試獲取資源的操作都會進入等待狀態,直到該線程完成了對資源的操作並且釋放了鎖後,其他線程才能重新操作資源;

1.1 樂觀鎖和悲觀鎖如何選擇呢?

樂觀鎖不會存在死鎖的問題,但是由於更新後驗證,所以當衝突頻率重試成本較高時更推薦使用悲觀鎖,而需要非常高的響應速度並且併發量非常大的時候使用樂觀鎖就能較好的解決問題,在這時使用悲觀鎖就可能出現嚴重的性能問題;在選擇併發控制機制時,需要綜合考慮上面的四個方面(衝突頻率、重試成本、響應速度和併發量)進行選擇。

2. 共享/排它鎖

簡單的鎖住太過粗暴,連“讀任務”也無法並行,任務執行過程本質上是串行的。

對數據的操作其實只有兩種,也就是讀和寫,而數據庫在實現鎖時,也會對這兩種操作使用不同的鎖;InnoDB 實現了標準的行級鎖,也就是共享鎖(Shared Lock)和互斥鎖(Exclusive Lock);共享鎖和互斥鎖的作用其實非常好理解:

  • 共享鎖(Share Locks,記爲S鎖),讀取數據時加S鎖
  • 排他鎖(eXclusive Locks,記爲X鎖),修改數據時加X鎖

鎖的兼容性上:

  • 共享鎖之間不互斥,即:讀讀可以並行
  • 排他鎖與任何鎖互斥,即:寫讀,寫寫不可以並行

所以我們可以在數據庫中並行讀,但是隻能串行寫,只有這樣才能保證不會發生線程競爭,實現線程安全。共享/排它鎖的潛在問題是,不能充分的並行,解決思路是數據多版本

3. 意向鎖

共享/排它鎖只是對某一個數據行進行加鎖,InnoDB 支持多種粒度的鎖,InnoDB 存儲引擎引入了意向鎖(Intention Lock),意向鎖就是一種表級鎖。它允許行級鎖與表級鎖共存,實際應用中,InnoDB使用的是意向鎖。

意向鎖是指,未來的某個時刻,事務可能要加共享/排它鎖了,先提前聲明一個意向。

如果沒有意向鎖,當已經有人使用行鎖對錶中的某一行進行修改時,如果另外一個請求要對全表進行修改,那麼就需要對所有的行是否被鎖定進行掃描,在這種情況下,效率是非常低的

意向鎖也分爲兩種:

  • 意向共享鎖(intention shared lock, IS):事務想要在獲得表中某些記錄的共享鎖,需要在表上先加意向共享鎖
  • 意向互斥鎖(intention exclusive lock, IX):事務想要在獲得表中某些記錄的互斥鎖,需要在表上先加意向互斥鎖

意向鎖協議(intention locking protocol)並不複雜,重點理解上面的加粗內容。由於意向鎖僅僅表明意向,它其實是比較弱的鎖(意向鎖其實不會阻塞全表掃描之外的任何請求,它們的主要目的是爲了表示是否有人請求鎖定表中的某一行數據),意向鎖之間並不相互互斥,而是可以並行,但它會與共享鎖/排它鎖互斥。

3.1 插入意向鎖

對已有數據行的修改與刪除,必須加互斥鎖X鎖,那對於數據的插入,是否還需要加這麼強的鎖,來實施互斥呢?插入意向鎖,孕育而生。

插入意向鎖,是間隙鎖(Gap Locks)的一種(所以,也是實施在索引上的),它是專門針對insert操作的。間隙鎖是對索引記錄中的一段連續區域的鎖,當使用類似 SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE; 的 SQL 語句時,就會阻止其他事務向表中插入 id = 15 的記錄,因爲整個範圍都被間隙鎖鎖定了。間隙鎖是存儲引擎對於性能和併發做出的權衡,並且只用於某些事務隔離級別。

除了間隙鎖外,InnoDB 還提供了另外兩種鎖算法。

記錄鎖(Record Lock)是加到索引記錄上的鎖。

如果我們使用 id 或者 last_name 作爲 SQL 中 WHERE 語句的過濾條件,那麼 InnoDB 就可以通過索引建立的 B+ 樹找到行記錄並添加索引,但是如果使用 first_name 作爲過濾條件時,由於 InnoDB 不知道待修改的記錄具體存放的位置,也無法對將要修改哪條記錄提前做出判斷就會鎖定整個表。

Next-Key 鎖,它是記錄鎖和記錄前的間隙鎖的結合,它作用其實是爲了解決幻讀的問題。

雖然間隙鎖中也分爲共享鎖和互斥鎖,不過它們之間並不是互斥的,也就是不同的事務可以同時持有一段相同範圍的共享鎖和互斥鎖,它唯一阻止的就是其他事務向這個範圍中添加新的記錄

//事務A先執行,在10與20兩條記錄中插入了一行,還未提交:
insert into t values(11, xxx);
//事務B後執行,也在10與20兩條記錄中插入了一行:
insert into t values(12, ooo);
/**
 * 雖然事務隔離級別是RR,雖然是同一個索引,雖然是同一個區間,但插入的記錄並不衝突,故這裏:
 * 1. 使用的是插入意向鎖
 * 2. 並不會阻塞事務B
**/

3.2 自增鎖

假如我們插入的數據中有AUTO_INCREMENT列,InnoDB在RR隔離級別下,能解決幻讀問題。

/**
 * 數據表:t(id AUTO_INCREMENT, name);
 * 數據:
 * 1, shenjian
 * 2, zhangsan
 * 3, lisi
**/
//事務A先執行,還未提交:
insert into t(name) values(xxx);
//事務B後執行:
insert into t(name) values(ooo);

1. 事務A先執行insert,會得到一條(4, xxx)的記錄,由於是自增列,InnoDB會自動增長,注意此時事務並未提交

2. 事務B後執行insert,假設不會被阻塞,那會得到一條(5, ooo)的記錄;

3. 事務A繼續insert:會得到一條(6, xxoo)的記錄。

4. 事務A再select:select * from t where id>3,得到(4, xxx)(6, xxoo)

咦,這對於事務A來說,就很奇怪了,對於AUTO_INCREMENT的列,連續插入了兩條記錄

自增鎖是一種特殊的表級別鎖(table-level lock),專門針對事務插入AUTO_INCREMENT類型的列。最簡單的情況,如果一個事務正在往表中插入記錄,所有其他事務的插入必須等待,以便第一個事務插入的行,是連續的主鍵值。與此同時,InnoDB提供了innodb_autoinc_lock_mode配置,可以調節與改變該鎖的模式與行爲。

4. 事務隔離級別

ISO 和 ANIS SQL 標準制定了四種事務隔離級別,而 InnoDB 遵循了 SQL:1992 標準中的四種隔離級別:READ UNCOMMITEDREAD COMMITEDREPEATABLE READSERIALIZABLE;每個事務的隔離級別其實都比上一級多解決了一個問題:

RAED UNCOMMITED 使用查詢語句不會加鎖,可能會讀到未提交的行(Dirty Read)
READ COMMITED 只對記錄加記錄鎖,而不會在記錄之間加間隙鎖,所以允許新的記錄插入到被鎖定記錄的附近,所以再多次使用查詢語句時,可能得到不同的結果(Non-Repeatable Read);
REPEATABLE READ 多次讀取同一範圍的數據會返回第一次查詢的快照,不會返回不同的數據行,但是可能發生幻讀(Phantom Read);它是默認的事務隔離級別
SERIALIZABLE InnoDB 隱式地將全部的查詢語句加上共享鎖,解決了幻讀的問題

引用資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章