圖1
圖1中是表t原有的數據,這個時候我們執行show create table t會看到如下輸出,如圖二所示現在的自增值是2,也就是下一個不指定主鍵值的插入的數據的主鍵就是2
圖2
Innodb引擎的自增值,是保存在內存中的,並且到了mysql8.0版本后,才有了“自增值持久化”的能力,也就是才實現了"如果發生重啟,表的自增值可以恢復為mysql重啟前的值"
也就說在mysql5.7及之前的版本,自增值保存在內存中,並沒有持久化,每次重啟后,第一次打開表的時候,都會去找自增值的最大值max(id),然后講max(id)+1作為當前表的自增值
舉例來說,如果一個表中當前數據行的最大id=3,自增值=4,這個時候我們刪除id=3的數據后,自增值還是=4,但是這個時候如果馬上重啟,重啟后這個表的自增值就會變成3,
我們可以做個實驗:
這個時候的自增值=4
我們刪掉了id=3的數據
然后我們在查詢一下自增值,我們可以看到這個時候的自增值還是4
然后我們在重啟以后在查詢一下,可以看到這個時候的自增值就是3了
在mysql8.0版本,將自增值的變更記錄在了redo log中,重啟的時候依靠redo log恢復重啟之前的值
接下來我們在做個實驗說明一下自增值為啥不是連續的,我們剛才建的表t的c字段我加了唯一索引,現在我們表中的自增值應該是3對吧,因為剛才我重啟過了,然后我們執行INSERT INTO t VALUES(NULL,2,3)
這條語句,會報錯插入失敗,這條語句的執行流程是這樣的
1,執行器調用innodb引擎接口寫入一行,傳入的這一行的值是(0,2,3);
2,innodb發現用戶沒有指定自增id的值,獲取表t當前的自增值3;
3,將傳入的行的值改成(3,1,1);
4,將表的自增值改成4;
5,繼續執行插入數據操作,由於已經存在C=2的記錄,所以報Duplicate key eror,語句返回
然后我們在查詢一下自增值,看到了沒有,現在的自增值已經變成了4,也就說上面這條語句插入失敗以后,自增值沒有回滾,這樣下次我們在插入數據的時候的主鍵值就是4,就會造成我們的主鍵值不一致
所以說,唯一鍵沖突是導致自增主鍵id不連續的第一種原因
同樣的,事務回滾也會產生類似的現象,這就是第二種原因
你可能會問,為什么在出現唯一鍵沖突或者事務回滾的時候,mysql沒有把表t的自增值改回去呢?如果把表t的當前自增值從4改回3,再插入新數據的時候,不就可以生成id=3的一行數據了嗎?
其實,mysql這么設計是為了提升性能,接下來,分析一下這個設計思路,看看自增值為什么不能回退
假設有兩個並行執行的事務,在申請自增值的時候,為了避免兩個事務申請到相同的自增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,但是,這個方法的成本很高,因為,本來申請id是一個很快的操作,現在還要再去主鍵索引樹上判斷id是否存在
2,把自增id的鎖范圍擴大,必須等到一個事務執行完成並提交,下一個事務才能再申請自增id,這個方法的問題,就是鎖的粒度太大,系統並發能力大大下降
可見,這兩個方法都會導致性能問題,造成這些麻煩的罪魁禍首,就是我們假設的這個”允許自增id回退“的前提導致的,
因此,innodb放棄了這個設計,語句執行失敗也不回退自增id,也正是因為這樣,所有才只保證了自增id是遞增的,但不保證是連續的。
了解更多:https://www.toutiao.com/c/user/83293539887/#mid=1633933053814798