深入剖析 MySQL 自增鎖
本文轉(zhuǎn)載自微信公眾號「SH的全棧筆記」,作者SH的全棧筆記。轉(zhuǎn)載本文請聯(lián)系SH的全棧筆記公眾號。
之前的文章把 InnoDB 中的所有的鎖都介紹了一下,包括意向鎖、記錄鎖...自增鎖巴拉巴拉的。但是后面我自己回過頭去看的時候發(fā)現(xiàn),對自增鎖的介紹居然才短短的一段。
其實自增鎖(AUTO-INC Locks)這塊還是有很多值得討論的細節(jié),例如在并發(fā)的場景下,InnoDB 是如何保證該值正確的進行自增的,本章就專門來簡單討論一下 InnoDB 中的自增鎖。
什么是自增鎖
之前我們提到過,自增鎖是一種比較特殊的表級鎖。并且在事務向包含了 AUTO_INCREMENT 列的表中新增數(shù)據(jù)時就會去持有自增鎖,假設(shè)事務 A 正在做這個操作,如果另一個事務 B 嘗試執(zhí)行 INSERT語句,事務 B 會被阻塞住,直到事務 A 釋放自增鎖。
這怎么說呢,說他對,但是他也不完全對。
行為與限制
其實上面說的那種阻塞情況只是自增鎖行為的其中一種,可以理解為自增鎖就是一個接口,其具體的實現(xiàn)有多種。具體的配置項為 innodb_autoinc_lock_mode ,通過這個配置項我們可以改變自增鎖中運行的一些細節(jié)。
并且,自增鎖還有一個限制,那就是被設(shè)置為 AUTO_INCREMENT 的列必須是索引,或者該列是索引的一部分(聯(lián)合索引),不過這個限制對于大部分開發(fā)場景下并沒有什么影響。
畢竟我們的基操不就是把 id 設(shè)置為 AUTO_INCREMENT 嗎。
鎖模式
其實在 InnoDB 中,把鎖的行為叫做鎖模式可能更加準確,那具體有哪些鎖模式呢,如下:
- 傳統(tǒng)模式(Traditional)
- 連續(xù)模式(Consecutive)
- 交叉模式(Interleaved)
分別對應配置項 innodb_autoinc_lock_mode 的值0、1、2.
看到這就已經(jīng)知道為啥上面說不準確了,因為三種模式下,InnoDB 對并發(fā)的處理是不一樣的,而且具體選擇哪種鎖模式跟你當前使用的 MySQL 版本還有關(guān)系。
在 MySQL 8.0 之前,InnoDB 鎖模式默認為連續(xù)模式,值為1,而在 MySQL 8.0 之后,默認模式變成了交叉模式。至于為啥會改變默認模式,后面會講。
傳統(tǒng)模式
傳統(tǒng)模式(Traditional),說白了就是還沒有鎖模式這個概念時,InnoDB 的自增鎖運行的模式。只是后面版本更新,InnoDB 引入了鎖模式的概念,然后 InnoDB 給了這種以前默認的模式一個名字,叫——傳統(tǒng)模式。
傳統(tǒng)模式具體是咋工作的?
我們知道,當我們向包含了 AUTO_INCREMENT 列的表中插入數(shù)據(jù)時,都會持有這么一個特殊的表鎖——自增鎖(AUTO-INC),并且當語句執(zhí)行完之后就會釋放。這樣一來可以保證單個語句內(nèi)生成的自增值是連續(xù)的。
這樣一來,傳統(tǒng)模式的弊端就自然暴露出來了,如果有多個事務并發(fā)的執(zhí)行 INSERT 操作,AUTO-INC的存在會使得 MySQL 的性能略有下降,因為同時只能執(zhí)行一條 INSERT 語句。
連續(xù)模式
連續(xù)模式(Consecutive)是 MySQL 8.0 之前默認的模式,之所以提出這種模式,是因為傳統(tǒng)模式存在影響性能的弊端,所以才有了連續(xù)模式。
在鎖模式處于連續(xù)模式下時,如果 INSERT 語句能夠提前確定插入的數(shù)據(jù)量,則可以不用獲取自增鎖,舉個例子,像 INSERT INTO 這種簡單的、能提前確認數(shù)量的新增語句,就不會使用自增鎖,這個很好理解,在自增值上,我可以直接把這個 INSERT 語句所需要的空間流出來,就可以繼續(xù)執(zhí)行下一個語句了。
但是如果 INSERT 語句不能提前確認數(shù)據(jù)量,則還是會去獲取自增鎖。例如像 INSERT INTO ... SELECT ... 這種語句,INSERT 的值來源于另一個 SELECT 語句。
連續(xù)模式的圖和交叉模式差不多
交叉模式
交叉模式(Interleaved)下,所有的 INSERT 語句,包含 INSERT 和 INSERT INTO ... SELECT ,都不會使用 AUTO-INC 自增鎖,而是使用較為輕量的 mutex 鎖。這樣一來,多條 INSERT 語句可以并發(fā)的執(zhí)行,這也是三種鎖模式中擴展性最好的一種。
并發(fā)執(zhí)行所帶來的副作用就是單個 INSERT 的自增值并不連續(xù),因為 AUTO_INCREMENT 的值分配會在多個 INSERT 語句中來回交叉的執(zhí)行。
優(yōu)點很明確,缺點是在并發(fā)的情況下無法保證數(shù)據(jù)一致性,這個下面會討論。
交叉模式缺陷
要了解缺陷是什么,還得先了解一下 MySQL 的 Binlog。Binlog 一般用于 MySQL 的數(shù)據(jù)復制,通俗一點就是用于主從同步。在 MySQL 中 Binlog 的格式有 3 種,分別是:
- Statement 基于語句,只記錄對數(shù)據(jù)做了修改的SQL語句,能夠有效的減少binlog的數(shù)據(jù)量,提高讀取、基于binlog重放的性能
- Row 只記錄被修改的行,所以Row記錄的binlog日志量一般來說會比Statement格式要多?;赗ow的binlog日志非常完整、清晰,記錄了所有數(shù)據(jù)的變動,但是缺點是可能會非常多,例如一條update語句,有可能是所有的數(shù)據(jù)都有修改;再例如alter table之類的,修改了某個字段,同樣的每條記錄都有改動。
- Mixed Statement和Row的結(jié)合,怎么個結(jié)合法呢。例如像alter table之類的對表結(jié)構(gòu)的修改,采用Statement格式。其余的對數(shù)據(jù)的修改例如update和delete采用Row格式進行記錄。
如果 MySQL 采用的格式為 Statement ,那么 MySQL 的主從同步實際上同步的就是一條一條的 SQL 語句。如果此時我們采用了交叉模式,那么并發(fā)情況下 INSERT 語句的執(zhí)行順序就無法得到保障。
可能你還沒看出問題在哪兒,INSERT 同時交叉執(zhí)行,并且 AUTO_INCREMENT 交叉分配將會直接導致主從之間同行的數(shù)據(jù)主鍵 ID 不同。而這對主從同步來說是災難性的。
換句話說,如果你的 DB 有主從同步,并且 Binlog 存儲格式為 Statement,那么不要將 InnoDB 自增鎖模式設(shè)置為交叉模式,會有問題。其實主從同步的過程遠比上圖中的復雜,之前我也寫過詳細的MySQL主從同步的文章,感興趣可以先去看看。
而后來,MySQL 將日志存儲格式從 Statement 變成了 Row,這樣一來,主從之間同步的就是真實的行數(shù)據(jù)了,而且 主鍵ID 在同步到從庫之前已經(jīng)確定了,就對同步語句的順序并不敏感,就規(guī)避了上面 Statement 的問題。
基于 MySQL 默認 Binlog 格式從 Statement 到 Row 的變更,InnoDB 也將其自增鎖的默認實現(xiàn)從連續(xù)模式,更換到了效率更高的交叉模式。
魚和熊掌
但是如果你的 MySQL 版本仍然默認使用連續(xù)模式,但同時又想要提高性能,該怎么辦呢?這個其實得做一些取舍。
如果你可以斷定你的系統(tǒng)后續(xù)不會使用 Binlog,那么你可以選擇將自增鎖的鎖模式從連續(xù)模式改為交叉模式,這樣可以提高 MySQL 的并發(fā)。并且,沒有了主從同步,INSERT 語句在從庫亂序執(zhí)行導致的 AUTO_INCREMENT 值不匹配的問題也就自然不會遇到了。
總結(jié)
你可能會說,為啥要了解這么深?有啥用?
其實還真有,例如在業(yè)務中你有一個需要執(zhí)行 幾十秒 的腳本,腳本中不停的調(diào)用多次 INSERT,這時就問你這個問題,在這幾十秒里,會阻塞其他的用戶使用對應的功能嗎?
如果你對自增鎖有足夠的了解,那么這個問題將會迎刃而解