面試官:MySQL主鍵為什么不是連續(xù)遞增的?

設(shè)計MySQL表時,我們一般會設(shè)置一個自增主鍵,從而讓主鍵索引盡可能的保持遞增的趨勢,這樣可以避免頁分裂,讓MySQL順序?qū)懭?,大大提高M(jìn)ySQL的性能。
但是,自增主鍵只能保持大致遞增,無法保證順序遞增。

當(dāng)我們創(chuàng)建完一個表后,通過show create table命令,可以看到MySQL定義了AUTO_INCREMENT來指定主鍵的遞增值。
在MySQL5.7之前,這個遞增值是直接保存在內(nèi)存里面的,當(dāng)服務(wù)器重啟后,MySQL會讀取表里面的最大主鍵id,然后將最大值+1作為下次遞增的值。
在MySQL8.0時,將其優(yōu)化為了保存在redo log中,從而實(shí)現(xiàn)了遞增值的持久化。
那都有哪些情況可能導(dǎo)致主鍵不能連續(xù)遞增呢?
首先我們要知道的是,MySQL對于主鍵遞增值得使用是一次性的,即每次獲取完遞增值之后,不管接下來的語句是否能真正執(zhí)行成功,這個遞增值都不會再回收利用了。
1、唯一索引沖突導(dǎo)致的主鍵不連續(xù)
有時為了滿足業(yè)務(wù)的需要,我們有時會對表中的字段設(shè)置唯一索引。但是當(dāng)唯一索引沖突時,會產(chǎn)生什么問題呢?
以上面的user表為例,我們對name設(shè)置唯一索引。
我們執(zhí)行兩次以下語句:
不難猜到,第二次的執(zhí)行結(jié)果肯定會報錯:

我們在上面已經(jīng)提到,MySQL對于遞增值的使用是一次性的,那么第二次執(zhí)行插入時,不管語句成功還是失敗,那么這個遞增值就會浪費(fèi)掉。
這時,我們再執(zhí)行一條正常的不沖突的插入語句,會發(fā)現(xiàn)主鍵id產(chǎn)生了間隔。

2、事務(wù)回滾會造成主鍵不連續(xù)
與唯一索引沖突類似,當(dāng)我們在一個事務(wù)中執(zhí)行插入語句時,那么必然會向MySQL申請一個遞增值作為主鍵id,如果最后事務(wù)沒有提交,而是回滾,那么這個遞增值自然也就浪費(fèi)掉了。
3、批量插入會造成主鍵不連續(xù)
為了保證主鍵id的唯一性,在申請自增id時,MySQL會對申請操作加鎖。一般情況下,這個申請動作會很快。
對于一般的批量插入,比如insert into ... values(xxx),由于插入的Value個數(shù)可以提前計算得出,MySQL會一次性的申請足夠數(shù)量的id,以保證性能。
但是對于insert into ... select 這種語句就有點(diǎn)麻煩了,由于無法確定到底需要申請多個主鍵id,如果插入一條申請一個的話,假設(shè)要插入100萬條記錄,那就得申請100萬次,可想而知性能會有多么差勁。
所以對于這種批量插入的語句,MySQL采用了一種翻倍申請的優(yōu)化策略:
語句執(zhí)行時,第一次申請一個自增id,第二次申請2個自增id,第三次申請4個自增id...
即每次申請的數(shù)量都比上次多一倍,這樣雖然會浪費(fèi)一些自增id,但是可以保證插入的效率,從性能角度來看,是可以接受的。
自增id為什么不回退復(fù)用
大家可能會有點(diǎn)疑問,為什么自增id是一次性使用的?
其實(shí)原因也很簡單,大家稍微一想就明白了。
假設(shè)有兩個事務(wù)在同時執(zhí)行,為了保證自增id的唯一性,MySQL會對申請動作加鎖,然后兩個事務(wù)各獲得一個自增id。比如事務(wù)1申請到了自增id100,事務(wù)2申請到了自增id101。
當(dāng)事務(wù)2成功提交,事務(wù)1因為某些原因回滾了。
如果我們要回退復(fù)用事務(wù)1的id,將AUTO_INCREMENT又設(shè)置成了100+1,那么下一個事務(wù)來申請自增id時,就會拿到101,而這時101已經(jīng)被事務(wù)2用掉了,就會造成主鍵沖突。
當(dāng)然我們也可以每次都讓MySQL檢查一下主鍵是否沖突,如果沖突就跳過這個id,但是這樣一來,本來申請自增id這個很輕的動作就會變得很重,對性能的影響就會很大。
所以,從性能角度考慮,InnoDB只保證了主鍵id是大致遞增的,而不保證是順序遞增的。?



































