MySQL AUTO_INCREMENT lock Handing in InnoDB
在MySQL的表设计中很普遍的使用自增长字段作为表主键, 实际生产中我们也是这样约束业务开发同学的, 其中的优势:
- 无意义自增长字段对业务透明, 后期不会存在更新主键等操作, 这在多数据中心同步中避免了很多问题.
- InnoDB表是IOT表, 数据按照主键组织存储, B-Tree叶子节点连续递增, 写入时保证了性能, 避免了频繁InnoDB page分裂, 并且也提高了空间利用率.
- 自增长字段只占4或者8个字节, 代价小. 一个page内存储更多的数据, buffer pool缓存命中概率更高.
当然, 使用自增长字段作为主键也会带来一些弊端:
- auto_increment热点争用
- replace逻辑可能导致主备auto_increment不一致, 在主备切换后会出现duplicate key问题
最近身边同学出现一起线上故障就是由于auto_increment使用不当/不够了解导致的, 因此花点时间探究下InnoDB引擎auto_increment问题, 总结一下, 分享给大家.
InnoDB为auto_increment列提供了可配置的锁机制, 在写入数据时提升SQL的并发和性能. auto_increment列必须是主键, 或者是索引的一部分(索引列的第一列).
- InnoDB AUTO_INCREMENT Lock Modes(锁级别)
- InnoDB AUTO_INCREMENT Lock Mode Usage Implications(用法示例)
- InnoDB AUTO_INCREMENT Counter Initialization(计数初始化)
InnoDB AUTO_INCREMENT Lock Modes
这部分主要分析不同auto_increment lock mode用以产生自增值的行为, 以及对主备复制的影响. Auto-increment lock modes配置在my.cnf中的参数innodb_autoinc_lock_mode
, mysqld启动参数.
-
"INSERT-like"语句
所有向表里插入新行的语句, 包括
INSERT
,INSERT...SELECT
,REPLACE
,REPLACE...SELECT
, 以及LOAD DATA
. 包括"Simple inserts", "Bulk inserts", "Mixed-mode inserts". -
"Simple inserts"
Simple inserts指那种可以事先确定插入行数的语句, 包括单行和多行插入, 不包含嵌套自查询的
INSERT
和REPLACE
语句(这样的语句也要除外:INSERT...ON DUPLICATE KEY UPDATE
) -
"Bulk inserts"
Bulk inserts指那种事先无法确定插入行数的语句, 包括
INSERT...SELECT
和REPLACE...SELECT
, 以及LOAD DATA
语句. InnoDB在处理每一行的时候为新纪录分配一个自增值. -
"Mixed-mode inserts"
Mixed-mode inserts指的是simple inserts类型中有些行指定了auto_increment列的值, 有些没有指定, 譬如:
INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
另外一种Mixed-mode inserts类型是
INSERT ... ON DUPLICATE KEY UPDATE
, 最坏的情况是"insert"其实是"update", 这可能会导致分配的auto_increment值没有被使用.innodb_autoinc_lock_mode
有3种配置模式, 0/1/2, 分别对应"传统模式", "连续模式", "交错模式".-
innodb_autoinc_lock_mode = 0 (传统锁模式)
保持了MySQL 5.1版本中相同的行为, 向后兼容.
在这种锁模式下, 所有"INSERT-like"语句在插入表AUTO-INCREMENT列时获取表级别的
AUTO-INC
锁, 该锁会持有到语句执行结束(而非事务结束), 确保auto-increment值以可预期, 可重复, 连续的序列顺序分配给INSERT
语句在SBR主备同步模式下, 可以保证同一条SQL语句复制到备库时可以产生和主库相同的auto-increment值.
Multiple-INSRT
语句在备库执行产生确定性的结果, 就如在主库上执行的一样. 如果Multiple-INSRT
语句产生的auto-increment值是交错的, 那么并发的两条INSERT
语句将产生不确定性的结果, 那么也就不能可靠的使用SBR模式复制主备数据同步.
假如这样一种case:
CREATE TABLE t1 ( c1 INT(11) NOT NULL AUTO_INCREMENT, c2 VARCHAR(10) DEFAULT NULL, PRIMARY KEY (c1) ) ENGINE=InnoDB;
有两个事务在运行, 都向表中插入auto-increment列.
tx1:
INSERT...SELECT
语句, 插入100行.tx2:
INSERT
语句.tx1: INSERT INTO t1 (c2) SELECT 1000 rows from another table ... tx2: INSERT INTO t1 (c2) VALUES ('xxx');
在tx1中, InnoDB无法事先预期到
INSERT...SELECT
语句产生多少行数据, 在语句执行过程中分配auto-increment值(table-level lock), 持有锁直到语句执行结束. 同一时刻只有一个INSERT
语句可以执行, 并且保证产生的auto-increment值不会交错. tx1事务执行INSERT...SELECT
语句产生连续的auto-increment值, tx2事务执行INSERT
语句产生的auto-increment值要么小于所有tx1语句产生的auto-increment值, 要么大于所有tx1语句产生的auto-increment值, 这要取决于哪一个事务先执行.当使用SBR模式主备复制, 或者恢复场景时, 回放binary log时只要SQL语句以相同的顺序执行, 其结果就如当时tx1和tx2运行时一样.
所以, table-level lock持有到语句执行结束来产生auto-increment值安全的保证了SBR模式的主备数据同步. 但是, table-level lock也限制了SQL(insert)语句的并发性能.
在"连续锁模式"中, InnoDB在执行"Simple inserts"时避免了table-level AUTO-INC lock, 同时也保证了安全的SBR主备数据复制.
如果在复制或者恢复场景中不使用binary log回放SQL语句, 那么"交错锁模式"可以被使用来消除table-level AUTO-INC lock以提升并发和性能. 但是这会带来并发执行SQL语句时产生间隙的auto-increment值或者交错的auto-increment值.
-
innodb_autoinc_lock_mode = 1 (连续锁模式)
这是InnoDB默认的锁模式.
- "Bulk inserts"使用特殊的AUTO-INC table-level lock, 并且持有锁到语句结束. 包括所有的
INSERT...SELECT
,REPLACE...SELECT
, 以及LOAD DATA
语句. 同一时刻只有一个语句可以持有AUTO-INC table-level lock. -
"Simple inserts"避免le使用table-level AUTO-INC lock, 而是使用互斥锁(mutex, 更轻量级锁)控制获取需要的auto-increment值, 只有在分配auto-increment值期间持有, 并不是语句执行结束. 如果有事务持有table-level AUTO-INC lock, 那么"Simple inserts"将会向"Bulk inserts"一样等待AUTO-INC lock.
这个锁模式确保了所有"INSER-like"语句产生连续的auto-increment值(包括哪些事先不确定插入行数的"INSERT"语句), 这些操作在SBR模式数据复制时都是安全的.
简单来说, 这个锁模式明显的提升了在使用SBR复制时的可扩展性以及安全性. 更深入的, 像"传统锁模式"那样对于特定的SQL语句分配的auto-increment值是连续的.
一个例外是"Mixed-mode inserts", 用户指定了一些auto-increment值, 有些则没有指定, "Simple inserts"插入多行数据. 对这些插入, InnoDB分配了比插入行数更多的auto-increment值. 所有auto-increment自动的连续产生(所有比最近之前执行的语句的auto-increment值大), 剩余没用的auto-increment值就忽略(丢失)不用了.
- "Bulk inserts"使用特殊的AUTO-INC table-level lock, 并且持有锁到语句结束. 包括所有的
-
innodb_autoinc_lock_mode = 2 (交错锁模式)
这个锁模式下, 所有"INSERT-like"语句不使用table-level AUTO-INC lock, 同一时刻SQL语句可以并发执行. 这是最快的, 更高扩展性的锁模式, 但是在使用SBR复制或者恢复场景中回放binary log时却是不安全的.
这个锁模式下, auto-increment值在所有并发执行的"INSERT-like"语句中保持唯一以及单调增长. 同一时刻多条SQL语句产生的交错的auto-increment值.
如果只有"Simple inserts"执行, 那么将不会产生的间隙的auto-increment值(排除"Mixdex-mode inserts"); 当执行"Bulk-inserts"时, 任何执行的SQL都可能产生间隙的auto-increment值.
-
InnoDB AUTO_INCREMENT Lock Mode Usage Implications
-
Using auto-increment with replication
-
SBR复制, 使用innodb_autoinc_lock_mode = 0/1是安全的.
innodb_autoinc_lock_mode = 2或者主备配置不同的lock mode将不能确保主备上产生相同的auto-increment值.
-
RBR(或者mixed-format)复制, 所有lock mode都是安全的.
因为行级别的复制对SQL的执行顺序不敏感. (mixed-format复制将任何SBR复制不安全的SQL语句使用RBR复制)
-
-
"Lost" auto-increment values and sequence gaps
在所有的lock mode中, 如果事务在产生auto-increment后执行roll back, 那么这个auto-increment值将"lost", 这样的auto-increment值是不会被重用的. 在auto-increment列的表中可能会出现间隙的自增长值现象.
-
Specifying NULL or 0 for the AUTO_INCREMENT column
在所有的lock mode中, 如果用户在
INSERT
语句中为auto-increment列指定了NULL或者0, InnoDB会将其对待为像没有特别指定那样, 为其产生一个新的auto-increment值. -
Assigning a negative value to the AUTO_INCREMENT column
在所有的lock mode中, 为auto-increment列指定一个负值是没有定义auto-increment机制行为的.
-
If the AUTO_INCREMENT value becomes larger than the maximum integer for the specified integer type
在所有的lock mode中, 如果指定auto-increment值比auto-increment列类型可以存储的最大值还大是没有定义auto-increment机制行为的.
-
Gaps in auto-increment values for "bulk inserts"
如果innodb_autoinc_lock_mode = 0/1, "Bulk-inserts"语句产生的auto-increment值是连续的, 并且没有间隙. 因为table-level AUTO-INC lock持有直到语句执行结束, 并且同一时刻只有一条SQL在执行.
如果innodb_autoinc_lock_mode = 2, "Bulk-inserts"语句产生的auto-increment值可能出现间隙(出现的条件是: 存在并发执行"INSERT-like"语句).
-
Auto-increment values assigned by "mixed-mode inserts"
考虑"mixed-mode insert", 当"Simple inserts"指定了一些插入行的auto-increment值(并发所有), 这样的SQL在不同的lock mode下表现的行为是不一样的.
譬如: 假如表t1的列c1是auto-increment的, 最新自动产生的序列值是100.
mysql> CREATE TABLE t1 ( -> c1 INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, -> c2 CHAR(1) -> ) ENGINE = INNODB;
考虑下面的"mixed-mode insert"语句:
mysql> INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
innodb_autoinc_lock_mode = 0, 4条新插入的行数据是:
mysql> SELECT c1, c2 FROM t1 ORDER BY c2; +-----+------+ | c1 | c2 | +-----+------+ | 1 | a | | 101 | b | | 5 | c | | 102 | d | +-----+------+
下一个可用的auto-increment值将会是103, 因为auto-increment值是一次分配一个, 并不是SQL语句开始执行的时候分配. 这个结果是可以确定的, 无论是否并发执行"INSERT-like"语句.
innodb_autoinc_lock_mode = 1, 4条新插入的行数据依然是:
mysql> SELECT c1, c2 FROM t1 ORDER BY c2; +-----+------+ | c1 | c2 | +-----+------+ | 1 | a | | 101 | b | | 5 | c | | 102 | d | +-----+------+
然而在这个case中, 下一个可用的auto-increment值将会是105(并发是103), 因为四个auto-increment值是语句开始处理时分配的, 但是只有两个被使用. 这个结果是可以确定的, 无论是否并发执行"INSERT-like"语句.
innodb_autoinc_lock_mode = 2, 4条新插入的行数据将会是:
mysql> SELECT c1, c2 FROM t1 ORDER BY c2; +-----+------+ | c1 | c2 | +-----+------+ | 1 | a | | x | b | | 5 | c | | y | d | +-----+------+
x和y的值将会是唯一并且大于任何之前产生的值, x和y的值取决于并发执行的SQL语句产生的auto-increment值.
最后, 考虑下面的语句, 假如最新产生的序列值是4:
mysql> INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
对于任何级别的lock mode, 该语句将会产生duplicate-key错误 23000(Can't write; duplicate key in table), 因为5将会被自动分配给(NULL,'b')作为auto-increment值, 但是(5,'c')插入将会失败.
-
Modifying AUTO_INCREMENT column values in the middle of a sequence of
INSERT
statements在所有lock mode级别下, 在一序列
INSERT
语句中修改auto-increment值将会导致"Duplicate entry"错误.假如, 将行一个
UPDATE
语句修改auto-increment值为当前最大的auto-increment值还大, 随后INSERT
语句将会遇到自动产生的auto-increment值发生"Duplicate entry"错误, 如下例:mysql> CREATE TABLE t1 ( -> c1 INT NOT NULL AUTO_INCREMENT, -> PRIMARY KEY (c1) -> ) ENGINE = InnoDB; mysql> INSERT INTO t1 VALUES(0), (0), (3); mysql> SELECT c1 FROM t1; +----+ | c1 | +----+ | 1 | | 2 | | 3 | +----+ mysql> UPDATE t1 SET c1 = 4 WHERE c1 = 1; mysql> SELECT c1 FROM t1; +----+ | c1 | +----+ | 2 | | 3 | | 4 | +----+ mysql> INSERT INTO t1 VALUES(0); ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY'
InnoDB AUTO_INCREMENT Counter Initialization
InnoDB如何初始化auto-increment计数器?
如果一个InnoDB表指定了一个auto-increment列, 在InnoDB数据字典中将会包含一个特殊的计数器, 叫做auto-increment counter
, 这个计数器仅仅存储在内存中.
mysqld server restart后会初始化auto-increment counter
计数器, 第一次向包含auto-increment列的表里插入数据时InnoDB会执行下面的语句:
-
初始化
SELECT MAX(ai_col) FROM table_name FOR UPDATE;
-
加步长
将得到的结果+1(默认)作为auto-increment计数器的起始值(auto_increment_increment参数可以指定步长)
如果表是空的, InnoDB使用1作为计数器的起始值(auto_increment_offset参数可以指定起始值)
如果SHOW TABLE STATUS
语句在auto-increment counter
计数器初始化前执行, InnoDB将会执行初始化, 但不会增加该值. 这个值将会存储着一辈后来的inserts
使用, 初始化使用简单的排他(exclusive-locking)锁读取表, 锁持续到事务结束.
在auto-increment counter
计数器被初始化后, 如果没有为auto-increment列特别指定值, 那么InnoDB将会增加计数器并且分配自增长值. 如果为auto-increment列特别指定了插入值, 并且插入值比当前计数器值大, 计数器将会被设置为指定的值.
在server运行期间InnoDB使用内存型的auto-increment counter
, 当server stopped并且restart, InnoDB在第一次插入表数据时为该表初始化计数器, 如之前讨论的那样.
server restart后同样会取消影响表级别的AUTO_INCREMENT = N
(CREATE TABLE
和ALTER TABLE
语句).
讨论
- INSERT INTO t1...SELECT ... FROM t该SQL导致故障, 想想为什么?
参考资料
http://dev.mysql.com/doc/refman/5.5/en/innodb-auto-increment-handling.html
http://dev.mysql.com/doc/refman/5.5/en/innodb-locks-set.html