深入剖析 MySQL 自增鎖

detectiveHLH發表於2021-05-31

之前的文章把 InnoDB 中的所有的鎖都介紹了一下,包括意向鎖、記錄鎖...自增鎖巴拉巴拉的。但是後面我自己回過頭去看的時候發現,對自增鎖的介紹居然才短短的一段。

其實自增鎖(AUTO-INC Locks)這塊還是有很多值得討論的細節,例如在併發的場景下,InnoDB 是如何保證該值正確的進行自增的,本章就專門來簡單討論一下 InnoDB 中的自增鎖。

什麼是自增鎖

之前我們提到過,自增鎖是一種比較特殊的表級鎖。並且在事務向包含了 AUTO_INCREMENT 列的表中新增資料時就會去持有自增鎖,假設事務 A 正在做這個操作,如果另一個事務 B 嘗試執行 INSERT語句,事務 B 會被阻塞住,直到事務 A 釋放自增鎖。

這怎麼說呢,說他對,但是他也不完全對。

行為與限制

其實上面說的那種阻塞情況只是自增鎖行為的其中一種,可以理解為自增鎖就是一個介面,其具體的實現有多種。具體的配置項為 innodb_autoinc_lock_mode ,通過這個配置項我們可以改變自增鎖中執行的一些細節。

並且,自增鎖還有一個限制,那就是被設定為 AUTO_INCREMENT 的列必須是索引,或者該列是索引的一部分(聯合索引),不過這個限制對於大部分開發場景下並沒有什麼影響。

畢竟我們的基操不就是把 id 設定為 AUTO_INCREMENT 嗎。

鎖模式

其實在 InnoDB 中,把鎖的行為叫做鎖模式可能更加準確,那具體有哪些鎖模式呢,如下:

  • 傳統模式(Traditional)
  • 連續模式(Consecutive)
  • 交叉模式(Interleaved)

分別對應配置項 innodb_autoinc_lock_mode 的值0、1、2.

看到這就已經知道為啥上面說不準確了,因為三種模式下,InnoDB 對併發的處理是不一樣的,而且具體選擇哪種鎖模式跟你當前使用的 MySQL 版本還有關係。

在 MySQL 8.0 之前,InnoDB 鎖模式預設為連續模式,值為1,而在 MySQL 8.0 之後,預設模式變成了交叉模式。至於為啥會改變預設模式,後面會講。

傳統模式

傳統模式(Traditional),說白了就是還沒有鎖模式這個概念時,InnoDB 的自增鎖執行的模式。只是後面版本更新,InnoDB 引入了鎖模式的概念,然後 InnoDB 給了這種以前預設的模式一個名字,叫——傳統模式。

傳統模式具體是咋工作的?

我們知道,當我們向包含了 AUTO_INCREMENT 列的表中插入資料時,都會持有這麼一個特殊的表鎖——自增鎖(AUTO-INC),並且當語句執行完之後就會釋放。這樣一來可以保證單個語句內生成的自增值是連續的。

這樣一來,傳統模式的弊端就自然暴露出來了,如果有多個事務併發的執行 INSERT 操作,AUTO-INC的存在會使得 MySQL 的效能略有下降,因為同時只能執行一條 INSERT 語句。

連續模式

連續模式(Consecutive)是 MySQL 8.0 之前預設的模式,之所以提出這種模式,是因為傳統模式存在影響效能的弊端,所以才有了連續模式。

在鎖模式處於連續模式下時,如果 INSERT 語句能夠提前確定插入的資料量,則可以不用獲取自增鎖,舉個例子,像 INSERT INTO 這種簡單的、能提前確認數量的新增語句,就不會使用自增鎖,這個很好理解,在自增值上,我可以直接把這個 INSERT 語句所需要的空間流出來,就可以繼續執行下一個語句了。

但是如果 INSERT 語句不能提前確認資料量,則還是會去獲取自增鎖。例如像 INSERT INTO ... SELECT ... 這種語句,INSERT 的值來源於另一個 SELECT 語句。

連續模式的圖和交叉模式差不多

