mysql自增_面試官:為什么 MySQL 的自增主鍵不單調也不連續?

1d55714bdcfcb91742e31cdc6691bd5d.png

為什么這么設計(Why’s THE Design)是一系列關于計算機領域中程序設計決策的文章,我們在這個系列的每一篇文章中都會提出一個具體的問題并從不同的角度討論這種設計的優缺點、對具體實現造成的影響。如果你有想要了解的問題,可以在文章下面留言。

當我們在使用關系型數據庫時,主鍵(Primary Key)是無法避開的概念,主鍵的作用就是充當記錄的標識符,我們能夠通過標識符在一張表中定位到唯一的記錄,作者在 為什么總是需要無意義的 ID 曾經介紹過為什么不應該使用有意義的字段來充當唯一標識符,感興趣的讀者可以了解一下。

在關系型數據庫中,我們會選擇記錄中多個字段的最小子集作為該記錄在表中的唯一標識符[^1],根據關系型數據庫對主鍵的定義,我們既可以選擇單個列作為主鍵,也可以選擇多個列作為主鍵,但是主鍵在整個記錄中必須存在并且唯一。最常見的方式當然是使用 MySQL 默認的自增 ID 作為主鍵,雖然使用其他策略設置的主鍵也是合法的,但是不是通用的以及推薦的做法。

f37603a45a55aebe50040d8e7e9d2d17.png
圖 1 - MySQL 的主鍵

MySQL 中默認的 AUTO_INCREMENT 屬性在多數情況下可以保證主鍵的連續性,我們通過 show create table 命令可以在表的定義中能夠看到 AUTO_INCREMENT 屬性的當前值,當我們向當前表中插入數據時,它會使用該屬性的值作為插入記錄的主鍵,而每次獲取該值也都會將它加一。

CREATE TABLE `trades` (  `id` bigint(20) NOT NULL AUTO_INCREMENT,  ...  `created_at` timestamp NULL DEFAULT NULL,  PRIMARY KEY (`id`),) ENGINE=InnoDB AUTO_INCREMENT=17130 DEFAULT CHARSET=utf8mb4

在很多開發者的認知中,MySQL 的主鍵都應該是單調遞增的,但是在我們與 MySQL 打交道的過程中會遇到兩個問題,首先是記錄的主鍵并不連續,其次是可能會創建多個主鍵相同的記錄,我們將從以下的兩個角度回答 MySQL 不單調和不連續的原因:

  • 較早版本的 MySQL 將 AUTO_INCREMENT 存儲在內存中,實例重啟后會根據表中的數據重新設置該值;
  • 獲取 AUTO_INCREMENT 時不會使用事務鎖,并發的插入事務可能出現部分字段沖突導致插入失敗;

需要注意的是,我們在這篇文章中討論的是 MySQL 中最常見的 InnoDB 存儲引擎,MyISAM 等其他引擎提供的 AUTO_INCREMENT 實現原理不在本文的討論范圍中。

刪除記錄

AUTO_INCREMENT 屬性雖然在 MySQL 中十分常見,但是在較早的 MySQL 版本中,它的實現還比較簡陋,InnoDB 引擎會在內存中存儲一個整數表示下一個被分配到的 ID,當客戶端向表中插入數據時會獲取 AUTO_INCREMENT 值并將其加一。

d2716fbfc998ca5bc7e852d01ad01412.png
圖 2 - AUTO_INCREMENT 的使用

因為該值存儲在內存中,所以在每次 MySQL 實例重新啟動后,當客戶端第一次向 table_name 表中插入記錄時,MySQL 會使用如下所示的 SQL 語句查找當前表中 id 的最大值,將其加一后作為待插入記錄的主鍵,并作為當前表中 AUTO_INCREMENT 計數器的初始值[^2]。

SELECT MAX(ai_col) FROM table_name FOR UPDATE;

如果讓作者實現 AUTO_INCREMENT,在最開始也會使用這種方法。不過這種實現雖然非常簡單,但是如果使用者不嚴格遵循關系型數據庫的設計規范,就會出現如下所示的數據不一致的問題:

77360ee6f7300b41370f123927a115c1.png
圖 3 - 5.7 版本之前的 AUTO_INCMRENT

因為重啟了 MySQL 的實例,所以內存中的 AUTO_INCREMENT 計數器會被重置成表中的最大值,當我們再向表中插入新的 trades 記錄時會重新使用 10 作為主鍵,主鍵也就不是單調的了。在新的 trades 記錄插入之后,executions 表中的記錄就錯誤的引用了新的 trades,這其實是一個比較嚴重的錯誤。

