再談MySQL auto_increment空洞問(wèn)題

在項(xiàng)目中偶爾會(huì)有這種需求士嚎,用戶通過(guò)第三方系統(tǒng)登錄時(shí)如果尚未注冊(cè)戳粒,則自動(dòng)給用戶注冊(cè)役首,注冊(cè)過(guò)的用戶則自動(dòng)登錄,更新最近登錄時(shí)間等信息掺逼。有時(shí)候圖省事可能就直接INSERT INTO user ON DUPLICAET KEY UPDATE...一句 SQL 解決了吃媒,功能沒(méi)問(wèn)題,只是如果用戶表中有auto_increment字段吕喘,則容易導(dǎo)致auto_increment字段產(chǎn)生空洞問(wèn)題赘那,一段時(shí)間后會(huì)發(fā)現(xiàn)用戶ID會(huì)經(jīng)常出現(xiàn)不連續(xù)的情況,雖然MySQL的自增ID一般都?jí)蛴寐戎剩侨绻軠p少不必要的空洞更好募舟。

場(chǎng)景

當(dāng)用戶從第三方登錄時(shí),假定用的是手機(jī)號(hào)做唯一標(biāo)識(shí)闻察,通常在我們自己的系統(tǒng)中會(huì)建一個(gè)用戶表拱礁,如下(MySQL版本為5.5.58琢锋,隔離級(jí)別為Repeatable Read):

 CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `mobile` varchar(11) DEFAULT NULL,
  `last_login_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `mobile` (`mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

當(dāng)用戶從第三方登錄時(shí),我們校驗(yàn)通過(guò)后呢灶,會(huì)將手機(jī)號(hào)插入到user表里注冊(cè)用戶吴超。如果用戶已經(jīng)存在,則更新最后登錄時(shí)間鸯乃,為了簡(jiǎn)便鲸阻,經(jīng)常像下面這么做,功能上看起來(lái)是沒(méi)錯(cuò)的缨睡,問(wèn)題就是運(yùn)行一段時(shí)間后會(huì)發(fā)現(xiàn)user表的id字段居然是不連續(xù)的鸟悴,而且經(jīng)常兩個(gè)id之間空洞還很大,比如上一個(gè)id是4奖年,下一個(gè)變成了21遣臼。如下面例子中,再插入一條新記錄時(shí)拾并,id會(huì)變成3,也就是說(shuō)id=2這個(gè)值被浪費(fèi)了鹏浅。

mysql> INSERT INTO user(mobile, last_login_time) VALUES('15012345678',
 NOW()) ON DUPLICATE KEY UPDATE last_login_time = NOW();
Query OK, 1 row affected (0.00 sec)

