mysql多事务回滚自增主键不连续的原因

一、自增主键不连续的原因:

对于表:

CREATE TABLE `t` (

`id` int(11) NOT NULL AUTO_INCREMENT,

`c` int(11) DEFAULT NULL,

`d` int(11) DEFAULT NULL,

PRIMARY KEY (`id`),

UNIQUE KEY `c` (`c`)

) ENGINE=InnoDB;

其中id是自增主键字段、c是唯一索引。

1、自增值保存位置:

表的结构定义存放在后缀名为.frm的文件中,不会保存自增值。

不同的引擎对于自增值的保存策略不同。

MyISAM引擎的自增值保存在数据文件中

InnoDB引擎的自增值,其实是保存在了内存里,并且到了MySQL 8.0版本后,才有了“自增值持久化”的能力,也就是才实现了“如果发生重启,表的自增值可以恢复为MySQL重启前的值”,具体情况是:

在MySQL 5.7及之前的版本,自增值保存在内存里,并没有持久化。每次重启后,第一次打开表的时候,都会去找自增值的最大值max(id),然后将max(id)+1作为这个表当前的自增值。

举例来说,如果一个表当前数据行里最大的id是10,AUTO_INCREMENT=11。这时候,我们删除id=10的行,AUTO_INCREMENT还是11。但如果马上重启实例,重启后这个表的AUTO_INCREMENT就会变成10。也就是说,MySQL重启可能会修改一个表的AUTO_INCREMENT的值。

在MySQL 8.0版本,将自增值的变更记录在了redo log中,重启的时候依靠redo log恢复重启之前的值。

2、自增值修改机制

在MySQL里面,如果字段id被定义为AUTO_INCREMENT,在插入一行数据的时候,自增值的行为如下:

  • 如果插入数据时id字段指定为0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT值填到自增字段;

  • 如果插入数据时id字段指定了具体的值,就直接使用语句里指定的值。

根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。假设,某次要插入的值是X,当前的自增值是Y。

如果X

如果X≥Y,就需要把当前自增值修改为新的自增值。

新的自增值生成算法是:从auto_increment_offset开始,以auto_increment_increment为步长,持续叠加,直到找到第一个大于X的值,作为新的自增值。其中,auto_increment_offset 和 auto_increment_increment是两个系统参数,分别用来表示自增的初始值步长,默认值都是1。

注意:在一些场景下,使用的就不全是默认值。比如,双M的主备结构里要求双写的时候,就可能会设置成auto_increment_increment=2,让一个库的自增id都是奇数,另一个库的自增id都是偶数,避免两个库生成的主键发生冲突。

当auto_increment_offset和auto_increment_increment都是1的时候,新的自增值的生成逻辑:如果准备插入的值>=当前自增值,新的自增值就是“准备插入的值+1”;否则,自增值不变。

3、自增值的修改时机:

假设表t里面已经有了(1,1,1)这条记录,这时再执行一条插入数据命令:

insert into t values(null, 1, 1);

(1)、该语句的执行流程:

执行器调用InnoDB引擎接口写入一行,传入的这一行的值是(0,1,1);

InnoDB发现用户没有指定自增id的值,获取表t当前的自增值2;

将传入的行的值改成(2,1,1);

将表的自增值改成3;

继续执行插入数据操作,由于已经存在c=1的记录,所以报Duplicate key error,语句返回。

insert(null, 1,1)唯一键冲突

可以看到,这个表的自增值由2改成3,是在真正执行插入数据的操作之前。这个语句真正执行的时候,因为碰到唯一键c冲突,所以id=2这一行并没有插入成功,但也没有将自增值再改回去。所以,在这之后,再插入新的数据行时,拿到的自增id就是3。也就是说,出现了自增主键不连续的情况。

(2)、完整复现:

4、自增值不能回退的原因:

假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增id,肯定要加锁,然后顺序申请。

假设事务A申请到了id=2, 事务B申请到id=3,那么这时候表t的自增值是4,之后继续执行。

事务B正确提交了,但事务A出现了唯一键冲突。

如果允许事务A把自增id回退,也就是把表t的当前自增值改回2,那么就会出现这样的情况:表里面已经有id=3的行,而当前的自增id值是2。

接下来,继续执行的其他事务就会申请到id=2,然后再申请到id=3。这时,就会出现插入语句报错“主键冲突”。

对于主键冲突,有两种方法解决方法:

