1、前言
一般,我們在建表都會設一個自增主鍵,因為自增主鍵可以讓主鍵索引盡量地保持遞增順序插入,避免了頁分裂,使得索引樹更加緊湊。
自增主鍵保持着遞增順序插入,但如果依賴於自增主鍵的連續性,是會有問題的,因為自增主鍵並不能保證連續遞增。
2、主鍵自增值
創建一個測試表,然后插入一行數據,那么下一次插入數據的時候,自增主鍵的值就會是2;
CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `c` (`c`) ) ENGINE=InnoDB; insert into values(null,1,1); show create table t\G
3、自增值修改機制
在MySQL中,如果字段id被定義為AUTO_INCREMENT時,在插入一行數據的時候:
- 如果插入數據的id為0、null或者未指定值,那么就把這個表當前的AUTO_INCREMENT值填到自增字段;
- 如果插入數據的id指定了具體的值,就直接使用語句里指定的值。根據插入的值和當前自增值的大小關系,自增值的變更結果也會有所不同,如果插入的值X < 當前自增的值Y,那么表的自增值不變;否則表的自增值修改為新的自增值。
自增值生成算法:從auto_increment_offset 開始,以auto_increment_increment 為步長,持續疊加,直到找到第一個大於X的值,作為新的自增值。auto_increment_offset 和auto_increment_increment 兩個參數的默認值都是1。
自增值的修改流程:
假設表t中已有了(1,1,1)這條記錄,那么再執行一條插入數據命令時:
insert into values(null,1,1);
-
執行器調用InnoDB引擎接口寫入一行數據,傳入的數據為(0,1,1);
-
InnoDB 發現沒有指定自增id的值,獲取表t當前的自增值 AUTO_INCREMENT = 2;
-
將傳入的數據修改為(2,1,1);
-
將表的自增值修改為 AUTO_INCREMENT = 3;
-
執行插入操作,如果操作成功,則返回ok;如果出現唯一索引報錯,則報Duplicate key error,語句返回。
唯一鍵沖突之前,即語句真正執行插入操作之前,就已經把 AUTO_INCREMENT 修改為3了,這是id = 2 這一行並沒有插入成功,但也沒有將 AUTO_INCREMENT 改回去,之所以不改回了,是為了在事務並發執行時,能確保拿到最准確的自增值,同時,也是為了性能考慮。
4、自增值為什么不能回退
假設有兩個並行執行的事務,在申請自增值的時候,為了避免兩個事務申請到相同的id,肯定要加鎖,然后順序申請。
-
假設事務A申請了id=2,事務B申請了id=3,那么這時候表t的自增值是4,繼續執行
-
事務B先提交了,並且正確,但事務A出現了唯一鍵沖突。
-
如果允許事務A把自增id回退,那么表t當前的自增值將被改為2,就會出現表里面已經有id=3的行,而當前自增id值是2
-
接下來其他請求會陸續申請到id=2,id=3的情況,在執行插入語句時就會報“主鍵沖突”。
為了解決這個主鍵沖突,有兩種方法:
-
每次申請自增值的時候,先去表中判斷是否已經存在這個id,如果存在則跳過。本來申請id自增值是一個很快的操作,現在得去主鍵索引樹上判斷id是否存在,性能大打折扣。
-
把自增id的鎖范圍擴大,必須等到一個事務執行完成並提交,下一個事務才能再申請自增id,這個方法的問題是鎖的粒度太大,系統並發能力大大下降。
這兩個方法都會導致性能問題,是在假設允許”自增值回退“的前提下導致的。為了性能考慮InnoDB放棄了這個設計,使用最簡單的方式獲取自增值,即使語句執行失敗也不回退自增id,因此自增id是遞增的,但不保證是連續遞增。
5、自增鎖的優化
自增id鎖並不是一個事務鎖,而是每次申請完就馬上釋放,以便允許別的事務再申請。其實,在MySQL5.1版本之前,並不是這樣的。
在MySQL 5.0版本的時候,自增鎖的范圍是語句級別。也就是說,如果一個語句申請了一個表自增鎖,這個鎖會等
語句執行結束以后才釋放。顯然,這樣設計會影響並發度。
MySQL 5.1.22版本引入了一個新策略,新增參數innodb_autoinc_lock_mode,默認值是1.
-
這個參數的值被設置為0時,表示才用之前MySQL5.0版本的策略
-
這個參數的值被設置為1時:
-
普通insert語句,自增鎖在申請之后就馬上釋放
-
類似 insert...select 這樣的批量插入語句,自增鎖還是要等語句結束后才釋放
-
-
這個參數的值被設置為2時,所有的申請自增主鍵的動作都是申請后就釋放鎖。
疑問:為什么默認設置下,insert...select 要使用語句鎖?為什么這個參數的默認值不是2?
答案是為了數據一致性。
解疑場景:
如果sessionB是申請了自增值以后馬上就釋放自增鎖,那么就可能出現這樣的情況:
-
sessionB先插入了兩個記錄,(1,1,1)、(2,2,2)
-
然后 sessionA來申請自增id得到id=3,插入了(3,5,5)
-
之后 sessionB繼續執行,插入兩條記錄(4,3,3)、(5,4,4)
從數據邏輯上看,並沒有問題,因為語義本身並沒有要求t2的所有行的數據都跟sessionA相同。但是,如果我們現在的binlog_format=statement,binlog會怎么記錄?
由於兩個session是同時執行插入數據命令的,所有binlog對表t2的更新日志只有兩種情況:要么先記sessionA,要么先記sessionB。但不論是哪一種情況,這個binlog拿去從庫執行,或者用來恢復臨時實例,備庫和臨時實例里面,sessionB這個語句執行出來,生成的結果里,id都是連續的,這時,這個庫就發生了數據不一致。
要解決這個問題,有兩種思路:
-
讓原庫的批量插入數據語句,固定生成連續的id值。所以,自增鎖直到語句執行結束才釋放,就是為了達到這個目的。
-
另一種是在binlog里面把插入數據的操作都如實記錄進來,到備庫執行的時候,就不再依賴自增主鍵去生成。這種情況,其實就是innodb_autoinc_lock_mode 設置為2,同時binlog_format設置為row。
因此,在生產上,尤其是有insert...select 這種批量插入數據的場景時,從並發插入數據性能的角度考慮,建議這樣設置:innodb_autoinc_lock_mode = 2,並且binlog_format = row,這樣做,既能提升並發性,又不會出現數據一致性問題。
批量插入數據,包含的語句類型是 insert...select、replace...select 和 load data 語句。
但是,在普通的insert語句里面包含多個value值的情況下,即時innodb_autoinc_lock_mode設置為1,也不會等語句執行完成才釋放鎖,因為這類語句在申請自增id的時候,是可以精確計算出需要多少個id的,然后一次性申請完,申請完就可以釋放鎖了。而批量插入數據的語句,之所以需要這么設置,是因為不知道預先要申請多少個自增id。
對於批量插入數據的語句,假設insert...select 有10萬行數據,如果按照需要1個id就申請1次,那么就需要申請10萬次,顯然在大批量插入數據的情況下,不但速度慢,還會影響並發插入的性能。
MySQL有一個批量申請自增id的策略:
-
語句執行過程中,第一次申請自增id,會分配1個;
-
1個用完以后,這個語句第二次申請自增id,會分配2個;
-
2個用完以后,還是這個語句,第三次申請自增id,會分配4個;
-
依次類推,同一個語句去申請自增id,每次會申請到的自增id個數都是上一次的兩倍。
CREATE TABLE `t` ( `id` int(11) NOT NULL AUTO_INCREMENT, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `c` (`c`) ) ENGINE=InnoDB; insert into t values(null, 1,1); insert into t values(null, 2,2); insert into t values(null, 3,3); insert into t values(null, 4,4); create table t2 like t; insert into t2(c,d) select c,d from t; insert into t2 values(null, 5,5);
由於這種規則,t2表的批量插入就只用到了 id=1 到 id=4,id=8,而id=5 到id=7就被浪費掉了,因此自增主鍵的批量申請也會導致自增主鍵出現不連續的情況。
6、總結
自增主鍵不能保持連續性
-
唯一鍵沖突
-
事務回滾
-
自增主鍵的批量申請
深層次的原因是:不判斷自增主鍵是否已存在和減少加鎖的時間范圍和粒度,以追求更高的性能,因此自增主鍵不允許回退,所以自增主鍵不連續。