深入剖析 MySQL 自增鎖
本文轉載自微信公眾號「SH的全棧筆記」,作者SH的全棧筆記。轉載本文請聯系SH的全棧筆記公眾號。
之前的文章把 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 語句,包含 INSERT 和 INSERT 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格式。其余的對數據的修改例如update和delete采用Row格式進行記錄。
如果 MySQL 采用的格式為 Statement ,那么 MySQL 的主從同步實際上同步的就是一條一條的 SQL 語句。如果此時我們采用了交叉模式,那么并發情況下 INSERT 語句的執行順序就無法得到保障。
可能你還沒看出問題在哪兒,INSERT 同時交叉執行,并且 AUTO_INCREMENT 交叉分配將會直接導致主從之間同行的數據主鍵 ID 不同。而這對主從同步來說是災難性的。
換句話說,如果你的 DB 有主從同步,并且 Binlog 存儲格式為 Statement,那么不要將 InnoDB 自增鎖模式設置為交叉模式,會有問題。其實主從同步的過程遠比上圖中的復雜,之前我也寫過詳細的MySQL主從同步的文章,感興趣可以先去看看。
而后來,MySQL 將日志存儲格式從 Statement 變成了 Row,這樣一來,主從之間同步的就是真實的行數據了,而且 主鍵ID 在同步到從庫之前已經確定了,就對同步語句的順序并不敏感,就規避了上面 Statement 的問題。
基于 MySQL 默認 Binlog 格式從 Statement 到 Row 的變更,InnoDB 也將其自增鎖的默認實現從連續模式,更換到了效率更高的交叉模式。
魚和熊掌
但是如果你的 MySQL 版本仍然默認使用連續模式,但同時又想要提高性能,該怎么辦呢?這個其實得做一些取舍。
如果你可以斷定你的系統后續不會使用 Binlog,那么你可以選擇將自增鎖的鎖模式從連續模式改為交叉模式,這樣可以提高 MySQL 的并發。并且,沒有了主從同步,INSERT 語句在從庫亂序執行導致的 AUTO_INCREMENT 值不匹配的問題也就自然不會遇到了。
總結
你可能會說,為啥要了解這么深?有啥用?
其實還真有,例如在業務中你有一個需要執行 幾十秒 的腳本,腳本中不停的調用多次 INSERT,這時就問你這個問題,在這幾十秒里,會阻塞其他的用戶使用對應的功能嗎?
如果你對自增鎖有足夠的了解,那么這個問題將會迎刃而解