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指那种可以事先确定插入行数的语句, 包括单行和多行插入, 不包含嵌套自查询的INSERTREPLACE语句(这样的语句也要除外: INSERT...ON DUPLICATE KEY UPDATE)

  • "Bulk inserts"

    Bulk inserts指那种事先无法确定插入行数的语句, 包括INSERT...SELECTREPLACE...SELECT, 以及LOAD DATA语句. InnoDB在处理每一行的时候为新纪录分配一个自增值.

  • "Mixed-mode inserts"

    Mixed-mode inserts指的是simple inserts类型中有些行指定了auto_increment列的值, 有些没有指定, 譬如:

    1. 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:

      1. CREATE TABLE t1 (
      2. c1 INT(11) NOT NULL AUTO_INCREMENT,
      3. c2 VARCHAR(10) DEFAULT NULL,
      4. PRIMARY KEY (c1)
      5. ) ENGINE=InnoDB;

      有两个事务在运行, 都向表中插入auto-increment列.

      tx1: INSERT...SELECT语句, 插入100行.

      tx2: INSERT语句.

      1. tx1: INSERT INTO t1 (c2) SELECT 1000 rows from another table ...
      2. 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值就忽略(丢失)不用了.

    • 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.

    1. mysql> CREATE TABLE t1 (
    2. -> c1 INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    3. -> c2 CHAR(1)
    4. -> ) ENGINE = INNODB;

    考虑下面的"mixed-mode insert"语句:

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

    innodb_autoinc_lock_mode = 0, 4条新插入的行数据是:

    1. mysql> SELECT c1, c2 FROM t1 ORDER BY c2;
    2. +-----+------+
    3. | c1 | c2 |
    4. +-----+------+
    5. | 1 | a |
    6. | 101 | b |
    7. | 5 | c |
    8. | 102 | d |
    9. +-----+------+

    下一个可用的auto-increment值将会是103, 因为auto-increment值是一次分配一个, 并不是SQL语句开始执行的时候分配. 这个结果是可以确定的, 无论是否并发执行"INSERT-like"语句.

    innodb_autoinc_lock_mode = 1, 4条新插入的行数据依然是:

    1. mysql> SELECT c1, c2 FROM t1 ORDER BY c2;
    2. +-----+------+
    3. | c1 | c2 |
    4. +-----+------+
    5. | 1 | a |
    6. | 101 | b |
    7. | 5 | c |
    8. | 102 | d |
    9. +-----+------+

    然而在这个case中, 下一个可用的auto-increment值将会是105(并发是103), 因为四个auto-increment值是语句开始处理时分配的, 但是只有两个被使用. 这个结果是可以确定的, 无论是否并发执行"INSERT-like"语句.

    innodb_autoinc_lock_mode = 2, 4条新插入的行数据将会是:

    1. mysql> SELECT c1, c2 FROM t1 ORDER BY c2;
    2. +-----+------+
    3. | c1 | c2 |
    4. +-----+------+
    5. | 1 | a |
    6. | x | b |
    7. | 5 | c |
    8. | y | d |
    9. +-----+------+

    x和y的值将会是唯一并且大于任何之前产生的值, x和y的值取决于并发执行的SQL语句产生的auto-increment值.

    最后, 考虑下面的语句, 假如最新产生的序列值是4:

    1. 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"错误, 如下例:

    1. mysql> CREATE TABLE t1 (
    2. -> c1 INT NOT NULL AUTO_INCREMENT,
    3. -> PRIMARY KEY (c1)
    4. -> ) ENGINE = InnoDB;
    5. mysql> INSERT INTO t1 VALUES(0), (0), (3);
    6. mysql> SELECT c1 FROM t1;
    7. +----+
    8. | c1 |
    9. +----+
    10. | 1 |
    11. | 2 |
    12. | 3 |
    13. +----+
    14. mysql> UPDATE t1 SET c1 = 4 WHERE c1 = 1;
    15. mysql> SELECT c1 FROM t1;
    16. +----+
    17. | c1 |
    18. +----+
    19. | 2 |
    20. | 3 |
    21. | 4 |
    22. +----+
    23. mysql> INSERT INTO t1 VALUES(0);
    24. 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会执行下面的语句:

  1. 初始化

    1. SELECT MAX(ai_col) FROM table_name FOR UPDATE;
  2. 加步长

    将得到的结果+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 TABLEALTER TABLE语句).

讨论

  • INSERT INTO t1...SELECT ... FROM t该SQL导致故障, 想想为什么?

参考资料

  1. http://dev.mysql.com/doc/refman/5.5/en/innodb-auto-increment-handling.html
  2. http://dev.mysql.com/doc/refman/5.5/en/innodb-locks-set.html