然而這也不完全是 MySQL 的問題,如果我們嚴格遵循關系型數據庫的設計規范,使用外鍵處理不同表之間的聯系,就可以避免上述問題,因為當前 trades 記錄仍然有外部的引用,所以外鍵會禁止 trades 記錄的刪除,不過多數公司內部的 DBA 都不推薦或者禁止使用外鍵,所以確實存在出現這種問題的可能。

然而在 MySQL 8.0 中,AUTO_INCREMENT 計數器的初始化行為發生了改變,每次計數器的變化都會寫入到系統的重做日志(Redo log)并在每個檢查點存儲在引擎私有的系統表中[^3]。

In MySQL 8.0, this behavior is changed. The current maximum auto-increment counter value is written to the redo log each time it changes and is saved to an engine-private system table on each checkpoint. These changes make the current maximum auto-increment counter value persistent across server restarts.

當 MySQL 服務被重啟或者處于崩潰恢復時,它可以從持久化的檢查點和重做日志中恢復出最新的 AUTO_INCREMENT 計數器,避免出現不單調的主鍵也解決了這里提到的問題。

并發事務

為了提高事務的吞吐量,MySQL 可以處理并發執行的多個事務,但是如果并發執行多個插入新記錄的 SQL 語句,可能會導致主鍵的不連續。如下圖所示,事務 1 向數據庫中插入 id = 10 的記錄,事務 2 向數據庫中插入 id = 11id = 12 的兩條記錄:

b366c89eed157c7672a324b99e91589d.png
圖 4 - 并發事務的執行

不過如果在最后事務 1 由于插入的記錄發生了唯一鍵沖突導致了回滾,而事務 2 沒有發生錯誤而正常提交,在這時我們會發現當前表中的主鍵出現了不連續的現象,后續新插入的數據也不再會使用 10 作為記錄的主鍵。

3abf0fd7b5c0e6538cc9677078e3dbf4.png
圖 5 - 不連續的主鍵

這個現象背后的原因也很簡單,雖然在獲取 AUTO_INCREMENT 時會加鎖,但是該鎖是語句鎖,它的目的是保證 AUTO_INCREMENT 的獲取不會導致線程競爭,而不是保證 MySQL 中主鍵的連續[^4]。

上述行為是由 InnoDB 存儲引擎提供的 innodb_autoinc_lock_mode 配置控制的,該配置決定了獲取 AUTO_INCREMENT 計時器時需要先得到的鎖,該配置存在三種不同的模式,分別是傳統模式(Traditional)、連續模式(Consecutive)和交叉模式(Interleaved)[^5],其中 MySQL 使用連續模式作為默認的鎖模式:

  • 傳統模式 innodb_autoinc_lock_mode = 0
    • 在包含 AUTO_INCREMENT 屬性的表中插入數據時,所有INSERT 語句都會獲取表級別AUTO_INCREMENT 鎖,該鎖會在當前語句執行后釋放;
  • 連續模式 innodb_autoinc_lock_mode = 1
    • INSERT ... SELECTREPLACE ... SELECT ?以及 LOAD DATA 等批量的插入操作需要獲取表級別AUTO_INCREMENT 鎖,該鎖會在當前語句執行后釋放;
    • 簡單的插入語句(預先知道插入多少條記錄的語句)只需要獲取獲取 AUTO_INCREMENT 計數器的互斥鎖并在獲取主鍵后直接釋放,不需要等待當前語句執行完成;
  • 交叉模式 innodb_autoinc_lock_mode = 2
    • 所有的插入語句都不需要獲取表級別AUTO_INCREMENT 鎖,但是當多個語句插入的數據行數不確定時,可能存在分配相同主鍵的風險;

這三種模式都不能解決 MySQL 自增主鍵不連續的問題,想要解決這個問題的終極方案是串行執行所有包含插入操作的事務,也就是使用數據庫的最高隔離級別 —— 可串行化(Serialiable)。當然直接修改數據庫的隔離級別相對來說有些簡單粗暴,基于 MySQL 或者其他存儲系統實現完全串行的插入也可以保證主鍵在插入時的連續,但是仍然不能避免刪除數據導致的不連續。

總結

早期 MySQL 的主鍵既不是單調的,也不是連續的,這些都是在當時工程上做出的一些選擇,如果嚴格地按照關系型數據庫的設計規范,MySQL 最初的設計造成問題的概率也比較低,只有當被刪除的主鍵被外部系統引用時才會影響數據的一致性,但是今天使用方式的不同卻增加出錯的可能性,而 MySQL 也在 8.0 中持久化了 AUTO_INCREMENT 以避免該問題的出現。

