MySQL自增ID跳跃问题
记一次MySQL自增ID发生跳跃的问题
在对大规模数据进行分页查询测试性能时,我使用了MySQL的蠕虫复制快速生成了百万级别的数据,但是发现了一个很奇怪的问题,先回顾一下我的操作:
CREATE TABLE `emp` (
`empno` mediumint(8) unsigned NOT NULL DEFAULT '0',
`ename` varchar(32) NOT NULL DEFAULT '',
`job` varchar(32) NOT NULL DEFAULT '',
`mgr` mediumint(8) unsigned DEFAULT NULL,
`hiredate` date NOT NULL,
`sal` double(6,0) NOT NULL,
`comm` decimal(7,2) DEFAULT NULL,
`deptno` int(11) unsigned NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `emp` VALUES (7369, 'SMITH', 'CLERK', 7902, '1990-12-17', 800, NULL, 20);
INSERT INTO `emp` VALUES (7499, 'ALLEN', 'SALESMAN', 7698, '1991-02-20', 1600, 300.00, 30);
INSERT INTO `emp` VALUES (7521, 'WARD', 'SALESMAN', 7698, '1991-02-22', 1250, 500.00, 30);
INSERT INTO `emp` VALUES (7566, 'JONES', 'MANAGER', 7839, '1991-04-02', 2975, NULL, 20);
INSERT INTO `emp`VALUES (7654, 'MARTIN', 'SALESMAN', 7698, '1991-09-28', 1250, 1400.00, 30);
INSERT INTO `emp` VALUES (7698, 'BLAKE', 'MANAGER', 7839, '1991-05-01', 2850, NULL, 30);
INSERT INTO `emp` VALUES (7782, 'CLARK', 'MANAGER', 7839, '1991-06-09', 2450, NULL, 10);
INSERT INTO `emp` VALUES (7788, 'SCOTT', 'ANALYST', 7566, '1997-04-19', 3000, NULL, 20);
INSERT INTO `emp` VALUES (7839, 'KING', 'PRESIDENT', NULL, '1991-11-17', 5000, NULL, 10);
INSERT INTO `emp` VALUES (7844, 'TURNER', 'SALESMAN', 7698, '1991-09-08', 1500, NULL, 30);
INSERT INTO `emp` VALUES (7900, 'JAMES', 'CLERK', 7698, '1991-12-03', 950, NULL, 30);
INSERT INTO `emp` VALUES (7902, 'FORD', 'ANALYST', 7566, '1991-12-03', 3000, NULL, 20);
INSERT INTO `emp` VALUES (7934, 'MILLER', 'CLERK', 7782, '1992-01-23', 1300, NULL, 10);
emp
表中有13条记录,此时再创建一个新表test
作为测试表,并从emp
表中查询数据插入到测试表中:
CREATE TABLE test
( `id` BIGINT(64) PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(32),
sal DOUBLE, job VARCHAR(32),
deptno INT(10))ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO test
(`name`, sal, job,deptno)
SELECT ename, sal, job, deptno FROM emp;
此时测试表test
中也有了13条记录
接下来进行蠕虫复制,反复执行如下sql,test
表的数据行数就会以指数级成倍增加:
INSERT INTO test (`name`, sal, job,deptno) SELECT `name`, sal, job,deptno FROM test;
但是在测试时却发现以这种方式批量插入的数据发生了id跳跃,可以看到第二次插入数据时直接从16开始了:
我以INSERT INTO ... SELECT ...
的方式插入数据时并没有指定插入id值,而是希望它以MySQL的自增ID方式自动生成
为什么会出现ID跳跃的情况呢?
首先要了解下MySQL是如何保证自增主键的单调递增属性的,这与MySQL的自增锁模式有关——innodb_autoinc_lock_mode
查看这个全局配置:
-- 查看自增锁模式配置
show variables like 'innodb_autoinc_lock_mode';
Mysql 8.0开始默认是 2,之前的版本默认是1
该配置有0/1/2三种可选值
参考文章:深入剖析 MySQL 自增锁 - detectiveHLH - 博客园 (cnblogs.com)
innodb_autoinc_lock_mode=0
传统锁模式【traditional 】
当我们向包含了
AUTO_INCREMENT
列的表中插入数据时,都会持有这么一个特殊的表锁——自增锁(AUTO-INC),并且当语句执行完之后就会释放。这样一来可以保证单个语句内生成的自增值是连续的。但是这样一来,传统模式的弊端就自然暴露出来了,如果有多个事务并发的执行
INSERT
操作,AUTO-INC
的存在会使得 MySQL 的性能略有下降,因为同时只能执行一条INSERT
语句。innodb_autoinc_lock_mode=1
连续模式【Consecutive】
在锁模式处于连续模式下时,如果
INSERT
语句能够提前确定插入的数据量,则可以不用获取自增锁,举个例子,像INSERT INTO
这种简单的、能提前确认数量的新增语句,就不会使用自增锁,这个很好理解,在自增值上,我可以直接把这个INSERT
语句所需要的空间流出来,就可以继续执行下一个语句了。通过持有所需要数量的自增值的互斥锁(轻量锁)来避免使用表锁,这个锁仅在分配过程中持有,不会持续到语句结束。但是如果
INSERT
语句不能提前确认数据量,则还是会去获取自增锁。例如像INSERT INTO ... SELECT ...
这种语句,INSERT
的值来源于另一个SELECT
语句,会使用表锁直到语句结束,同时只有一个语句持有表锁。innodb_autoinc_lock_mode=2
交叉模式【Interleaved】
交叉模式(Interleaved)下,所有的
INSERT
语句,包含INSERT
和INSERT INTO ... SELECT
,都不会使用AUTO-INC
自增锁,而是使用较为轻量的mutex
锁。这样一来,多条INSERT
语句可以并发的执行,这也是三种锁模式中扩展性最好的一种。并发执行所带来的副作用就是单个
INSERT
的自增值并不连续,因为AUTO_INCREMENT
的值分配会在多个INSERT
语句中来回交叉的执行。优点很明确,缺点是在并发的情况下无法保证数据一致性,
关于MySQL的锁机制,可以看下这篇博客(引用):MySQL中的锁机制 - 周二鸭 - 博客园
如何理解交叉模式的缺陷,先了解下MySQL的binlog
机制,Binlog
一般用于MySQL的数据复制/主从同步。
在 MySQL 中 Binlog 的格式有 3 种,分别是:
- Statement 基于语句,只记录对数据做了修改的SQL语句,能够有效的减少binlog的数据量,提高读取、基于binlog重放的性能
- Row 只记录被修改的行,所以Row记录的binlog日志量一般来说会比Statement格式要多。基于Row的binlog日志非常完整、清晰,记录了所有数据的变动,但是缺点是可能会非常多,例如一条
update
语句,有可能是所有的数据都有修改;再例如alter table
之类的,修改了某个字段,同样的每条记录都有改动。 - Mixed Statement和Row的结合,例如像
alter table
之类的对表结构的修改,采用Statement格式。其余的对数据的修改例如update
和delete
采用Row格式进行记录。
如果 MySQL 采用的格式为 Statement
,那么 MySQL 的主从同步实际上同步的就是一条一条的 SQL 语句。如果此时我们采用了交叉模式,那么并发情况下 INSERT
语句的执行顺序就无法得到保障。
INSERT
同时交叉执行,并且 AUTO_INCREMENT
交叉分配将会直接导致主从之间同行的数据主键 ID 不同。而这对主从同步来说是灾难性的。
再回过头来看进行蠕虫复制的时候为什么会发生ID跳跃呢?
我的MySQL版本是5.7,默认是连续模式,每次会预申请多余的id,申请个数与当前的数据记录行数N有关,每次都会申请2N个id,当下一次insert时会把从多余的id中取最大值作为自增的起始点,我画了一个图来表示:
当前数据行数 | 申请id | 插入操作记录序列 |
---|---|---|
N=1 | 2 | 1,2,3 (申请的id是2和3,3是多余的id) |
N=2 | 4 | 3,4,5,6 (5,6是多余的id) |
N=4 | 8 | 6,7,8,9,10,11,12,13 (10-13是多余的id) |
N=8 | 16 | 13,14,... ,19,20,21,... ,27,28 (21-28是多余的id) |
这就能解释为什么蠕虫复制如果初始值为1的情况下,id的序列会是:【1,2,3,4,6,7,8,9,13,……】
如何修改锁模式呢?
在MySQL的配置文件(Windows是my.ini,Linux是my.cnf)中进行配置即可:innodb_autoinc_lock_mode=1
,需要重启MySQL服务