MySQL 中 AUTO_INCREMENT 的“坑” --重复值问题
MySQL · 捉蟲動(dòng)態(tài)· InnoDB自增列重復(fù)值問題
問題重現(xiàn)
先從問題入手,重現(xiàn)下這個(gè) bug
use test; drop table if exists t1; create table t1(id int auto_increment, a int, primary key (id)) engine=innodb; insert into t1 values (1,2); insert into t1 values (null,2); insert into t1 values (null,2); select * from t1;| 1 | 2 |
| 2 | 2 |
| 3 | 2 |
| 1 | 2 |
這里我們關(guān)閉MySQL,再啟動(dòng)MySQL,然后再插入一條數(shù)據(jù)
insert into t1 values (null,2); select * FROM T1;| 1 | 2 |
| 2 | 2 |
我們看到插入了(2,2),而如果我沒有重啟,插入同樣數(shù)據(jù)我們得到的應(yīng)該是(4,2)。 上面的測(cè)試反映了MySQLd重啟后,InnoDB存儲(chǔ)引擎的表自增id可能出現(xiàn)重復(fù)利用的情況。
自增id重復(fù)利用在某些場(chǎng)景下會(huì)出現(xiàn)問題。依然用上面的例子,假設(shè)t1有個(gè)歷史表t1_history用來存t1表的歷史數(shù)據(jù),那么MySQLd重啟前,ti_history中可能已經(jīng)有了(2,2)這條數(shù)據(jù),而重啟后我們又插入了(2,2),當(dāng)新插入的(2,2)遷移到歷史表時(shí),會(huì)違反主鍵約束。
原因分析
InnoDB 自增列出現(xiàn)重復(fù)值的原因:
MySQL> show create table t1\G; *************************** 1. row *************************** Table: t1 Create Table: CREATE TABLE `t1` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=innodb AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 1 row in set (0.00 sec)建表時(shí)可以指定 AUTO_INCREMENT值,不指定時(shí)默認(rèn)為1,這個(gè)值表示當(dāng)前自增列的起始值大小,如果新插入的數(shù)據(jù)沒有指定自增列的值,那么自增列的值即為這個(gè)起始值。
InnoDB自增值
對(duì)于InnoDB表,這個(gè)值沒有持久到文件中。而是存在內(nèi)存中(dict_table_struct.autoinc)。那么又問,既然這個(gè)值沒有持久下來,為什么我們每次插入新的值后, show create table t1看到AUTO_INCREMENT值是跟隨變化的。其實(shí)show create table t1是直接從dict_table_struct.autoinc取得的(ha_innobase::update_create_info)。
知道了AUTO_INCREMENT是實(shí)時(shí)存儲(chǔ)內(nèi)存中的。那么,MySQLd 重啟后,從哪里得到AUTO_INCREMENT呢? 內(nèi)存值肯定是丟失了。實(shí)際上MySQL采用執(zhí)行類似select max(id)|1 from t1;方法來得到AUTO_INCREMENT。而這種方法就是造成自增id重復(fù)的原因。
MyISAM自增值
MyISAM也有這個(gè)問題嗎?MyISAM是沒有這個(gè)問題的。myisam會(huì)將這個(gè)值實(shí)時(shí)存儲(chǔ)在.MYI文件中(mi_state_info_write)。MySQLd重起后會(huì)從.MYI中讀取AUTO_INCREMENT值(mi_state_info_read)。因此,MyISAM表重啟是不會(huì)出現(xiàn)自增id重復(fù)的問題。
問題修復(fù)
MyISAM選擇將AUTO_INCREMENT實(shí)時(shí)存儲(chǔ)在.MYI文件頭部中。實(shí)際上.MYI頭部還會(huì)實(shí)時(shí)存其他信息,也就是說寫AUTO_INCREMENT只是個(gè)順帶的操作,其性能損耗可以忽略。InnoDB 表如果要解決這個(gè)問題,有兩種方法。
1)將AUTO_INCREMENT最大值持久到frm文件中。
2)將 AUTO_INCREMENT最大值持久到聚集索引根頁(yè)trx_id所在的位置。
第一種方法直接寫文件性能消耗較大,這是一額外的操作,而不是一個(gè)順帶的操作。
我們采用第二種方案。為什么選擇存儲(chǔ)在聚集索引根頁(yè)頁(yè)頭trx_id,頁(yè)頭中存儲(chǔ)trx_id,只對(duì)二級(jí)索引頁(yè)和insert buf 頁(yè)頭有效(MVCC)。而聚集索引根頁(yè)頁(yè)頭trx_id這個(gè)值是沒有使用的,始終保持初始值0。正好這個(gè)位置8個(gè)字節(jié)可存放自增值的值。我們每次更新AUTO_INCREMENT值時(shí),同時(shí)將這個(gè)值修改到聚集索引根頁(yè)頁(yè)頭trx_id的位置。 這個(gè)寫操作跟真正的數(shù)據(jù)寫操作一樣,遵守write-ahead log原則,只不過這里只需要redo log ,而不需要undo log。因?yàn)槲覀儾恍枰貪LAUTO_INCREMENT的變化(即回滾后自增列值會(huì)保留,即使insert 回滾了,AUTO_INCREMENT值不會(huì)回滾)。
因此,AUTO_INCREMENT值存儲(chǔ)在聚集索引根頁(yè)trx_id所在的位置,實(shí)際上是對(duì)內(nèi)存根頁(yè)的修改和多了一條redo log(量很小),而這個(gè)redo log 的寫入也是異步的,可以說是原有事務(wù)log的一個(gè)順帶操作。因此AUTO_INCREMENT值存儲(chǔ)在聚集索引根頁(yè)這個(gè)性能損耗是極小的。
修復(fù)后的性能對(duì)比,我們新增了全局參數(shù)innodb_autoinc_persistent 取值on/off; on 表示將AUTO_INCREMENT值實(shí)時(shí)存儲(chǔ)在聚集索引根頁(yè)。off則采用原有方式只存儲(chǔ)在內(nèi)存。
./bin/sysbench --test=sysbench/tests/db/insert.lua --MySQL-port=4001 --MySQL-user=root \--MySQL-table-engine=innodb --MySQL-db=sbtest --oltp-table-size=0 --oltp-tables-count=1 \--num-threads=100 --MySQL-socket=/u01/zy/sysbench/build5/run/MySQL.sock --max-time=7200 --max-requests run set global innodb_autoinc_persistent=off; tps: 22199 rt:2.25ms set global innodb_autoinc_persistent=on; tps: 22003 rt:2.27ms可以看出性能損耗在%1以下。
改進(jìn)
新增參數(shù)innodb_autoinc_persistent_interval 用于控制持久化AUTO_INCREMENT值的頻率。例如:
innodb_autoinc_persistent_interval=100,auto_incrememt_increment=1時(shí),即每100次insert會(huì)控制持久化一次AUTO_INCREMENT值。每次持久的值為:當(dāng)前值+innodb_autoinc_persistent_interval。
測(cè)試結(jié)論
innodb_autoinc_persistent=ON, innodb_autoinc_persistent_interval=1時(shí)性能損耗在%1以下。 innodb_autoinc_persistent=ON, innodb_autoinc_persistent_interval=100時(shí)性能損耗可以忽略。限制
注意:如果我們使用需要開啟innodb_autoinc_persistent,應(yīng)該在參數(shù)文件中指定
innodb_autoinc_persistent= on如果這樣指定set global innodb_autoinc_persistent=on;重啟后將不會(huì)從聚集索引根頁(yè)讀取AUTO_INCREMENT最大值。
疑問:對(duì)于InnoDB表,重啟通過select max(id)|1 from t1得到AUTO_INCREMENT值,如果id上有索引那么這個(gè)語(yǔ)句使用索引查找就很快。那么,這個(gè)可以解釋MySQL 為什么要求自增列必須包含在索引中的原因。 如果沒有指定索引,則報(bào)如下錯(cuò)誤,
ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key而myisam表竟然也有這個(gè)要求,感覺是多余的。
總結(jié)
以上是生活随笔為你收集整理的MySQL 中 AUTO_INCREMENT 的“坑” --重复值问题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL 中 AUTO_INCREME
- 下一篇: Sping boot系列--redis之