记一个MySQL插入死锁问题

写在文章开头

在进行MySQL数据备份迁移时,很多人为了避免网络IO的开销而选用insert into ...... select 进行数据迁移备份,但你是否知道这种做法会存在那些隐患呢? 所以本文就以一个简单的功能示例为大家演示一下insert into ...... select 可能引发的问题和解决方案:

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。

问题复现

这里为了演示问题,笔者生成了一张带有500w数据的数据表,对应DDL语句如下:

sql 复制代码
CREATE TABLE `batch_insert_test` (
  `id` int NOT NULL AUTO_INCREMENT,
  `fileid_1` varchar(100) DEFAULT NULL,
  `fileid_2` varchar(100) DEFAULT NULL,
  `fileid_3` varchar(100) DEFAULT NULL,
  `fileid_4` varchar(100) DEFAULT NULL,
  `fileid_5` varchar(100) DEFAULT NULL,
  `fileid_6` varchar(100) DEFAULT NULL,
  `fileid_7` varchar(100) DEFAULT NULL,
  `fileid_8` varchar(100) DEFAULT NULL,
  `fileid_9` varchar(100) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2520001 DEFAULT CHARSET=utf8mb3 COMMENT='测试批量插入,一行数据1k左右';

使用count语句查看数据量:

sql 复制代码
select count(*)  from batch_insert_test;

稍微久等了一小会,输出语句如下,可以看到一张表数据刚刚好达到500w:

bash 复制代码
count(*)|
--------+
 5000000|

同样的我们给出备份迁移表的DDL,表结构是一样的,唯一区别就是表名后缀多了个bak

sql 复制代码
CREATE TABLE `batch_insert_test_bak` (
  `id` int NOT NULL AUTO_INCREMENT,
  `fileid_1` varchar(100) DEFAULT NULL,
  `fileid_2` varchar(100) DEFAULT NULL,
  `fileid_3` varchar(100) DEFAULT NULL,
  `fileid_4` varchar(100) DEFAULT NULL,
  `fileid_5` varchar(100) DEFAULT NULL,
  `fileid_6` varchar(100) DEFAULT NULL,
  `fileid_7` varchar(100) DEFAULT NULL,
  `fileid_8` varchar(100) DEFAULT NULL,
  `fileid_9` varchar(100) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2520001 DEFAULT CHARSET=utf8mb3 COMMENT='测试批量插入,一行数据1k左右';

此时我们使用数据库连接工具在会话窗口,执行如下迁移语句:

bash 复制代码
insert into   batch_insert_test_bak   select * from batch_insert_test;

然后我们再写一段程序模拟插入:

java 复制代码
 @Test
    public void insert() {
        while (true) {
            BatchInsertTest batchInsertTest = new BatchInsertTest();
            batchInsertTest.setFileid1(RandomUtil.randomString(1));
            batchInsertTest.setFileid2(RandomUtil.randomString(1));
            batchInsertTest.setFileid3(RandomUtil.randomString(1));
            batchInsertTest.setFileid4(RandomUtil.randomString(1));
            batchInsertTest.setFileid5(RandomUtil.randomString(1));
            batchInsertTest.setFileid6(RandomUtil.randomString(1));
            batchInsertTest.setFileid7(RandomUtil.randomString(1));
            batchInsertTest.setFileid8(RandomUtil.randomString(1));
            batchInsertTest.setFileid9(RandomUtil.randomString(1));
            batchInsertTest.setCreateDate(new Date());

            long begin = System.currentTimeMillis();
            batchInsertTestMapper.insert(batchInsertTest);
            long end = System.currentTimeMillis();

            log.info("插入耗时:{} ms", end - begin);

            ThreadUtil.sleep(10000L);
        }
    }

从输出结果来看,一开始插入并不是很耗时,基本都是毫秒级完成,但是随着时间的推移,插入的耗时逐渐增加,最慢的一次数据插入竟然花费了1分多钟:

bash 复制代码
18.546 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:148 ms
28.778 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:221 ms
38.926 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:143 ms
49.588 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:652 ms
59.763 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:166 ms
09.820 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:56 ms
19.930 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:99 ms
30.027 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:86 ms
40.145 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:113 ms
50.238 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:79 ms
17.178 INFO  c.s.w.WebTemplateApplicationTests:117  main                    插入耗时:76927 ms

原因剖析

我们不妨使用如下语句查看一下执行计划:

sql 复制代码
explain insert into   batch_insert_test_bak   select * from batch_insert_test;

可以看出无论是insert还是select都是走全表扫描,这意味着select子句的执行过程会针对整张表从上到下的扫描进行一个逐步锁(S锁)的过程,随着时间的推移它最终就会变为全表锁。

insert语句也因为对于插入数量未知而上全表锁,进而长期持有auto-inc锁,当然因为insert的表是用于迁移备份数据的,auto-inc锁的长时间持有对于业务来说影响不大。

bash 复制代码
id|select_type|table                |partitions|type|possible_keys|key|key_len|ref|rows   |filtered|Extra|
--+-----------+---------------------+----------+----+-------------+---+-------+---+-------+--------+-----+
 1|INSERT     |batch_insert_test_bak|          |ALL |             |   |       |   |       |        |     |
 1|SIMPLE     |batch_insert_test    |          |ALL |             |   |       |   |4692967|   100.0|     |

select则不同,因为全表扫描的逐步S锁,导致一段时间后,整张表都被锁住,使得我们的新的会话的插入语句的事务无法提交。进而导致大量连接数阻塞积压,各种超时问题也就随之诞生,严重一点就很可能导致整个业务线瘫痪。

解决方案

所以如果我们希望迁移时不锁住全表,可以在指定在每次迁移时指定一个范围,所以我们针对时间字段增加索引:

sql 复制代码
ALTER TABLE db1.batch_insert_test DROP INDEX batch_insert_test_create_date_IDX;
CREATE INDEX batch_insert_test_create_date_IDX USING BTREE ON db1.batch_insert_test (create_date);

然后迁移的sql改为:

sql 复制代码
insert into   batch_insert_test_bak   select * from batch_insert_test where create_date <now() ;

查看执行计划发现,select走了range索引,避免全表扫描,解决了上述的风险:

sql 复制代码
id|select_type|table                |partitions|type |possible_keys                    |key                              |key_len|ref|rows|filtered|Extra                |
--+-----------+---------------------+----------+-----+---------------------------------+---------------------------------+-------+---+----+--------+---------------------+
 1|INSERT     |batch_insert_test_bak|          |ALL  |                                 |                                 |       |   |    |        |                     |
 1|SIMPLE     |batch_insert_test    |          |range|batch_insert_test_create_date_IDX|batch_insert_test_create_date_IDX|6      |   |   1|   100.0|Using index condition|

小结

由此可以得出,再使用数据insert into ...... select进行数据迁移时,无比考虑读写锁的工作机制,以及迁移可能导致的锁的粒度和范围,只有精确的评估风险点才能保证功能上限不影响正常业务的工作。

我是sharkchiliCSDN Java 领域博客专家开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程JVMMySQL数据库个人专栏导航。

参考

同事埋了个坑:Insert into select 语句把生产服务器炸了!:zhuanlan.zhihu.com/p/139486473

一条 SQL 引发的事故,同事直接被开除:blog.csdn.net/youanyyou/a...

insert into ... select 由于SELECT表引起的死锁情况分析:blog.csdn.net/asdfsadfasd...

MySQL锁详解 :blog.csdn.net/shark_chili...

复制代码
相关推荐
许野平30 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod2 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。3 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man4 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*4 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu4 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s4 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子4 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
想进大厂的小王4 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构