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

文若2年前技术文章703

前言

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 报错,所以主键(自增列)尽量不要去修改。

相关文章

image.png

KVM网络配置

一、创建桥接网络1、创建桥接网卡br0(相当于vmnet1桥接器)cd /etc/sysconfig/network-scripts/cp ifcfg-ens33 ifcfg-br02、修改br0参数...

Kubernetes openelb

1、背景在云服务环境中的 Kubernetes 集群里,通常可以用云服务提供商提供的负载均衡服务来暴露 Service,但是在本地没办法这样操作。而 OpenELB 可以让用户在裸金属服务器、边缘以及...

Ansible部署和使用(sshpass)

Ansible部署和使用(sshpass)

简介Ansible默认通过 SSH 协议管理机器。安装Ansible之后,不需要启动或运行一个后台进程,或是添加一个数据库。只要在一台电脑(可以是一台笔记本)上安装好,就可以通过这台电脑管理一组远程的...

hive元数据操作

1.查看hive从超过5000分区的表select dbs.name, tbls.TBL_NAME, count(1) as part_count from dbs, tbls, partitions...

Flinksql Kafka 接收流数据并打印到控制台

Flinksql Kafka 接收流数据并打印到控制台

本文目的使用Flink SQL创建一个流处理作业,将来自Kafka主题"dahua_picrecord"的数据写入到另一个表”print_table”控制台中。使用sql-client前 需要启动ya...

使用Velero备份与恢复K8s集群及应用

使用Velero备份与恢复K8s集群及应用

环境3台虚拟机组成一主两从的测试集群,使用NFS作为动态存储主机IP系统k8s-master192.168.1.10centos7.9k8s-node1192.168.1.11centos7.9k8s...

发表评论    

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