mysql> show create table user;
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                               |
+----------------------------------------------------------------------+
| user  | CREATE TABLE `user` (
......
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 |

mysql> INSERT INTO user(mobile, last_login_time) VALUES('15012345678', 
NOW()) ON DUPLICATE KEY UPDATE last_login_time = NOW();
Query OK, 2 rows affected (0.00 sec)

mysql> show create table user;
+-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                               |
+-------+---------------------------------------------------------------------
| user  | CREATE TABLE `user` (
......
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 |

分析

在MySQL官方文檔其實(shí)提到過(guò)這個(gè)問(wèn)題嗅义,當(dāng)表t1中unique key的列a已經(jīng)有一個(gè)值為1的情況下,通常情況執(zhí)行下面這兩條語(yǔ)句效果是一樣的隐砸,但是之碗,如果表t1是InnoDB引擎而且有一列為auto_increment的情況下,影響是不一樣的季希,會(huì)浪費(fèi)一個(gè)auto_increment值褪那。(注:MyISAM引擎的表不受此影響,不會(huì)產(chǎn)生空洞問(wèn)題)

INSERT INTO t1 (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

UPDATE t1 SET c=c+1 WHERE a=1;

確切的說(shuō)式塌,產(chǎn)生空洞問(wèn)題還跟innodb_autoinc_lock_mode這個(gè)MySQL配置相關(guān)博敬。該配置在MySQL5.1引入,是為了提升auto_increment字段的并發(fā)性能引入的峰尝,默認(rèn)值為1偏窝。該值可以配置為0(traditional lock mode),1(consecutive lock mode)武学,2(interleaved lock mode)祭往,除了0基本不產(chǎn)生空洞外,配置其他值都是可能有auto_increment空洞的火窒,簡(jiǎn)單總結(jié)如下硼补,更詳細(xì)的可以參考 innodb-auto-increment-handling

  • 1)如果事務(wù)回滾了熏矿,則不管是0已骇,1离钝,2都會(huì)導(dǎo)致事務(wù)中使用過(guò)的auto_increment的值浪費(fèi)。

  • 2)如果設(shè)置為0疾捍,是traditional lock mode奈辰,則任意插入語(yǔ)句都會(huì)加 AUTO-INC 鎖,基本不會(huì)產(chǎn)生空洞乱豆,除了1中的rollback情況外奖恰。

  • 3)如果設(shè)置為1或者2的時(shí)候,simple inserts語(yǔ)句(simple inserts指的是那種能夠事先確定插入行數(shù)的語(yǔ)句宛裕,比如INSERT/REPLACE INTO 等插入單行或者多行的語(yǔ)句瑟啃,語(yǔ)句中不包括嵌套子查詢)不會(huì)有空洞。但是對(duì)于bulk inserts(bulk inserts指的是事先無(wú)法確定插入行數(shù)的語(yǔ)句揩尸,比如INSERT/REPLACE INTO ... SELECT FROM..., LOAD DATA等)和mixed-mode inserts(指的是simple inserts類型中有些行指定了auto_increment列的值有些沒(méi)有指定蛹屿,比如:INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d')INSERT ... ON DUPLICATE KEY UPDATE這種語(yǔ)句)會(huì)預(yù)先分配auto_increment值,導(dǎo)致一些浪費(fèi)岩榆。 特別是設(shè)置為2的時(shí)候错负,在執(zhí)行任意插入語(yǔ)句都不會(huì)加 AUTO-INC 鎖,從而在語(yǔ)句執(zhí)行過(guò)程中都可能產(chǎn)生空洞勇边。

一種錯(cuò)誤示范

為了減少第一節(jié)中的auto_increment空洞問(wèn)題犹撒,一種方法就是INSERT前先判斷下用戶是否存在,不存在才執(zhí)行插入語(yǔ)句粒褒,存在則更新最近登錄時(shí)間识颊。

with transaction:
    user = SELECT * FROM user WHERE mobile = '15012345678' FOR UPDATE;
    if not user:
       INSERT INTO user(mobile, last_login_time) VALUES('15012345678', NOW()) 
    UPDATE user SET last_login_time = NOW();

這個(gè)代碼乍看是沒(méi)有問(wèn)題了,mobile是unique key奕坟,FOR UPDATE看似沒(méi)有問(wèn)題祥款,這是一個(gè)lock read,而且是排他鎖月杉。一個(gè)session對(duì)這條記錄加了排他鎖刃跛,其他session不能對(duì)這條記錄加鎖和修改,如不能 LOCK IN SHARE MODE 以及 UPDATE 等(注:SELECT ... FOR UPDATE只在事務(wù)中或者autocommit關(guān)閉的情況下才會(huì)加鎖)沙合。但是奠伪,這只在記錄存在的情況下才是對(duì)記錄加X(jué)鎖,沒(méi)有Gap鎖首懈。而如果這個(gè)記錄不存在绊率,則對(duì)第一個(gè)不滿足條件的記錄加Gap鎖,保證沒(méi)有滿足條件的記錄插入究履。

如果mobile=15012345678這條記錄不存在滤否,并發(fā)的多個(gè)session都可以進(jìn)入SELECT ... FOR UPDATE都是加的Gap鎖(X locks gap before rec)最仑,這兩個(gè)Gap鎖之間是兼容的藐俺。此時(shí)炊甲,其中任意一個(gè)session再執(zhí)行 INSERT INTO user(mobile, last_login_time) VALUES('15012345678', NOW())語(yǔ)句會(huì)因?yàn)榧?code>insert intention lock(注:插入意向鎖是一種特殊的Gap鎖,不是MySQL的表級(jí)意向鎖IS欲芹,IX等卿啡,它與前面FOR UPDATE加的Gap鎖不兼容)超時(shí)而執(zhí)行失敗。其實(shí)此時(shí)的Gap鎖不只是鎖住了 15012345678 這條記錄菱父,如果表中有其他的記錄颈娜,會(huì)將可能插入 15012345678 的區(qū)間都鎖住,MySQL加鎖詳細(xì)分析可以見(jiàn)參考資料5浙宜。另外官辽,是否啟用Gap鎖跟MySQL的配置項(xiàng)innodb_locks_unsafe_for_binlog有關(guān),默認(rèn)值為0粟瞬,即是啟用狀態(tài)同仆。