MySQL 中不連續的主鍵又是一個工程設計向性能低頭的例子,犧牲主鍵的連續性來支持數據的并發插入,最終提高了 MySQL 服務的吞吐量,作者在幾年前剛剛使用 MySQL 時就遇到過這個問題,但是當時并沒有深究背后的原因,今天重新理解該問題背后的設計決策也是個非常有趣的過程。我們在這里簡單總結一下本文的內容,重新回到今天的問題 — 為什么 MySQL 的自增主鍵不單調也不連續:

  • MySQL 5.7 版本之前在內存中存儲 AUTO_INCREMENT 計數器,實例重啟后會根據表中的數據重新設置,在刪除記錄后重啟就可能出現重復的主鍵,該問題在 8.0 版本使用重做日志解決,保證了主鍵的單調性;
  • MySQL 插入數據獲取 AUTO_INCREMENT 時不會使用事務鎖,而是會使用互斥鎖,并發的插入事務可能出現部分字段沖突導致插入失敗,想要保證主鍵的連續需要串行地執行插入語句;

到最后,我們還是來看一些比較開放的相關問題,有興趣的讀者可以仔細思考一下下面的問題:

  • MyISAM 和其他的存儲引擎如何存儲 AUTO_INCREMENT 計數器?
  • MySQL 中的 auto_increment_incrementauto_increment_offset 是用來做什么的?

c4ba1f1362455b3454f0cab86a3d7df2.png

求關注

求轉發

f7c6c0bca157073952a0994c5fa2d187.png71ac999a1a2f454853ddafe93cde65dd.png

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/258666.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/258666.shtml
英文地址,請注明出處:http://en.pswp.cn/news/258666.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

caffe 初學參考鏈接

最近在學習caffe,也搜集了一些資料,主要是一些網上公開的博客資源,現匯總一下,以便后面參考。 caffe 安裝 編譯py-faster-rcnn全過程caffe依賴庫安裝(非root)編譯py-faster-rcnn的問題匯總及解決方法 ca…

java timer 定時任務

監聽類1 package com.xx.model;2 3 import java.util.Calendar;4 import java.util.Date;5 import java.util.Timer;6 import javax.servlet.ServletContextEvent;7 import javax.servlet.ServletContextListener;8 import org.apache.commons.logging.Log;9 import org.apache…

python 打開txt_在python中從txt文件打開鏈接

我想請求一個rss程序的幫助。我所做的是收集包含我項目相關信息的網站,然后檢查它們是否有rss提要。鏈接存儲在txt文件中(每行一個鏈接)。因此,我有一個txt文件,其中包含了需要檢查rss的基本url。在我找到了這個代碼,這會使我的工…

IOS-awakeFromNib和viewDidLoad

awakeFromNib 當.nib文件被加載的時候,會發送一個awakeFromNib的消息到.nib文件中的每個對象,每個對象都可以定義自己的 awakeFromNib函數來響應這個消息,執行一些必要的操作。也就是說通過nib文件創建view對象是執行awakeFromNib 。 viewDid…

使用過濾統計信息解決基數預估錯誤

基數預估是SQL Server里一顆隱藏的寶石。一般而言,基數預估指的是,在查詢編譯期間,查詢優化器嘗試找出在執行計劃里從各個運算符平均返回的行數。這個估計用來驅動計劃本身生成并選擇正確的計劃運算符——例如像Nested Loop, Merge Join,還是…

faster-rcnn系列學習之準備數據

如下列舉了 將數據集做成VOC2007格式用于Faster-RCNN訓練的相關鏈接。 RCNN系列實驗的PASCAL VOC數據集格式設置 制作VOC2007數據集用于Faster-RCNN訓練 將數據集做成VOC2007格式用于Faster-RCNN訓練 這一篇比較詳細地介紹了如何制造voc2007的所有文件,內含相關軟件…

C# 委托鏈、多路廣播委托

