前言
在 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_columns\G *************************** 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 应尽的职责。