交叉模式

交叉模式(Interleaved)下,所有的 INSERT 語句,包含 INSERTINSERT INTO ... SELECT ,都不會使用 AUTO-INC 自增鎖,而是使用較為輕量的 mutex 鎖。這樣一來,多條 INSERT 語句可以併發的執行,這也是三種鎖模式中擴充套件性最好的一種。

併發執行所帶來的副作用就是單個 INSERT 的自增值並不連續,因為 AUTO_INCREMENT 的值分配會在多個 INSERT 語句中來回交叉的執行。

優點很明確,缺點是在併發的情況下無法保證資料一致性,這個下面會討論。

交叉模式缺陷

要了解缺陷是什麼,還得先了解一下 MySQL 的 Binlog。Binlog 一般用於 MySQL 的資料複製,通俗一點就是用於主從同步。在 MySQL 中 Binlog 的格式有 3 種,分別是:

  • Statement 基於語句,只記錄對資料做了修改的SQL語句,能夠有效的減少binlog的資料量,提高讀取、基於binlog重放的效能
  • Row 只記錄被修改的行,所以Row記錄的binlog日誌量一般來說會比Statement格式要多。基於Row的binlog日誌非常完整、清晰,記錄了所有資料的變動,但是缺點是可能會非常多,例如一條update語句,有可能是所有的資料都有修改;再例如alter table之類的,修改了某個欄位,同樣的每條記錄都有改動。
  • Mixed Statement和Row的結合,怎麼個結合法呢。例如像alter table之類的對錶結構的修改,採用Statement格式。其餘的對資料的修改例如updatedelete採用Row格式進行記錄。

如果 MySQL 採用的格式為 Statement ,那麼 MySQL 的主從同步實際上同步的就是一條一條的 SQL 語句。如果此時我們採用了交叉模式,那麼併發情況下 INSERT 語句的執行順序就無法得到保障。

可能你還沒看出問題在哪兒,INSERT 同時交叉執行,並且 AUTO_INCREMENT 交叉分配將會直接導致主從之間同行的資料主鍵 ID 不同。而這對主從同步來說是災難性的。

換句話說,如果你的 DB 有主從同步,並且 Binlog 儲存格式為 Statement,那麼不要將 InnoDB 自增鎖模式設定為交叉模式,會有問題。其實主從同步的過程遠比上圖中的複雜,之前我也寫過詳細的MySQL主從同步的文章,感興趣可以先去看看。

而後來,MySQL 將日誌儲存格式從 Statement 變成了 Row,這樣一來,主從之間同步的就是真實的行資料了,而且 主鍵ID 在同步到從庫之前已經確定了,就對同步語句的順序並不敏感,就規避了上面 Statement 的問題。

基於 MySQL 預設 Binlog 格式從 StatementRow 的變更,InnoDB 也將其自增鎖的預設實現從連續模式,更換到了效率更高的交叉模式

魚和熊掌

但是如果你的 MySQL 版本仍然預設使用連續模式,但同時又想要提高效能,該怎麼辦呢?這個其實得做一些取捨。

如果你可以斷定你的系統後續不會使用 Binlog,那麼你可以選擇將自增鎖的鎖模式從連續模式改為交叉模式,這樣可以提高 MySQL 的併發。並且,沒有了主從同步,INSERT 語句在從庫亂序執行導致的 AUTO_INCREMENT 值不匹配的問題也就自然不會遇到了。

總結

你可能會說,為啥要了解這麼深?有啥用?

其實還真有,例如在業務中你有一個需要執行 幾十秒 的指令碼,指令碼中不停的呼叫多次 INSERT,這時就問你這個問題,在這幾十秒裡,會阻塞其他的使用者使用對應的功能嗎?

如果你對自增鎖有足夠的瞭解,那麼這個問題將會迎刃而解。

好了以上就是本篇部落格的全部內容了,歡迎微信搜尋關注【SH的全棧筆記】,回覆【佇列】獲取MQ學習資料,包含基礎概念解析和RocketMQ詳細的原始碼解析,持續更新中。

如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

相關文章