完蛋!!!线上运行好好的数据库,突然数据插不进去了

154 阅读5分钟

「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

在一天加班晚上吃饭的时候,公司的同事跟我分享了一例他们刚刚遇到的问题,就是数据库的主键自增ID用完了,导致他们的数据无法插进去,他们是商品组的,公司的系统承接的商家比较多,商品量比较大,我想这商品的数量也不至于这么大把,能够把自增ID用完,当时就挺觉得不可思议,居然能够把自增ID用完。后来他们去排查问题,是公司为了后期有些系统分库分表好操作就将数据库的ID自增步长设置为40,然而他们的ID设的是int型,这样的就会导致自增ID用完的那天,商品系统肯定会不停的增加商品和删除商品,这样的表里的数据实际没有很多,但是频繁的删除就会导致ID自增用的比较快,后来他们的解决方案是将int改为bigint(32),这个临时的方案用来快速恢复线上任务。后来跟他们了解到商品的自增ID对于他们业务来说没有任何含义,他们自己生成了一个商品ID,在后面的业务操作中他们就用的是商品ID,后来他们启动了一项大工程,将主键自增ID去掉,采用商品ID来作为主键,至于这怎么无缝衔接的方案就不讨论了。那么接下来的例子,模拟下自增ID用完,插入数据后数据库被报怎样的错误。

自增ID用完

当把自增ID设为int型的时候,其实这个ID的上限值就固定了为: 2^32 -1(4294967295),对于表数据频繁的新增删除,总有天会用完,那么接下来模拟下,自增主键用完,数据库报怎样的错误。

  • 新建一张测试表
CREATE TABLE `test1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  • 设置表的自增序号
alter table test1 AUTO_INCREMENT=4294967295;

为了模拟插入没有自增ID,就直接将自增序号设置到最后一位,这样的话,再次插入就无法获取到自增ID,这个时候我们候在插入数据看下会报怎样的错误:

INSERT INTO test1(name) VALUES ('最后一位');

image.pngimage.png

可以看到数据库报了一个这样的错误:

1467 - Failed to read auto-increment value from storage engine

存储引擎没有自增值可用,会直接插入数据失败,将具体的错误抛给客户端,由客户端来决定后续的操作。

InnoDB自带的row_id

大家都知道,使用InnoDB引擎,当我们没有指定主键的时候,系统会自动给我们创建一个不可见,长度为6个字节的row_id,没插入一条数据后,系统的row_id 的值加1,但是InnoDB在实现row_id的时候,实际上是一个长度为8字节的无符号长整型,但是给row_id只留了6个字节的长度,所以就决定了row_id的值范围是:0到2^48-1。下面来模拟下row_id用完后的场景。

  • 新建一张测试表
CREATE TABLE `test2` ( 
  `name` varchar(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  • 修改row_id自增序号

修改row_id 的自增序号,比自增ID麻烦,需要使用gdb来修改操作系统的自增row_id来实现

  1. 查找MySQL的pid
ps -ef |grep mysql

image.png

  1. 安装gdb
yum install gdb
  1. 使用gbd修改row_id值
gdb -p 1282 -ex 'p dict_sys.row_id=2814749767106561' --batch

image.png

  1. 模拟row_id d用完的情况
  • 先往数据库插入10条数据
INSERT INTO test2(name) VALUES ('最后一位1');
INSERT INTO test2(name) VALUES ('最后一位2');
INSERT INTO test2(name) VALUES ('最后一位3');
INSERT INTO test2(name) VALUES ('最后一位4');
INSERT INTO test2(name) VALUES ('最后一位5');
INSERT INTO test2(name) VALUES ('最后一位6');
INSERT INTO test2(name) VALUES ('最后一位7');
INSERT INTO test2(name) VALUES ('最后一位8');
INSERT INTO test2(name) VALUES ('最后一位9');
INSERT INTO test2(name) VALUES ('最后一位10');

image.png

  • 执行第3条,使用gdb修改row_id序列号,由于这个是操作系统层面的,所以执行完成后要快速的执行二次插入语句,为了区分,二次插入就插入5条数据,插入语句:
INSERT INTO test2(name) VALUES ('二次插入1');
INSERT INTO test2(name) VALUES ('二次插入2');
INSERT INTO test2(name) VALUES ('二次插入3');
INSERT INTO test2(name) VALUES ('二次插入4');
INSERT INTO test2(name) VALUES ('二次插入5');
  • 查询看到结果

image.png

PS:能够模拟出上面的情况,必须手速要快,执行了gdb的时候,就要立刻执行二次插入语句,不然系统的row_id可能会被其他应用使用递增而导致复位,无法模拟出想要的结果。

看到很神奇的一幕,二次插入的数据直接覆盖了第一次插入的数据,所以从这个实验可以得出,row_id用完了,InnoDB不会报错而是会覆盖原来的数据重新写入。这种情况就比较危险,如果真到了这步就会覆盖数据,这样就意味着数据丢失,影响数据的可靠性,会导致数据错误,系统可靠性无法得到保障。