本文翻译自MySQL官方手册,对InnoDB的AUTO_INCREMENT处理作了介绍。
InnoDB提供了可配置的锁机制,可显著提升在有AUTO_INCREMENT列的表中插入行记录的SQL语句的可伸缩性和性能。
为了在InnoDB表中使用AUTO_INCREMENT机制,必须将AUTO_INCREMENT列定义为某个索引的第一列(联合索引)或唯一列,以便于在表上执行相当于索引列的SELECT MAX(ai_col)查找以获得列的最大值。
索引不需要是主键或唯一索引,但是为了避免AUTO_INCREMENT列的值重复,建议使用这些索引类型。
接下来介绍AUTO_INCREMENT锁模式,不同AUTO_INCREMENT锁模式设置的使用含义,以及InnoDB如何初始化AUTO_INCREMENT计数器。
InnoDB AUTO_INCREMENT锁模式
本节描述用于生成自动增量值的AUTO_INCREMENT锁模式,以及每种锁模式对复制的影响。自动增量(auto-increment)锁模式在服务启动时使用innodb_autoinc_lock_mode
变量配置。
以下术语被用于介绍innodb_autoinc_lock_mode
设置:
-
“类插入(INSERT-like)”语句
指所有在表中生成新行的语句,包括
INSERT
,INSERT ... SELECT
,REPLACE
,REPLACE ... SELECT
, andLOAD DATA
语句。包括下面提到的“简单插入(simple-inserts)”、“批量插入(bulk-inserts)”、以及“混合模式(mixed-mode)”插入。 -
“简单插入(simple-inserts)”
指可以提前(在开始处理语句时)确定插入行数的语句。包括单行、多行的不包括嵌套子查询的INSERT和REPLACE语句,但是不包括
INSERT ... ON DUPLICATE KEY UPDATE
。 -
“批量插入(Bulk inserts)”
指不能提前知道插入行数(以及所需自动增量值的数量)的语句。包括
INSERT ... SELECT
、REPLACE ... SELECT
、以及LOAD DATA
语句,但不是普通的插入语句。InnoDB需要在处理每一行时为AUTO_INCREMENT列分配一个新值。 -
“混合模式插入(Mixed-mode inserts)”
包括为新行中的一部分(而非全部)指定了自动增量值的“简单插入(simple insert)”语句。下面是一个例子,c1是表t1的自增列:
INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
-
另一种类型的混合模式插入是
INSERT ... ON DUPLICATE KEY UPDATE
,在最坏的情况下实际上是INSERT,紧接着是UPDATE,在此期间,为自增列分配的值可能在更新阶段使用,也可能不用。
innodb_autoinc_lock_mode
变量有三种可能的设置。分别是0(传统的),1(连续的),2(交错的)。从MySQL 8.0开始,默认设置是交错锁模式(innodb_autoinc_lock_mode
=2)。在8.0之前,默认设置是连续锁模式(innodb_autoinc_lock_mode
=1)。
MySQL 8.0的默认设置反映了其默认复制类型从基于语句的复制变为了基于行复制。基于语句的复制需要连续自增锁模式,用来保证在给定的SQL语句下,自增值按照可预测和可重复的顺序分配,但是基于行的复制则对SQL语句的执行顺序不敏感。
-
innodb_autoinc_lock_mode = 0
(传统锁模式)传统锁模式提供了与引入
innodb_autoinc_lock_mode
变量之前相同的行为。传统锁模式选项是为了向后兼容、性能测试和解决“混合模式插入”问题而提供的,因为可能存在语义上的差异。在这种模式下,所有在表上有自增列的类插入(INSERT-like)语句都要获取一个AUTO_INC表级锁。这个锁通常持有到语句结束(不是事务结束),以确保为给定的INSERT语句顺序以可预测和可重复的顺序分配自增值,并确保给任何语句分配的自增值都是连续的。
对于基于语句的复制,这意味着当在副本服务器上复制SQL语句时,自增列使用与源服务器上相同的值。执行多个INSERT语句的结果是确定的,并且副本与源服务器有相同的数据。如果多个INSERT语句生成的自动增量值是交错的,那么两个并发INSERT语句的结果将是不确定的,并且不能使用基于语句的复制可靠地传递数据到副本服务器。
为了更清楚的说明这一点,考虑用这张表的例子:
CREATE TABLE t1 (c1 INT(11) NOT NULL AUTO_INCREMENT,c2 VARCHAR(10) DEFAULT NULL,PRIMARY KEY (c1) ) ENGINE=InnoDB;
假如有两个事务正在运行,每个事务都将行插入到具有AUTO_INCREMENT列的表中。一个事务正在使用INSERT…SELECT语句插入1000行,另一个使用简单的INSERT语句插入一行:
Tx1: INSERT INTO t1 (c2) SELECT 1000 rows from another table ...
Tx2: INSERT INTO t1 (c2) VALUES ('xxx');
InnoDB无法提前知道Tx1事务的INSERT语句从SELECT中检索了多少行,并且随着插入的进行,它一次分配一个自增值。对于表级锁(持有到语句末尾),一次只能执行一条引用表t1的INSERT语句,并且不同语句生成的自动递增数不会交错。由Tx1事务的INSERT…SELECT语句是连续的,并且Tx2中INSERT语句使用的(单个)自动增量值要么更小(在Tx1之前),要么更大(在Tx1之后)。
只要SQL语句从二进制日志中重放时(在使用基于语句的复制时,或在恢复场景中)以相同的顺序执行,结果就与Tx1和Tx2首次运行时相同。因此,一直持有到语句结束的表级锁使得使用自增值的INSERT语句对于基于语句的复制是安全的。但是,当多个事务同时执行插入语句时,这些表级锁会限制并发性和可伸缩性。
在前面的例子中,如果没有表级锁,则Tx2中INSERT所使用的自动递增列的值取决于语句的执行时间。如果Tx2的INSERT在Tx1的INSERT运行时执行(而不是在它开始之前或完成之后),则两个INSERT语句分配的自增值是不确定的,并且可能因运行而异。
在连续锁模式下,InnoDB可以避免对预先知道行数的“简单插入”语句使用表级AUTO-INC锁,并且仍然保持确定性执行和基于语句复制的安全性。
如果不使用二进制日志重放SQL语句作为恢复或复制的一部分,可以使用交错锁模式来消除所有表级AUTO-INC锁的使用,以获得更高的并发性和性能,但代价是允许语句分配的自增值出现间隙,并且可能会让并发执行语句分配的值交错。
-
innodb_autoinc_lock_mode = 1
(连续锁模式)在这种模式下,“批量插入”使用AUTO-INC表级锁并持有直到语句结束。这适用于所有
INSERT ... SELECT
、REPLACE ... SELECT
、以及LOAD DATA
语句。同一时间只有一条持有AUTO-INC锁的语句可以执行。如果批量插入操作的源表与目标表不同,则在对源表中选择的第一行取得共享锁之后,对目标表获取AUTO-INC锁。如果批量插入操作的源表和目标表是同一个表,那么AUTO-INC锁将在共享锁获取之后被获取。“简单插入”(插入行数事先已知)通过在互斥锁(轻量级锁)的控制下获得所需数量的自动递增值来避免表级AUTO-INC锁,互斥锁只在分配过程期间持有,无需等到语句完成。除非AUTO-INC锁被另一个事务持有,否则不会使用AUTO-INC表级锁。如果另一个事务持有AUTO-INC锁,则“简单插入”等待AUTO-INC锁,就像“批量插入”一样。
在存在事先不知道行数的INSERT语句时(在语句进行时分配自增值数量),这种锁模式确保任何“类插入”语句分配的所有自增值都是连续的,并且操作对于基于语句的复制是安全的。
这种锁模式大大提高了可伸缩性,同时对于基于语句的复制来说是安全的。此外,与传统锁模式一样,任何给定语句分配的自动递增数是连续的。对于任何使用自增值的语句,与传统模式相比,在语义上没有变化,但有一个重要的特例。
特例是“混合模式插入”,其中用户为多行“简单插入”中的一些(而非全部)行提供AUTO_INCREMENT列的显式值。对于这样的插入,InnoDB分配的自增值比要插入的行数要多。但是,“多余”的自增值被丢弃了。
-
innodb_autoinc_lock_mode = 2
(交错锁模式)在这种锁模式下,没有“类插入”语句使用AUTO-INC表级锁,并且多个语句可以同时执行。这是最快和最具可伸缩性的锁模式,但是当使用基于语句的复制或从二进制日志中重放SQL语句的恢复场景时,它并不安全。
在这种锁模式下,自增值保证在所有并发执行的“类插入”语句中是唯一且单调递增的。但是,由于多个语句可以同时生成值(即值的分配在多个语句间交错进行),因此为任何插入语句生成的值可能都不是连续的。
如果只执行“简单插入”语句,其中要插入的行数是事先已知的,则为单个语句生成的行数没有间隔,除了“混合模式插入”。然而,当执行“批量插入”时,给任何语句分配的自增值都可能存在间隙。
InnoDB AUTO_INCREMENT锁模式的使用含义
-
对复制使用自动增量
如果使用基于语句的复制,将innodb_autoinc_lock_mode设置为0或1,并在源及其副本上使用相同的值。如果使用2或者源和副本不使用相同配置,则不能确保副本上的自增值与源上相同。
如果使用的是基于行或混合格式的复制,那么所有的锁模式都是安全的,因为基于行的复制对SQL语句的执行顺序不敏感(混合格式使用基于行的复制来处理对于基于语句的复制来说不安全的语句)。
-
“丢失”自增值和序列间隙
在所有的锁模式(0、1和2)中,如果一个事务产生了自增值回滚,那么这些自增值就会“丢失”。一旦为自增列生成了值,无论“类插入”语句是否完成,也无论事务是否回滚,都不能回滚该值。这些丢失的值不会被重用。因此,存储在表的AUTO_INCREMENT列中的值可能存在间隙。
-
为AUTO_INCREMENT列指定NULL或0
在所有锁模式下(0、1、2),如果用户在INSERT操作中为AUTO_INCREMENT列指定了NULL或0,InnoDB会将该行视为没有指定该值,并为其生成一个新值。
-
给AUTO_INCREMENT列赋一个负值
在所有锁模式(0、1和2)中,自增机制没有明确给AUTO_INCREMENT列赋一个负值会发生什么。
-
如果AUTO_INCREMENT值变得大于指定整数类型的最大整数
在所有锁模式(0、1和2)中,未明确会发什么。
-
“批量插入”的自动增量值存在间隙
当innodb_autoinc_lock_mode设置为0(“传统”)或1(“连续”)时,任何语句生成的自增值都是连续的,没有间隔,因为AUTO-INC表级锁一直持有到语句结束,并且同时只能执行一个这样的语句。
当innodb_autoinc_lock_mode设置为2(“interleaved”)时,“批量插入”生成的自增值可能会有间隙,但只有在并发执行“类插入”语句时才会。
对于锁模式1或2,连续语句之间可能会出现间隙,因为对于批量插入,因为不知道每个语句所需的自增值的确切数量,所以可能会高估。
-
由“混合模式插入”分配的自增值
考虑“混合模式插入”时,其中“简单插入”为一些(而非全部)行指定自增值。这样的语句在锁模式0、1和2中的行为不同。例如,假设c1是表t1的AUTO_INCREMENT列,并且最新已生成自增值是100。
mysql> CREATE TABLE t1 (-> c1 INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, -> c2 CHAR(1)-> ) ENGINE = INNODB;
现在,考虑下面的“混合模式插入”语句:
mysql> INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');
当innodb_autoinc_lock_mode设置为0 (" 传统")时,这四个新行是:
mysql> SELECT c1, c2 FROM t1 ORDER BY c2; +-----+------+ | c1 | c2 | +-----+------+ | 1 | a | | 101 | b | | 5 | c | | 102 | d | +-----+------+
下一个可用的自增值是103,因为每次分配一个自增值,而不是在语句执行开始时一次性分配所有的自增值。无论是否并发执行“类插入”语句(任何类型),这个结果都为真。
当innodb_autoinc_lock_mode设置为1("连续")时,这四个新行也是:
mysql> SELECT c1, c2 FROM t1 ORDER BY c2; +-----+------+ | c1 | c2 | +-----+------+ | 1 | a | | 101 | b | | 5 | c | | 102 | d | +-----+------+
而在本例中,下一个可用自增值是105,而不是103,因为在处理语句时分配了四个自增值,但只使用了两个。无论是否并发执行“类插入”语句(任何类型),这个结果都为真。
innodb_autoinc_lock_mode设置为2 (" 交错"),这四个新行是:
mysql> SELECT c1, c2 FROM t1 ORDER BY c2; +-----+------+ | c1 | c2 | +-----+------+ | 1 | a | | x | b | | 5 | c | | y | d | +-----+------+
x和y的值是唯一的,且大于之前生成的任何行。但是,x和y的具体值取决于并发执行语句生成的自增值的数量。
最后,考虑以下语句,当最新已生成自增值是100时发出:
mysql> INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (101,'c'), (NULL,'d');
无论innodb_autoinc_lock_mode是什么值,该语句都会生成一个duplicate-key error 23000(Can't write; duplicate key in table),因为101是为行(NULL, 'b')分配的,且行(101,'c')的插入失败。
-
修改INSERT语句序列中间的AUTO_INCREMENT列值
在MySQL 5.7以及更早的版本中,在INSERT语句序列的中间修改AUTO_INCREMENT列值可能会导致“Duplicate entry”错误。例如,如果执行UPDATE操作,将AUTO_INCREMENT列值更改为大于当前最大自增值的值,则后续没有指定未使用过的自增值的INSERT操作可能会遇到“Duplicate entry”错误。在MySQL 8.0及更高版本中,如果你修改AUTO_INCREMENT列的值大于当前的最大自增值,新值将被保留。
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);mysql> SELECT c1 FROM t1;
+----+
| c1 |
+----+
| 2 |
| 3 |
| 4 |
| 5 |
+----+
InnoDB AUTO_INCREMENT计数器初始化
本节介绍InnoDB如何初始化AUTO_INCREMENT计数器。
如果为InnoDB表指定AUTO_INCREMENT列,内存中的表对象将包含一个特殊的计数器,称为自增计数器,用于为该列分配新值。
在MySQL 5.7及更早的版本中,自增计数器存储在主存中,而不是磁盘上。为了在服务器重启后初始化自增计数器,InnoDB会在第一次插入包含AUTO_INCREMENT列的表时执行相当于下面语句的操作。
SELECT MAX(ai_col) FROM table_name FOR UPDATE;
在MySQL 8.0中,这种行为有所变化。当前的最大自增计数器值在每次更改时写入重做日志(redo log),并保存到每个检查点(checkpoint)的数据字典(data dictionary)中。这些更改使当前的最大自增计数器值在服务器重启时保持不变。
当服务器正常关机后重启时,InnoDB使用存储在数据字典中的最大自增值初始化内存中的自增计数器。
当服务器在崩溃恢复期间重启时,InnoDB使用存储在数据字典中的最大自增值初始化内存中的自增计数器,并扫描重做日志中自上次检查点以来写入的自增计数器值。如果重做日志值大于内存计数器值,则应用(apply)重做日志值。但是,在服务器意外退出的情况下,不能保证重用先前已分配的自增值。每次由于INSERT或UPDATE操作而改变当前最大自增值时,新值被写入重做日志,但如果在重做日志刷新到磁盘之前异常退出,则在服务器重启后初始化自增计数器时,可以重用先前分配的值。
InnoDB使用SELECT MAX(ai_col) FROM table_name FOR UPDATE语句初始化自增计数器的唯一情况是导入一个没有.cfg元数据文件的表。否则,如果存在,则从.cfg元数据文件中读取当前最大自增计数器值。除了计数器值初始化之外,当试图使用ALTER table ... AUTO_INCREMENT = N语句将计数器值设置为小于或等于当前使用的计数器值时,服务器将用与SELECT MAX(ai_col) FROM table_name语句等价的语句来确定表当前的最大自增计数器值。例如在删除一些记录后,可能会尝试将计数器值设置为较小的值。在这种情况下,必须搜索表以确保新的计数器值不小于或等于当前的实际最大计数器值。
在MySQL 5.7及更早的版本中,服务器重启时AUTO_INCREMENT = N会失效,该选项可以在CREATE table或ALTER table语句中使用,分别用于设置初始计数器值或修改现有计数器值。在MySQL 8.0中,服务器重启时AUTO_INCREMENT = N不会失效。如果将自增计数器初始化为特定值,或者将自增计数器值更改为更大的值,则新值将在服务器重启时持久化。
注意:ALTER TABLE ... AUTO_INCREMENT = N
只能将自增计数器的值修改为大于当前最大值的值。
在MySQL 5.7及更早的版本中,在ROLLBACK操作之后立即重启服务器可能会导致重用先前分配给回滚事务的自增值,从而有效地回滚当前的最大自增值。在MySQL 8.0中,当前的最大自增值是持久的,无法重用以前分配的值。
如果一个SHOW TABLE STATUS语句在自增计数器初始化之前检查(examine)了一个表,InnoDB打开表并使用当前存储在数据字典中的最大自增值初始化计数器值。然后将该值存储在内存中,以供以后的插入或更新使用。计数器值的初始化使用排它锁在表上读取,该读取持续到事务结束。InnoDB在为用户指定的自增值大于0的新创建表初始化自增计数器时遵循相同的过程。
在自增计数器初始化之后,如果在插入行时没有显式地指定一个自增值,InnoDB会隐式地递增计数器并将新值赋给该列。如果插入显式指定自增列值的行,并且该值大于当前最大计数器值,则将计数器设置为指定的值。
只要服务器在运行,InnoDB就会使用内存中的自动增量计数器。当服务器停止和重启时,InnoDB重新初始化自动增量计数器,如前所述。
auto_increment_offset变量确定AUTO_INCREMENT列值的起始点。默认为1。
auto_increment_increment变量控制值的步长。默认为1。
注意
一般情况下,当AUTO_INCREMENT值用完时,后续的INSERT操作将返回一个重复键错误(duplicate-key error)。
原始文档:MySQL :: MySQL 8.0 Reference Manual :: 17.6.1.6 AUTO_INCREMENT Handling in InnoDB