關於MySQL自增主鍵的幾點問題(上)


前段時間遇到一個InnoDB表自增鎖導致的問題,最近剛好有一個同行網友也問到自增鎖的疑問,所以抽空系統的總結一下,這兩個問題下篇會有闡述。

1. 划分三種插入類型

這里區分一下幾種插入數據行的類型,便於后面描述:(純邏輯上的划分)

  1. “Simple inserts”
    簡單插入,就是在處理sql語句的時候,能夠提前預估到插入的行數,包括 INSERT / REPLACE 的單行、多行插入,但不含嵌套子查詢以及 INSERT ... ON DUPLICATE KEY UPDATE

  2. “Bulk inserts”
    本文暫且叫做 大塊插入,不能提前預知語句要插入的行數,也就無法知道分配多少個自增值,包括 INSERT ... SELECTREPLACE ... SELECT, 以及 LOAD DATA 導入語句。InnoDB會每處理一行記錄就為 AUTO_INCREMENT 列分配一個值。

  3. “Mixed-mode inserts”
    混合插入,比如在 “簡單插入” 多行記錄的時候,有的新行有指定自增值,有的沒有,所以獲得最壞情況下需要插入的數量,然后一次性分配足夠的auto_increment id。比如:

    1
    2
    # c1 是 t1 的 AUTO_INCREMENT 列
    INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');

又比如 INSERT ... ON DUPLICATE KEY UPDATE,它在 update 階段有可能分配新的自增id,也可能不會。

2. 三種自增模式:innodb_autoinc_lock_mode

在以 5.6 版本,自增id累加模式分為:

  • 傳統模式
    traditional,innodb_autoinc_lock_mode = 0
    在具有 AUTO_INCREMENT 的表上,所有插入語句會獲取一個特殊的表級鎖 AUTO-INC ,這個表鎖是在語句結束之后立即釋放(無需等到事務結束),它可以保證在一個insert里面的多行記錄連續遞增,也能保證多個insert並發情況下自增值是連續的(不會有空洞)。

  • 連續模式 
    consecutive,innodb_autoinc_lock_mode = 1
    MySQL 5.1.22開始,InnoDB提供了一種輕量級互斥的自增實現機制,在內存中會有一個互斥量(mutex),每次分配自增長ID時,就通過估算插入的數量(前提是必須能夠估算到插入的數量,否則還是使用傳統模式),然后更新mutex,下一個線程過來時從新 mutex 開始繼續計算,這樣就能避免傳統模式非要等待每個都插入之后才能獲取下一個,把“鎖”降級到 只在分配id的時候 鎖定互斥量。
    在 innodb_autoinc_lock_mode = 1(默認) 模式下,“簡單插入”采用上面的 mutex 方式,“大塊插入”(insert/replace … select … 、load data…)依舊采用 AUTO-INC 表級鎖方式。當然如果一個事務里已經持有表 AUTO-INC 鎖,那么后續的簡單插入也需要等待這個 AUTO-INC 鎖釋放。這能夠保證任意insert並發情況下自增值是連續的。

  • 交叉模式 
    interleaved,innodb_autoinc_lock_mode = 2
    該模式下所有 INSERT SQL 都不會有表級 AUTO-INC 鎖,多個 語句 可以同時執行,所以在高並發插入場景下性能會好一些。但是當 binlog 采用 SBR 格式時,對於從庫重放日志或者主庫實例恢復時,並不可靠。
    另者,它只能保證自增值在 insert語句級別 (單調)遞增,所以多個insert可能會交叉着分配id,最終可能導致多個語句之間的id值不連續,這種情況出現在 混合插入:

    1
    INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');

    mutex 會按行分配4個id,但實際只用到2個,因此出現空洞。

3. 自增空洞(auto-increment sequence gap)

關於 AUTO_INCREMENT 自增出現空洞的問題,有必要再說明一下。

  1. 在 0, 1, 2 三種任何模式下,如果事務回滾,那么里面獲得自增值的sql回滾,但產生的自增值會一起丟失,不可能重新分配給其它insert語句。這也會產生空洞。

  2. 在大塊插入情景下

    • innodb_autoinc_lock_mode為 0 或 1 時,因為 AUTO-INC 鎖會持續到語句結束,同一時間只有一個 語句 在表上執行,所以自增值是連續的(其它事務需要等待),不會有空洞;
    • innodb_autoinc_lock_mode為 2 時,兩個 “大塊插入” 之間可能會有空洞,因為每條語句事先無法預知精確的數量而導致分配過多的id,可能有空洞。

4. 混合插入對 AUTO_INCREMENT 的影響

混合插入在 innodb_autoinc_lock_mode 不同模式下會有對 表自增值有不同的表現。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE t1 (
c1 INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
c2 CHAR(1)
) ENGINE=INNODB;

ALTER TABLE t1 AUTO_INCREMENT 101;


mysql> SHOW CREATE TABLE t1\G
*************************** 1. row ***************************
Table: t1
Create Table: CREATE TABLE `t1` (
`c1` int(10) unsigned NOT NULL AUTO_INCREMENT,
`c2` char(1) DEFAULT NULL,
PRIMARY KEY (`c1`)
) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8

 

