MySQL 自增列使用上的一些 “坑”

文若2年前技术文章454

前言

MySQL 的规范中,一般都会建议表要有主键,常使用自增列作为主键字段,这和 MySQL 属于聚簇索引表有关,顺序增长的主键比较合适。最近有研发咨询,为什么有张表的自增主键变的非常大?而且偶尔还出现 Duplicate entry '4' for key 'PRIMARY' 的异常,这篇文章将介绍下此类问题。

1. 自增列空洞

1.1 手动指定

自增列的空洞一般指的就是自增列不是连续增长,中间出现一些数值上的断层,这种情况常发生在手动指定自增列的值,请看下面的 case:

-- 创建一张测试表,id 为自增主键
create table t1(
    id bigint auto_increment primary key,
    c1 varchar(10) not null,
    c2 varchar(10) not null
);

插入测试数据:

insert into t1(c1, c2) VALUES ('a', 'b'),('a', 'b'),('a', 'b'),('a', 'b'),('a', 'b');

id

c1

c2

1

a

b

2

a

b

3

a

b

4

a

b

5

a

b

此时再插入手动 ID 插入数据:

insert into t1 value (10, 'a', 'b');

再查询 t1 表的自增值:

select TABLE_NAME, AUTO_INCREMENT from information_schema.TABLES where TABLE_NAME = 't1';

TABLE_NAME

AUTO_INCREMENT

t1

11

查询表中的数据:

id

c1

c2

1

a

b

2

a

b

3

a

b

4

a

b

5

a

b

10

a

b

此时可以看到表中的数据只有 6 行,但是自增值已经变为 6,浪费了中间四个值,如果为主键字段为 INT 类型,浪费过多会导致字段溢出,插入数据时就会报错,所以规范里也提到使用 bigint 类型作为表的主键。

2.2  分配未使用

自增列有一个特点,就是一旦分配后,就无法被 rollback 相当于被 “浪费”,请看下方 case:

-- 创建一张测试表,id 为自增主键
create table t1(
    id bigint auto_increment primary key,
    c1 varchar(10) not null,
    c2 varchar(10) not null
);

开启事务,插入一条数据,然后回滚:

-- 开启事务
begin;
-- 插入一条记录
insert into t1(c1, c2) value('a', 'b');
-- 回滚
rollback;
-- 再次插入数据
insert into t1(c1, c2) value('a', 'b');

id

c1

c2

2

a

b

可以看到自增列是从 2 开始分配的,相当于第一个事务拿到了自增值,但是没有实际使用。

除了 Rollback 会产生分配未使用的情况外,还有 REPLACEINSERT…ON DUPLICATE KEY UPDATE 在特定情况下也会出现分配未使用的情况:

-- 创建测试表,注意 c1 字段有唯一索引
create table t1
(
    id bigint auto_increment,
    c1 varchar(10) not null,
    c2 varchar(10) not null,
    PRIMARY KEY (`id`),
    UNIQUE KEY uqk_c1(c1)
);

插入测试数据:

insert into t1(c1, c2) VALUES ('a', 'b'),('b', 'b');

使用 REPLACE 语句:

-- 如果有 c1 = a 将 c2 修改为 w
replace into t1(c1, c2) value ('a', 'w');
-- 上面已经修改了值,下面执行后数据没有实际变化
replace into t1(c1, c2) value ('a', 'w');
replace into t1(c1, c2) value ('a', 'w');
replace into t1(c1, c2) value ('a', 'w');
replace into t1(c1, c2) value ('a', 'w');
replace into t1(c1, c2) value ('a', 'w');

此时表中只有两条记录:

id

c1

c2

2

b

b

8

a

w

再看该表的 AUTO_INCREMENT 值,已经增长到 9 相当于产生了空洞:

select TABLE_NAME, AUTO_INCREMENT from information_schema.TABLES where TABLE_NAME = 't1';

TABLE_NAME

AUTO_INCREMENT

t1

9

除此之外,还有 DUPLICATE KEY UPDATE 语法也会有此类情况。这两个操作会获取自增列的值,但是经常不会触发 insert,而是 update。

insert into t1(c1, c2) value ('a', 'b') on duplicate key update c2 = 'vvvv';

2. 自增列监控

2.1 sys 库监控

为了防止自增列的值 “用光”,导致业务报错。可以使用下方 SQL 监控自增列:

select * from sys.schema_auto_increment_columns;

2.2 通用查询

如果你的数据库不支持使用这条 SQL(版本问题,或者有些云厂商没有开放 sys 库)可以使用下方 SQL:

SELECT table_schema,
       table_name,
       column_name,
       c.COLUMN_TYPE,
       AUTO_INCREMENT,
       POW(2, CASE data_type
                  WHEN 'tinyint' THEN 7
                  WHEN 'smallint' THEN 15
                  WHEN 'mediumint' THEN 23
                  WHEN 'int' THEN 31
                  WHEN 'bigint' THEN 63
                  END + (column_type LIKE '% unsigned')) - 1 AS max_int