每次申请id之前,先判断表里面是否已经存在这个id。如果存在,就跳过这个id。但是,这个方法的成本很高。因为,本来申请id是一个很快的操作,现在还要再去主键索引树上判断id是否存在。

把自增id的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。

可见,这两个方法都会导致性能问题。造成这些麻烦的罪魁祸首,就是我们假设的这个“允许自增id回退”的前提导致的。因此,InnoDB放弃了这个设计,语句执行失败也不回退自增id。也正是因为这样,所以才只保证了自增id是递增的,但不保证是连续的。

5、自增锁的优化:

(1)、自增锁:

在MySQL 5.0版本的时候,自增锁的范围是语句级别。也就是说,如果一个语句申请了一个表自增锁,这个锁会等语句执行结束以后才释放。显然,这样设计会影响并发度。

MySQL 5.1.22版本引入了一个新策略,新增参数innodb_autoinc_lock_mode,默认值是1。

这个参数的值被设置为0时,表示采用之前MySQL 5.0版本的策略,即语句执行结束后才释放锁;

这个参数的值被设置为1时:

普通insert语句,自增锁在申请之后就马上释放;

类似insert … select这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;

这个参数的值被设置为2时,所有的申请自增主键的动作都是申请后就释放锁。

(2)、默认设置下,insert … select 要使用语句级的锁的原因:

这么设计是为了数据的一致性。对于下面的场景:

批量插入数据的自增锁如果session B是申请了自增值以后马上就释放自增锁,那么就可能出现这样的情况:

session B先插入了两个记录,(1,1,1)、(2,2,2);

然后,session A来申请自增id得到id=3,插入了(3,5,5);

之后,session B继续执行,插入两条记录(4,3,3)、 (5,4,4)。

此时就导致了session A和session B不一致,而当binlog_format=statement时,由于两个session是同时执行插入数据命令的,所以binlog里面对表t2的更新日志只有两种情况:要么先记session A的,要么先记session B的。但不论是哪一种,这个binlog拿去从库执行,或者用来恢复临时实例,备库和临时实例里面,session B这个语句执行出来,生成的结果里面,id都是连续的。这时,这个库就发生了数据不一致。

(3)、库发生数据不一致的原因及解决方法:

(1)、原因:

因为原库session B的insert语句,生成的id不连续。这个不连续的id,用statement格式的binlog来串行执行,是执行不出来的。

(2)、解决方法:

一种思路是,让原库的批量插入数据语句,固定生成连续的id值。所以,自增锁直到语句执行结束才释放,就是为了达到这个目的。

另一种思路是,在binlog里面把插入数据的操作都如实记录进来,到备库执行的时候,不再依赖于自增主键去生成。这种情况,其实就是innodb_autoinc_lock_mode设置为2,同时binlog_format设置为row。

因此,在生产上,尤其是有批量插入数据的场景时,从并发插入数据性能的角度考虑,建议这样设置:innodb_autoinc_lock_mode=2 ,并且 binlog_format=row.这样做,既能提升并发性,又不会出现数据一致性问题。

需要注意的是,这里说的批量插入数据,包含的语句类型是insert … select、replace … select和load data语句。

(4)、对于批量插入数据的语句,MySQL有一个批量申请自增id的策略:

语句执行过程中,第一次申请自增id,会分配1个;

1个用完以后,这个语句第二次申请自增id,会分配2个;

2个用完以后,还是这个语句,第三次申请自增id,会分配4个;

依此类推,同一个语句去申请自增id,每次申请到的自增id个数都是上一次的两倍。

比如对于下面语句:

insert into t values(null, 1,1);
insert into t values(null, 2,2);
insert into t values(null, 3,3);
insert into t values(null, 4,4);

create table t2 like t;
insert into t2(c,d) select c,d from t;
insert into t2 values(null, 5,5);

insert…select,实际上往表t2中插入了4行数据。但是,这四行数据是分三次申请的自增id,第一次申请到了id=1,第二次被分配了id=2和id=3, 第三次被分配到id=4到id=7。由于这条语句实际只用上了4个id,所以id=5到id=7就被浪费掉了。之后,再执行insert into t2 values(null, 5,5),实际上插入的数据就是(8,5,5)。

6、自增主键不连续的原因:

  •  唯一键冲突是导致自增主键id不连续的第一种原因。

  •  事务回滚是导致自增主键id不连续的第二种原因。

  •  批量申请自增id的策略是导致自增主键id不连续的第三种原因。

3fe896ea709be177ef1cfe90075d3d05.jpeg

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值