1. mode 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql> select @@innodb_autoinc_lock_mode;
+----------------------------+
| @@innodb_autoinc_lock_mode |
+----------------------------+
| 0 |
+----------------------------+

mysql> INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
mysql> select * from t1;
+-----+------+
| c1 | c2 |
+-----+------+
| 1 | a |
| 5 | c |
| 101 | b |
| 102 | d |
+-----+------+

mysql> show create table t1\G
...
) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8
...

可以看到下一個自增值是 103 ,因為即使這是 一條 insert語句(多行記錄),自增值還是每次分配一個,不會在語句開始前一次分配全。

2. mode 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
mysql> truncate table t1; ALTER TABLE t1 AUTO_INCREMENT 101;  -- 復原
mysql> select @@innodb_autoinc_lock_mode;
+----------------------------+
| @@innodb_autoinc_lock_mode |
+----------------------------+
| 1 |
+----------------------------+
1 row in set (0.00 sec)

mysql> INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0

mysql> select * from t1;
+-----+------+
| c1 | c2 |
+-----+------+
| 1 | a |
| 5 | c |
| 101 | b |
| 102 | d |
+-----+------+

mysql> show create table t1\G
...
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8

可以看到最終插入的值是一樣的,但下一個自增值變成了 105,因為該模式下insert語句處理的時候,提前分配了 4 個自增值,但實際只有了兩個。

注:如果你的insert自增列全都有帶值,那么處理的時候是不會分配自增值的,經過下面這個實驗,可以知道 分配自增值,是在遇到第一個沒有帶自增列的行時,一次性分配的 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- Tx1,先運行。 -- 插入第2行的時候 sleep 5s
INSERT INTO t1 (c1,c2) VALUES (2,'e'),(sleep(5)+6,'g'),(NULL,'f'), (NULL,'h');

-- Tx2,后運行。 -- 第一行沒有給自增列值,馬上分配 4 個
INSERT INTO t1 (c1,c2) VALUES (NULL,'b'), (1,'a'), (sleep(5)+5,'c'), (NULL,'d');

-- 得到的結果是
+-----+------+
| c1 | c2 |
+-----+------+
| 1 | a |
| 2 | e |
| 5 | c |
| 6 | g |
| 101 | b |
| 102 | d |
| 105 | f |
| 106 | h |
+-----+------+

 

3. mode 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mysql> truncate table t1; ALTER TABLE t1 AUTO_INCREMENT 101;  -- 復原
mysql> select @@innodb_autoinc_lock_mode;
+----------------------------+
| @@innodb_autoinc_lock_mode |
+----------------------------+
| 2 |
+----------------------------+

mysql> INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
mysql> select * from t1;
+-----+------+
| c1 | c2 |
+-----+------+
| 1 | a |
| 5 | c |
| 101 | b |
| 102 | d |
+-----+------+

mysql> show create table t1\G
...
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8

結果看起來與 連續模式 一樣,其實不然!該模式下,如果另外一個 大塊插入 並發執行時,可能會出現以下現象:

  1. 大塊插入的的自增值有間斷
  2. 其它並發執行的事務插入出現 duplicate-key error
1
2
3
4
5
6
7
8
9
10
11
第1點 (create t2 select * from t1)
Tx1: insert into t1(c2) select c2 from t2; -- 先執行
Tx2: INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d'); -- 后 並發執行

在交叉模式下,Tx1事務插入的數據行會與 Tx1 交叉出現。
注:如果 Tx1 改成 insert into t1 select * from t2 ,那么 Tx2 執行極有可能會報 duplicate-key error,與下面第2點所說的重復鍵是不一樣的

第2點
mysql> truncate table t1; ALTER TABLE t1 AUTO_INCREMENT 5; -- 復原
mysql> INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
ERROR 1062 (23000): Duplicate entry '5' for key 'PRIMARY'

總結

上面說了這么多,那么自增模式到底該怎么選擇呢?其實很簡單,目前數據庫默認的 consecutive 即 innodb_autoinc_lock_mode=1 就是最好的模式,一般業務生產庫不會有 insert into ... select ...或者 load data infile 這樣的維護動作。(提示:即使晚上有數據遷移任務,也不要通過這樣的形式進行)

innodb_autoinc_lock_mode=2 可以提高獲取表自增id的並發能力(性能),但是除非出現上面演示的 duplicate-key 特殊用法情形,不會像網上所說的獲取到相同key導致重復的問題。但是如果binlog在 RBR 格式下不建議使用,可能出現主從數據不一致。還有就是能夠容忍gap的存在,以及多個語句insert的自增值交叉。

參考: https://dev.mysql.com/doc/refman/5.6/en/innodb-auto-increment-handling.html

下篇分析遇到過的 MySQL 自增主鍵相關的具體問題。

 

轉載地址:http://seanlook.com/2017/02/16/mysql-autoincrement/ 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM