MySQL 自增主鍵為啥不是連續遞增


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

InnoDB引擎的自增值,在8.0版本之前,是保存在內存中的,根據內存中取值,才有了自增值持久化的能力,一旦發生重啟,表的自增值就會遍歷主鍵索引,獲取一個最大值+1做為下一個自增值,而在8.0版本中,將自增值的變更記錄存到了redo log中,重啟的時候就依靠redo log恢復重啟之前的值。

3、自增值修改機制

在MySQL中,如果字段id被定義為AUTO_INCREMENT時,在插入一行數據的時候:

  1. 如果插入數據的id為0、null或者未指定值,那么就把這個表當前的AUTO_INCREMENT值填到自增字段;
  2. 如果插入數據的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);

語句的執行流程如下:

  1. 執行器調用InnoDB引擎接口寫入一行數據,傳入的數據為(0,1,1);

  2. InnoDB 發現沒有指定自增id的值,獲取表t當前的自增值 AUTO_INCREMENT = 2;

  3. 將傳入的數據修改為(2,1,1);

  4. 將表的自增值修改為 AUTO_INCREMENT = 3;

  5. 執行插入操作,如果操作成功,則返回ok;如果出現唯一索引報錯,則報Duplicate key error,語句返回。

唯一鍵沖突之前,即語句真正執行插入操作之前,就已經把 AUTO_INCREMENT 修改為3了,這是id = 2 這一行並沒有插入成功,但也沒有將 AUTO_INCREMENT 改回去,之所以不改回了,是為了在事務並發執行時,能確保拿到最准確的自增值,同時,也是為了性能考慮。

4、自增值為什么不能回退

假設有兩個並行執行的事務,在申請自增值的時候,為了避免兩個事務申請到相同的id,肯定要加鎖,然后順序申請。

  1. 假設事務A申請了id=2,事務B申請了id=3,那么這時候表t的自增值是4,繼續執行

  2. 事務B先提交了,並且正確,但事務A出現了唯一鍵沖突。

  3. 如果允許事務A把自增id回退,那么表t當前的自增值將被改為2,就會出現表里面已經有id=3的行,而當前自增id值是2

  4. 接下來其他請求會陸續申請到id=2,id=3的情況,在執行插入語句時就會報“主鍵沖突”。

為了解決這個主鍵沖突,有兩種方法:

  1. 每次申請自增值的時候,先去表中判斷是否已經存在這個id,如果存在則跳過。本來申請id自增值是一個很快的操作,現在得去主鍵索引樹上判斷id是否存在,性能大打折扣。

  2. 把自增id的鎖范圍擴大,必須等到一個事務執行完成並提交,下一個事務才能再申請自增id,這個方法的問題是鎖的粒度太大,系統並發能力大大下降。

這兩個方法都會導致性能問題,是在假設允許”自增值回退“的前提下導致的。為了性能考慮InnoDB放棄了這個設計,使用最簡單的方式獲取自增值,即使語句執行失敗也不回退自增id,因此自增id是遞增的,但不保證是連續遞增。

5、自增鎖的優化

自增id鎖並不是一個事務鎖,而是每次申請完就馬上釋放,以便允許別的事務再申請。其實,在MySQL5.1版本之前,並不是這樣的。

在MySQL 5.0版本的時候,自增鎖的范圍是語句級別。也就是說,如果一個語句申請了一個表自增鎖,這個鎖會等

語句執行結束以后才釋放。顯然,這樣設計會影響並發度。

MySQL 5.1.22版本引入了一個新策略,新增參數innodb_autoinc_lock_mode,默認值是1.

  1. 這個參數的值被設置為0時,表示才用之前MySQL5.0版本的策略

  2. 這個參數的值被設置為1時:

    • 普通insert語句,自增鎖在申請之后就馬上釋放

    • 類似 insert...select 這樣的批量插入語句,自增鎖還是要等語句結束后才釋放

  3. 這個參數的值被設置為2時,所有的申請自增主鍵的動作都是申請后就釋放鎖。

疑問:為什么默認設置下,insert...select 要使用語句鎖?為什么這個參數的默認值不是2?

答案是為了數據一致性。

解疑場景:

 往表t1中插入了4行數據,然后創建了一個相同結構的表t2,然后兩個session同時執行向表t2中插入數據的操作。

如果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都是連續的,這時,這個庫就發生了數據不一致。

要解決這個問題,有兩種思路:

  1. 讓原庫的批量插入數據語句,固定生成連續的id值。所以,自增鎖直到語句執行結束才釋放,就是為了達到這個目的。

  2. 另一種是在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的策略:

  1. 語句執行過程中,第一次申請自增id,會分配1個;

  2. 1個用完以后,這個語句第二次申請自增id,會分配2個;

  3. 2個用完以后,還是這個語句,第三次申請自增id,會分配4個;

  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、總結

自增主鍵不能保持連續性

  • 唯一鍵沖突

  • 事務回滾

  • 自增主鍵的批量申請

深層次的原因是:不判斷自增主鍵是否已存在和減少加鎖的時間范圍和粒度,以追求更高的性能,因此自增主鍵不允許回退,所以自增主鍵不連續。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM