[MySQL] AUTO_INCREMENT lock Handing in InnoDB

时间:2022-06-16 01:10:54

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列的值, 有些没有指定, 譬如:

    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值就忽略(丢失)不用了.

    • 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会执行下面的语句:

  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导致故障, 想想为什么?

参考资料

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