MySQL 案例:自增列的空洞问题与“小”BUG

时间:2022-07-23
本文章向大家介绍MySQL 案例:自增列的空洞问题与“小”BUG,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

在 MySQL 的常见规范里面,每个表都要设置主键,一般来说都会推荐自增列作为主键,这和 MySQL 属于聚簇索引表有关,顺序增长的主键比较合适。而自增列中比较常遇见的问题就是自增列的空洞。原生的 MySQL 自增列也存在一个 BUG,可能会影响到数据一致性,本文也会详细介绍,在自建 MySQL 的时候尽量不要踩到这个坑。

空洞问题

问题介绍

自增列的空洞一般指的就是自增列不是连续增长,中间出现一些数值上的断层。比如手动写入自增列的值,就有可能会出现这种现象:

mysql> insert into t1 values(1,1),(3,3),(5,5);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql>
mysql>
mysql> select * from t1;
+----+------+
| id | a    |
+----+------+
|  1 |    1 |
|  3 |    3 |
|  5 |    5 |
+----+------+
3 rows in set (0.00 sec)

mysql> show create table t1;
+-------+---------------------------------------------------------------------------+
| Table | Create Table                                                              |
+-------+---------------------------------------------------------------------------+
| t1    | CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 |
+-------+---------------------------------------------------------------------------+
1 row in set (0.00 sec)

可以看到表只有三行数据,但是自增列的值已经变到了 6,“浪费”了两个值。当浪费的值过多的时候,就可能会出现如下的问题:

mysql> insert into t1 values(2147483647,0);
Query OK, 1 row affected (0.01 sec)

mysql> show create table t1;
+-------+--------------------------------------------------------------------------+
| Table | Create Table                                                             |
+-------+--------------------------------------------------------------------------+
| t1    | CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2147483647 DEFAULT CHARSET=utf8mb4 |
+-------+--------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> insert into t1 values(null,0);
ERROR 1062 (23000): Duplicate entry '2147483647' for key 'PRIMARY'
mysql>

自增列如果浪费过多,即使是 int 也很可能会用完,然后再尝试插入数据的时候就会报错了。

问题的监控

自增列的监控,在 5.7 版本(低于 5.7 的还是尽快升级上来吧)是比较方便的,可以在 sys 视图中直接查询视图

mysql> select * from sys.schema_auto_increment_columnsG
*************************** 1. row ***************************
        table_schema: test
          table_name: t1
         column_name: id
           data_type: int
         column_type: int(11)
           is_signed: 1
         is_unsigned: 0
           max_value: 2147483647
      auto_increment: 2147483647
auto_increment_ratio: 1.0000
*************************** 2. row ***************************
        table_schema: sbtest
          table_name: sbtest1
         column_name: id
           data_type: int
         column_type: int(11)
           is_signed: 1
         is_unsigned: 0
           max_value: 2147483647
      auto_increment: 10000001
auto_increment_ratio: 0.0047
2 rows in set (0.04 sec)

mysql>

通过比较 max_value,auto_increment,auto_increment_ratio 这三个值很容就能监控到即将出现问题的自增列。真的可能会出现问题的时候,就需要及时在业务维护窗口来调整自增列的数据类型,使用 unsigned 或者是 bigint。

问题的避免

如何避免这个问题,涉及到 MySQL 业务实际使用的 insert 语句的类型,这里推荐阅读(官方文档),里面有详细的分析和场景解析。

简单来说,自增列的值有一个特点:一旦被使用之后,是不会被 rollback 的,因此当各类 insert 操作被回滚之后,自增列的值就被“浪费了”。实际上这个 rollback 指代的不仅仅是回滚,而是指那些拿到了自增列值,但是没有实际 insert 数据的场景

因此除了 rollback 事务的场景以外,需要特别关注的就是 REPLACE 和 INSERT…ON DUPLICATE KEY UPDATE,因为这两个操作会获取自增列的值,但是经常不会触发 insert,而是 update。

BUG

关于这个 BUG,其实也不好说是 BUG 还是“特性”。现象是 MySQL 在 5.7 和 5.7 之前,自增列的值是保存在内存中的,这就导致了 MySQL 重启之后会丢失这个自增列的值,所以每次重启之后,MySQL 会把表的自增列的值重置为自增列的 MAX VALUE。实践一下看看效果:

mysql> truncate table t1;
Query OK, 0 rows affected (0.03 sec)

mysql> insert into t1 values(1,1),(3,3),(5,5),(7,7);
Query OK, 4 rows affected (0.01 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> show create table test.t1;
+-------+----------------------------------------------------------------------------+
| Table | Create Table                                                               |
+-------+----------------------------------------------------------------------------+
| t1    | CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 |
+-------+---------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> delete from t1 where id = 7;
Query OK, 1 row affected (0.01 sec)

mysql> show create table test.t1;
+-------+--------------------------------------------------------------------------+
| Table | Create Table                                                             |
+-------+--------------------------------------------------------------------------+
| t1    | CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 |
+-------+--------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> exit
Bye
root@VM-56-136-debian:~# service mysql restart
root@VM-56-136-debian:~# mysqllocal
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or g.
Your MySQL connection id is 2
Server version: 5.7.31-log MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or 'h' for help. Type 'c' to clear the current input statement.

mysql> show create table test.t1;
+-------+---------------------------------------------------------------------------+
| Table | Create Table                                                              |
+-------+---------------------------------------------------------------------------+
| t1    | CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 |
+-------+---------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql>

可以看到原生的 MySQL 在重启之后自增列的值被重置了。这个现象被称之为 BUG 主要是在现在的互联网业务中,支撑业务数据的不仅仅只有 MySQL,还可能会有 Redis,RabbitMQ 等缓存和消息队列或者是单独的 MySQL 日志归档库,自增列可能会被用来作为关联各个存储之间的“逻辑外键”,当 MySQL 重启之后,新写入的数据可能会用到已经被删除的值,导致数据库中的数据和外部系统之间的数据出现错误的关联。另外一种问题场景就是 MySQL 自身各个表之间有外键关系,但是没有建立外键约束,也会遇到类似的问题。

这个 BUG 在 MySQL 8.0 修复了,在 8.0 中,MySQL 把这个值进行了持久化,重启之后也不会被重置。当然,腾讯云数据库 MySQL 也修复了这个问题,可以放心使用~

总结一下

自增列虽然已经长时间,大范围的使用过了,但是也要注意这类技术可能会存在一些隐藏的问题,防微杜渐,保障业务安全运行是 DBA 应尽的职责。