解決方案

若要減少auto_increment的浪費(fèi)問(wèn)題,又要避免上一節(jié)提到的加鎖超時(shí)問(wèn)題裙品,還是有點(diǎn)事情要做的俗批。可行的幾種方法如下:

  • a) 如果對(duì)數(shù)據(jù)沒(méi)有很強(qiáng)的提交讀的需求市怎,可以不加FOR UPDATE查詢扶镀,若記錄不存在,再INSERT IGNORE INTO ...焰轻。
  • b) 如果需要提交讀的,可以通過(guò)GET_LOCK(str, timeout)而不是FOR UPDATE來(lái)避免上一節(jié)提到的問(wèn)題昆雀。
  • c) percona的這篇文章avoiding-auto-increment-holes-on-innodb-with-insert-ignore描述了一種很tricky的方法來(lái)避免auto_increment的空洞問(wèn)題辱志,有興趣的可以參考。

MySQL innodb在RR模式下狞膘,在非唯一索引上使用FOR UPDATE要慎重揩懒,因?yàn)闀?huì)加gap lock,如果加鎖不當(dāng)很容易出現(xiàn)死鎖挽封。MySQL InnoDB引擎如果出現(xiàn)了一些加鎖問(wèn)題已球,可以通過(guò)下面這幾個(gè)命令來(lái)輔助分析。

show engine innodb status;
select * from information_schema.innodb_trx;
select * from information_schema.innodb_locks;
select * from information_schema.innodb_lock_waits;

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末辅愿,一起剝皮案震驚了整個(gè)濱河市智亮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌点待,老刑警劉巖阔蛉,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異癞埠,居然都是意外死亡状原,警方通過(guò)查閱死者的電腦和手機(jī)聋呢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)颠区,“玉大人削锰,你說(shuō)我怎么就攤上這事”侠常” “怎么了器贩?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)央串。 經(jīng)常有香客問(wèn)我磨澡,道長(zhǎng),這世上最難降的妖魔是什么质和? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任稳摄,我火速辦了婚禮,結(jié)果婚禮上饲宿,老公的妹妹穿的比我還像新娘厦酬。我一直安慰自己,他們只是感情好瘫想,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布仗阅。 她就那樣靜靜地躺著,像睡著了一般国夜。 火紅的嫁衣襯著肌膚如雪减噪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,792評(píng)論 1 290
  • 那天车吹,我揣著相機(jī)與錄音筹裕,去河邊找鬼。 笑死窄驹,一個(gè)胖子當(dāng)著我的面吹牛朝卒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乐埠,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼抗斤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了丈咐?” 一聲冷哼從身側(cè)響起瑞眼,我...
    開(kāi)封第一講書(shū)人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棵逊,沒(méi)想到半個(gè)月后负拟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歹河,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年掩浙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了花吟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡厨姚,死狀恐怖衅澈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谬墙,我是刑警寧澤今布,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站拭抬,受9級(jí)特大地震影響部默,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜造虎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一傅蹂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧算凿,春花似錦份蝴、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至署鸡,卻和暖如春案糙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背靴庆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工侍筛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撒穷。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像裆熙,于是被迫代替她去往敵國(guó)和親端礼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348