委托鏈、多路廣播委托:也就是把多個委托鏈接在一起,我們把鏈接了多個方法的委托稱為委托鏈或多路廣播委托 例: 1 class HelloWorld2 {3 //定義委托類型4 delegate void DelegationChain();5 static void Main(string[] args)6 …

openssl 生成證書_使用證書和私鑰導出P12格式個人證書!

【OpenSSL】使用證書和私鑰導出P12格式個人證書1, 產生CA證書1.1, 生成ca的私鑰openssl genrsa -out cakey.pem 20481.2, 生成ca的自簽名證書請求openssl req -new -key cakey.pem -subj "/CNExample Root CA" -out cacsr.pem1.3, 自簽名ca的證書openssl x509 -req -…

PHP (20140505)

數據庫表與表之間的連接是用id聯系。 join on;轉載于:https://www.cnblogs.com/sunshine-c/p/3710283.html

py-faster-rcnn代碼roidb.py的解讀

roidb是比較復雜的數據結構,存放了數據集的roi信息。原始的roidb來自數據集,在trian.py的get_training_roidb(imdb)函數進行了水平翻轉擴充數量,然后prepare_roidb(imdb)【定義在roidb.py】為roidb添加了一些說明性的屬性。 在這里暫時記錄下…

python 概率分布_python實現概率分布

伯努利分布from scipy import statsimport numpy as npimport matplotlib.pyplot as pltxnp.arange(0,2,1)xarray([0, 1])# 求對應分布的概率:概率質量函數 (PMF)p0.5# 硬幣朝上的概率dfstats.bernoulli.pmf(x,p)dfarray([0.5, 0.5])#繪圖vlines用于繪制豎直線(vert…

CodeForces 7D Palindrome Degree 字符串hash

題目鏈接&#xff1a;點擊打開鏈接 #include<stdio.h> #include<iostream> #include<string.h> #include<set> #include<vector> #include<map> #include<math.h> #include<queue> #include<string> #include<stdlib…

程序清單8-9 回送所有命令行參數和所有環境字符串

1 /*2 3 Name : test.c4 Author : blank5 Version :6 Copyright : Your copyright notice7 Description : 程序清單8-9 回送所有命令行參數和所有環境字符串8 9 */ 10 11 #include "ourhdr.h" 12 13 int main(int argc, char *argv[]) 14…

SQL快速入門

關系化數據庫保存關系模式數據的容器關系模式是對業務對象實體&#xff0c;屬性以及關系的抽象&#xff0c;提煉需求的名詞是建立實體關系模型常用的方法。要了解E-R實體關系圖的繪制。常用關系數據庫Microsoft SQL Server&#xff1b;微軟公司產品&#xff0c;中等規模數據庫&…

Faster RCNN minibatch.py解讀

minibatch.py 的功能是&#xff1a; Compute minibatch blobs for training a Fast R-CNN network. 與roidb不同的是&#xff0c; minibatch中存儲的并不是完整的整張圖像圖像&#xff0c;而是從圖像經過轉換后得到的四維blob以及從圖像中截取的proposals&#xff0c;以及與之對…

oracle精簡版_使用Entity Framework Core訪問數據庫(Oracle篇)

前言哇。。看看時間 真的很久很久沒寫博客了 將近一年了。最近一直在忙各種家中事務和公司的新框架 終于抽出時間來更新一波了。本篇主要講一下關于Entity Framework Core訪問oracle數據庫的采坑。。強調一下&#xff0c;本篇文章發布之前 關于Entity Framework Core訪問oracl…

interrupt、interrupted 、isInterrupted 區別

interrupt&#xff1a;調用方法&#xff0c;是線程處于中斷狀態&#xff0c;但是這個方法只是讓線程設置為中斷狀態&#xff0c;并不會真正的停止線程。支持線程中斷的方法就是在堅持線程中斷狀態&#xff0c;一旦線程中斷狀態被設置為中斷&#xff0c;就會拋出異常。interrupt…

java String部分源碼解析

String類型的成員變量 /** String的屬性值 */ private final char value[];/** The offset is the first index of the storage that is used. *//**數組被使用的開始位置**/private final int offset;/** The count is the number of characters in the String. *//**String中…

python在材料模擬中的應用_基于Python的ABAQUS二次開發及在板料快速沖壓成形模擬中的應用...

2009doi:1013969/j1issn1100722012120091041013基于Python的ABAQUS二次開發及在板料快速沖壓成形模擬中的應用(北京航空航天大學飛行器制造工程系,北京100191)吳向東劉志剛萬敏王文平黃霖摘要:采用Python腳本語言對ABAQUS的前處理模塊進行二次開發,討論了Python腳本在ABAQUS二次…

Doxygen簡介

&#xff08;轉自&#xff1a;http://www.cnblogs.com/liuliunumberone/archive/2012/04/10/2441391.html&#xff09; 一&#xff0e;什么是Doxygen? Doxygen 是一個程序的文件產生工具&#xff0c;可將程序中的特定批注轉換成為說明文件。通常我們在寫程序時&#xff0c;或多…