[MySQL] AUTO_INCREMENT lock Handing in InnoDB的更多相关文章

  1. mysql中lock tables与unlock tables

    官网:https://dev.mysql.com/doc/refman/5.0/en/lock-tables.html LOCK TABLES tbl_name [[AS] alias] lock_t ...

  2. [MySQL] gap lock/next-key lock浅析

    当InnoDB在判断行锁是否冲突的时候, 除了最基本的IS/IX/S/X锁的冲突判断意外, InnoDB还将锁细分为如下几种子类型: record lock (RK) 记录锁, 仅仅锁住索引记录的一行 ...

  3. mysql metadata lock(一)

    想必玩过mysql的人对Waiting for table metadata lock肯定不会陌生,一般都是进行alter操作时被堵住了,导致了我们在show processlist 时,看到线程的状 ...

  4. MySQL 全文搜索支持, mysql 5.6.4支持Innodb的全文检索和类memcache的nosql支持

    背景:搞个个人博客的全文搜索得用like啥的,现在mysql版本号已经大于5.6.4了也就支持了innodb的全文搜索了,刚查了下目前版本号都到MySQL Community Server 5.6.1 ...

  5. MySQL中lock tables和unlock tables浅析

    MySQL中lock tables和unlock tables浅析   在MySQL中提供了锁定表(lock tables)和解锁表(unlock tables)的语法功能,ORACLE与SQL Se ...

  6. MySQL存储引擎 - Myisam和Innodb

    Mysql有两种存储引擎:InnoDB与Myisam,下表是两种引擎的简单对比   MyISAM InnoDB 构成上的区别: 每个MyISAM在磁盘上存储成三个文件.第一个 文件的名字以表的名字开始 ...

  7. [MySQL优化案例]系列 — 优化InnoDB表BLOB列的存储效率

    首先,介绍下关于InnoDB引擎存储格式的几个要点:1.InnoDB可以选择使用共享表空间或者是独立表空间方式,建议使用独立表空间,便于管理.维护.启用 innodb_file_per_table 选 ...

  8. mysql 流程函数 存储引擎 InnoDB简单特性

    建表及插入数据语句: mysql> create table salary(userid int,salary decimal(9,2)); Query OK, 0 rows affected ...

  9. MySQL数据库之MyISAM与InnoDB的区别

    MySQL数据库之MyISAM与InnoDB的区别 从以下几个方面: 1.存储结构 每个MyISAM在磁盘上存储成三个文件.第一个文件的名字以表的名字开始,扩展名指出文件类型. .frm文件存储表定义 ...

随机推荐

  1. Android四大组件之Activity & Fragement(续)

    1.Activity和Fragment的异同. Activity是UI界面交互的主体,而fragment是这个主体上的元素. 一个activity可以包含0到n个fragment. fragment可 ...

  2. Contest 3

    A:非常裸的dp. #include<iostream> #include<cstdio> #include<cstdlib> #include<cstrin ...

  3. DP——P2300 合并神犇

    题目背景 loidc来到了NOI的赛场上,他在那里看到了好多神犇. 题目描述 神犇们现在正排成一排在刷题.每个神犇都有一个能力值p[i].loidc认为坐在附近的金牌爷能力参差不齐非常难受.于是loi ...

  4. 转载乙醇大师的appium简明教程

    appium简明教程(11)——使用resource id定位(仅支持安卓4.3以上系统) 乙醇 2014-06-28 21:01 阅读:16406 评论:21 appium简明教程(10)——控件定 ...

  5. 解题:CTSC 2017 吉夫特

    题面 首先有个结论:$C_n^m$为奇数当且仅当$m$是$n$的一个子集 于是从后往前推,记录每个数出现的位置,然后对每个位置枚举子集统计在它后面的贡献即可 #include<cstdio> ...

  6. BZOJ1002【FJOI2007】轮状病毒

    1002: [FJOI2007]轮状病毒 Time Limit: 1 Sec  Memory Limit: 162 MBSubmit: 6917  Solved: 3777[Submit][Statu ...

  7. Codeforces 894.E Ralph and Mushrooms

    E. Ralph and Mushrooms time limit per test 2.5 seconds memory limit per test 512 megabytes input sta ...

  8. 用for语句从数组中剔除数据,注意,count,要放到for语句之外才行

    date_default_timezone_set('Asia/Shanghai'); $arr = array( ,), ,), ,), ,) ); print_r($arr); ;$i<co ...

  9. 「Django」与mysql8连接的若干问题

    1.setting配置 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', #数据库驱动名 'NAME': 'my_tes ...

  10. Splay 区间操作

    据大佬说,\(Splay\)是序列操作之王.\(Splay\)是一种平衡树,通过伸展(\(Splay\)),在不改变中序遍历的前提下变换根的位置,从而快速的进行序列操作 \(Splay\)最常见的序列 ...