前言
在 MySQL 的常见规范里面,每个表都要设置主键,一般来说都会推荐自增列作为主键,这和 MySQL 属于聚簇索引表有关,顺序增长的主键比较合适。而自增列中比较常遇见的问题就是自增列的空洞。原生的 MySQL 自增列也存在一个 BUG,可能会影响到数据一致性,本文也会详细介绍,在自建 MySQL 的时候尽量不要踩到这个坑。
空洞问题
问题介绍
自增列的空洞一般指的就是自增列不是连续增长,中间出现一些数值上的断层。比如手动写入自增列的值,就有可能会出现这种现象:
代码语言:txt复制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,“浪费”了两个值。当浪费的值过多的时候,就可能会出现如下的问题:
代码语言:txt复制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 视图中直接查询视图
代码语言:txt复制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。实践一下看看效果:
代码语言:txt复制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 应尽的职责。