FROM information_schema.tables t
         JOIN information_schema.columns c USING (table_schema, table_name)
WHERE c.extra = 'auto_increment'
  and c.COLUMN_KEY = 'PRI'
  AND t.TABLE_SCHEMA NOT IN ('information_schema', 'mysql', 'sys', 'performance_schema')
  AND t.auto_increment IS NOT NULL;

3. 一些 BUG

3.1 重启失效

MySQL 5.7 版本 auto_increment  是存储在内存中的,这就导致每次重启 MySQL 都会重新计算该值,计算逻辑是取该字段的 MAX VALUE 请看下方 case:

-- 创建一张测试表,id 为自增主键
create table t1(
    id bigint auto_increment primary key,
    c1 varchar(10) not null,
    c2 varchar(10) not null
);

插入 8 行记录:

insert into t1(c1, c2) value ('a', 'b'),('a', 'b'),('a', 'b'),('a', 'b'),('a', 'b'),('a', 'b'),('a', 'b'),('a', 'b');

此时 AUTO_INCREMENT 的值为 9,下一条写入会被分配自增 ID 为 9:

select TABLE_NAME, AUTO_INCREMENT from information_schema.TABLES where TABLE_NAME = 't1';

TABLE_NAME

AUTO_INCREMENT

t1

9

然后删除后 4 条记录:

delete from t1 where id > 4;

重启数据库:

>$ service mysqld restart
Shutting down MySQL.... SUCCESS! 
Starting MySQL... SUCCESS!

查询 t1 表的自增 ID:

select TABLE_NAME, AUTO_INCREMENT from information_schema.TABLES where TABLE_NAME = 't1';

TABLE_NAME

AUTO_INCREMENT

t1

5

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

该 BUG 已在 MySQL 8.0 版本修复。

3.2 冲突问题

当自增列被修改过时,可能会出现 Duplicate entry '4' for key 'PRIMARY' 的异常,详细看下方 case:

-- 创建一张测试表,id 为自增主键
create table t1(
    id bigint auto_increment primary key,
    c1 varchar(10) not null,
    c2 varchar(10) not null
);

插入测试数据:

insert into t1(c1, c2) VALUES ('a', 'b'),('a', 'b'),('a', 'b'),('a', 'b'),('a', 'b');

修改自增列的值:

update t1 set id = 6 where id = 1;

-- 表中数据:
+----+----+----+
| id | c1 | c2 |
+----+----+----+
|  2 | a  | b  |
|  3 | a  | b  |
|  4 | a  | b  |
|  5 | a  | b  |
|  6 | a  | b  |
+----+----+----+

再尝试获取自增值插入:

insert into t1(c1, c2) VALUES ('a', 'b');

ERROR 1062 (23000): Duplicate entry '6' for key 'PRIMARY'

这个 case 报错的原因是 UPDATE 修改了自增列的值,但是 UPDATE 不会触发自增值的分配,却占用了一个未来要被分配的自增值,当该值需要被分配出来的时候,由于被 UPDATE 修改的行占用了,导致 SQL 报错,所以主键(自增列)尽量不要去修改。

相关文章

ip_foward未开启导致slb健康检查异常

ip_foward未开启导致slb健康检查异常

现象:在slb上配置一条7层http监听,并开启健康检查,发现健康检查一直异常。处理步骤:针对slb健康检查异常的问题,可从以下四个方面入手,排查原因。1、确认服务器上业务程序正常启动,且在后端服务器...

MySQL创建用户授权

创建用户授权一、创建用户1.创建管理用户create user 'test'@'%' identified by 'Test123@'create user 'test'@'localhost' id...

Storage Classes之nfs provisioner

Storage Classes之nfs provisioner

每个 StorageClass 都有一个制备器(Provisioner),用来决定使用哪个卷插件制备 PV。 该字段必须指定。这里我们使用nfs作为StorageClass的制备器,官方并未对nfs进...

grafana常见问题处理

grafana常见问题处理

发现了一个不错的grafana模版,下载安装后很多问题,需要处理后才能使用。第一个问题就是显示插件不存在。插件未找到问题处理安装插件并重启既然未找到插件,那我们肯定要安装下插件,使用grafana-c...

Containerd OCI runtime create failed

Containerd OCI runtime create failed

1、背景Kubernetes 集群新增 worker 节点(centos7、containerd),发现容器一直启动不起来。2、报错信息经查看系统 message 日志发现如下报错信息:rpc err...

MySQL性能优化(六)优化or条件

MySQL性能优化(六)优化or条件

优化器是数据库中非常核心,又非常复杂的一个组件。有的SQL,优化器选择的执行计划并不是最优的,通过改写SQL,可以帮助优化器找到最优的执行计划。where条件中的or子句,是比较容易出问题的一个场景。...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。