以下僅考慮 InnoDB 存儲引擎。
自增主鍵有兩個性質需要考慮:
- 單調性
每次插入一條數據,其 ID 都是比上一條插入的數據的 ID 大,就算上一條數據被刪除。 - 連續性
插入成功時,其數據的 ID 和前一次插入成功時數據的 ID 相鄰。
自增主鍵的單調性
爲何會有單調性的問題?
這主要跟自增主鍵最大值的獲取方式,以及存放位置有關係。
如果最大值是通過計算獲取的,並且在某些情況下需要重新獲取時,會因爲最新的數據被刪除而減小。
自增主鍵最大值怎麼取的?存放到哪裏?
MySQL 5.7 及之前的版本,自增主鍵最大值會在啓動(重啓)後從數據庫中取出放到內存:
SELECT MAX(ai_col) FROM table_name FOR UPDATE;
這樣獲取是通過計算的,並且由於存放在內存而容易丟失。
如果刪除最新一條數據(假設 ID 爲 10),因故障或者其他必要重啓後再插入一條數據時會使用之前的 ID (即 ID 爲 10)。
問題在於如果有其他表依賴了該 ID,則其他表的數據關聯到的數據就符合要求了。除非設置了外鍵。
比如我要向最大一個 ID 的賬號充了 100 萬。但是在充值之前,該賬號被刪除,然後服務器故障重啓,重啓後有人新註冊了一個賬號。結果我的 100 萬充到了他的新賬號上。註冊新賬號的人以爲是新手福利,笑嘻嘻。
如何解決單調性的問題?
從 MySQL 8.0 開始,自增主鍵最大值會在每次修改後寫入到 redo log,並且在每個檢查點寫入引擎私有的系統表。
- 如果是正常重啓,則讀取系統表裏的值。
- 如果是故障重啓,則先讀取系統表裏的值放到內存。接着掃描 redo log 裏存儲的值。如果掃描到的值大於內存的值,則將該值覆蓋到內存。
但由於數據庫可能在 redo log 刷入磁盤前就故障了,所以可能會用到之前申請的 ID。
注:如果 redo log 都沒刷入,就更不用說將數據插入數據表了。
InnoDB AUTO_INCREMENT Counter Initialization
https://dev.mysql.com/doc/refman/8.0/en/innodb-auto-increment-handling.html#innodb-auto-increment-initialization
自增主鍵插入時的連續性
這裏不考慮由於刪除導致的連續性問題
爲何會有連續性問題?
這主要是跟插入事務回滾有關係。
對於兩個插入事務,事務 A 先執行插入語句,之後事務 B 執行插入語句。在這之後,事務 A 回滾,導致 A 執行插入語句時佔用的 ID 被拋棄。
之所以事務 A 沒提交的情況下,事務 B 就能執行插入語句,跟 InnoDB 的自增長鎖(AUTO-INC Locking)相關。該鎖是一種特殊的表鎖(table-level lock),但會在插入語句執行後立即釋放,不會等到事務結束。
如何解決連續性問題?
使用最高隔離級別 SERIALIZABLE (串行)。
由於性能上的考慮,通常不這樣做。
多事務批量插入的連續性
事務 A 和事務 B 都在執行 不確定數量 的批量插入(INSERT ... SELECT):
- 保證事務 A 的數據的 ID 連續: innodb_autoinc_lock_mode = 0 (AUTO-INC Locking)
必須等待語句執行結束才釋放鎖。 - 保證事務 A 的數據的 ID 連續: innodb_autoinc_lock_mode = 1 (AUTO-INC Locking)
和上面的區別在於,當執行 確定數量 的批量插入時,使用輕量級互斥量(mutex)而不是特殊表鎖(AUTO-INC Locking),從而提前向內存的計數器申請相應數量的 ID。之後立即釋放,不用等語句執行結束。
會因爲回滾而使得全局 ID 不連續。 - 不保證事務 A 的數據的 ID 連續: innodb_autoinc_lock_mode = 2 (mutex)
三種插入定義:
- 簡單插入
能夠提前知道插入的行數 - 批量插入
不能提前知道插入的行數 - 混合插入
批量插入中的一部分的 ID 是指定的(非 0 且非 NULL),另一部分未指定,使用數據庫生成的自增 ID。
其他
如果主動指定 ID 爲 0 或者 NULL 插入,則會使用數據庫生成的自增 ID。
參考文檔
爲什麼 MySQL 的自增主鍵不單調也不連續
https://database.51cto.com/art/202004/614923.htm
《MySQL技術內幕——InnoDB存儲引擎》 第 6 章:鎖