MySQL INSERT 导致的死锁分析
前言
本文选用的 MySQL 版本:8.4.6
使用的数据
表结构:
DROP TABLE IF EXISTS store_snapshot_ext;
DROP TABLE IF EXISTS store_snapshot;
create table store_snapshot
(
id varchar(32) not null comment '主键'
primary key,
warehouse_id varchar(32) null comment '仓库主键',
snap_date datetime null comment '快照日期',
create_id varchar(32) null comment '创建人id',
create_time datetime null comment '创建日期',
modify_id varchar(32) null comment '更新人 id',
modify_time datetime null comment '更新时间'
)
comment '仓库快照';
create table store_snapshot_ext
(
id varchar(32) not null comment '主键'
primary key,
fk_snapshot_id varchar(32) null comment '快照外键',
ext_attr varchar(32) null comment '扩展属性',
create_id varchar(32) null comment '创建人id',
create_time datetime null comment '创建日期',
modify_id varchar(32) null comment '更新人 id',
modify_time datetime null comment '更新时间',
constraint store_snapshot_ext___fk_snapshot_id
foreign key (fk_snapshot_id) references store_snapshot (id)
)
comment '仓库快照扩展属性';
这里使用 Java 语言模拟并发情况下对数据的插入:
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.RandomUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.tea.common.entity.StoreSnapshot;
import org.tea.common.entity.StoreSnapshotExt;
import org.tea.common.mapper.StoreSnapshotExtMapper;
import org.tea.common.mapper.StoreSnapshotMapper;
import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = StoreSnapshotApplication.class)
public class StoreSnapBenchTest {
@Resource
private PlatformTransactionManager txManager;
@Resource
private StoreSnapshotMapper storeSnapshotMapper;
@Resource
private StoreSnapshotExtMapper storeSnapshotExtMapper;
@Test
public void batchTest() throws InterruptedException {
Thread[] ts = new Thread[10];
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(ts.length);
for (int i = 0; i < ts.length; i++) {
ts[i] = new Thread(() -> {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
// 设置事务隔离级别为 "可重复读"
definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
TransactionStatus status = txManager.getTransaction(definition);
try {
/**
提高事务竞争的激烈度
*/
startLatch.await();
/**
一般情况下,不会使用如下的循环方式来插入数据,这里这么做的目的是为了
提高事务的处理时间,增大锁的竞争激烈度
*/
for (int j = 0; j < 2000; j++) {
StoreSnapshot snapshot = new StoreSnapshot();
// 使用 UUID 的方式来引发 Page 分裂
snapshot.setId(UUID.randomUUID().toString().replaceAll("-", ""));
snapshot.setWarehouseId("warehouse_1");
snapshot.setSnapDate(new DateTime());
snapshot.init();
storeSnapshotMapper.insertSelective(snapshot);
StoreSnapshotExt snapshotExt = new StoreSnapshotExt();
snapshotExt.setId(UUID.randomUUID().toString().replaceAll("-", ""));
snapshotExt.setExtAttr(RandomUtil.randomString(32));
// 注意这里的外键,后文会分析这个外键带来的一些影响
snapshotExt.setFkSnapshotId(snapshot.getId());
storeSnapshotExtMapper.insertSelective(snapshotExt);
}
txManager.commit(status);
} catch (InterruptedException e) {
txManager.rollback(status);
throw new RuntimeException(e);
} finally {
endLatch.countDown();
}
});
}
for (Thread t : ts) {
t.start();
}
startLatch.countDown();
endLatch.await();
}
}
@SpringBootApplication
@EnableTransactionManagement
@MapperScan("org.tea.*.mapper")
class StoreSnapshotApplication {
public static void main(String[] args) {
SpringApplication.run(StoreSnapshotApplication.class, args);
}
}
在执行完上面的测试用例后,查看 MySQL InnoDB 的状态信息,发现已经出现了死锁:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2025-08-26 21:01:55 135637563962944
*** (1) TRANSACTION:
TRANSACTION 3866, ACTIVE 2 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 27 lock struct(s), heap size 8312, 220 row lock(s), undo log entries 430
MySQL thread id 17, OS thread handle 135637553456704, query id 8736 localhost ::1 root update
INSERT INTO store_snapshot (create_id, warehouse_id, snap_date, modify_id, id, modify_time, create_time) VALUES ('system', 'warehouse_1', '2025-08-26 21:01:54', 'system', '402ce43f650a483eb0c9c5138e50d6f0', '2025-08-26 21:01:54', '2025-08-26 21:01:54')
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 10 page no 11 n bits 240 index PRIMARY of table `lxh_db`.`store_snapshot` trx id 3866 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10 page no 20 n bits 160 index PRIMARY of table `lxh_db`.`store_snapshot` trx id 3866 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
0: len 30; hex 343033303963393162373166343731633936323164616565643434666363; asc 40309c91b71f471c9621daeed44fcc; (total 32 bytes);
1: len 6; hex 000000000f14; asc ;;
2: len 7; hex 82000001070630; asc 0;;
3: len 11; hex 77617265686f7573655f31; asc warehouse_1;;
4: len 5; hex 99b7755074; asc uPt;;
5: len 6; hex 73797374656d; asc system;;
6: len 5; hex 99b7755074; asc uPt;;
7: len 6; hex 73797374656d; asc system;;
8: len 5; hex 99b7755074; asc uPt;;
*** (2) TRANSACTION:
TRANSACTION 3860, ACTIVE 3 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 46 lock struct(s), heap size 24696, 1258 row lock(s), undo log entries 2474
MySQL thread id 11, OS thread handle 135637890971200, query id 12330 localhost ::1 root update
INSERT INTO store_snapshot (create_id, warehouse_id, snap_date, modify_id, id, modify_time, create_time) VALUES ('system', 'warehouse_1', '2025-08-26 21:01:55', 'system', '917f3578682c467384e520fd6c00b86d', '2025-08-26 21:01:55', '2025-08-26 21:01:55')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 10 page no 20 n bits 160 index PRIMARY of table `lxh_db`.`store_snapshot` trx id 3860 lock_mode X locks gap before rec
Record lock, heap no 3 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
0: len 30; hex 336661623363323037333232346465666263363762333630363763623666; asc 3fab3c2073224defbc67b36067cb6f; (total 32 bytes);
1: len 6; hex 000000000f14; asc ;;
2: len 7; hex 82000000932d80; asc - ;;
3: len 11; hex 77617265686f7573655f31; asc warehouse_1;;
4: len 5; hex 99b7755076; asc uPv;;
5: len 6; hex 73797374656d; asc system;;
6: len 5; hex 99b7755076; asc uPv;;
7: len 6; hex 73797374656d; asc system;;
8: len 5; hex 99b7755076; asc uPv;;
Record lock, heap no 4 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
0: len 30; hex 336664326234626538643463343338326130303764383431366364613536; asc 3fd2b4be8d4c4382a007d8416cda56; (total 32 bytes);
1: len 6; hex 000000000f14; asc ;;
2: len 7; hex 82000000903800; asc 8 ;;
3: len 11; hex 77617265686f7573655f31; asc warehouse_1;;
4: len 5; hex 99b7755076; asc uPv;;
5: len 6; hex 73797374656d; asc system;;
6: len 5; hex 99b7755076; asc uPv;;
7: len 6; hex 73797374656d; asc system;;
8: len 5; hex 99b7755076; asc uPv;;
Record lock, heap no 5 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
0: len 30; hex 343033303963393162373166343731633936323164616565643434666363; asc 40309c91b71f471c9621daeed44fcc; (total 32 bytes);
1: len 6; hex 000000000f14; asc ;;
2: len 7; hex 82000001070630; asc 0;;
3: len 11; hex 77617265686f7573655f31; asc warehouse_1;;
4: len 5; hex 99b7755074; asc uPt;;
5: len 6; hex 73797374656d; asc system;;
6: len 5; hex 99b7755074; asc uPt;;
7: len 6; hex 73797374656d; asc system;;
8: len 5; hex 99b7755074; asc uPt;;
Record lock, heap no 93 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
0: len 30; hex 336664396165326435663738346237656161346134323336643031643366; asc 3fd9ae2d5f784b7eaa4a4236d01d3f; (total 32 bytes);
1: len 6; hex 000000000f14; asc ;;
2: len 7; hex 82000000a81784; asc ;;
3: len 11; hex 77617265686f7573655f31; asc warehouse_1;;
4: len 5; hex 99b7755077; asc uPw;;
5: len 6; hex 73797374656d; asc system;;
6: len 5; hex 99b7755077; asc uPw;;
7: len 6; hex 73797374656d; asc system;;
8: len 5; hex 99b7755077; asc uPw;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 10 page no 11 n bits 240 index PRIMARY of table `lxh_db`.`store_snapshot` trx id 3860 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (1)
------------
TRANSACTIONS
------------
可以看到,在日志中,事务 3866 持有一个页号为 11,supremum (相当于双链表的尾部哑节点) 的间隙锁,在等待页号为 20,主键为 40309c91b71f471c9621daeed44fcc 的间隙锁;同时,事务 3860 持有页号为 20,主键为 40309c91b71f471c9621daeed44fcc 的间隙锁,同时在等待页号为 10 的 supremum 的间隙锁。这两个事务构成了循环回路,并且在相互等待,因此形成了死锁,具体的图示如下所示:

具体分析
查询语句的加锁
MySQL的的一般SELECT语句在非串行化隔离级别下是通过一致性读的方式进行读取,本身不会对记录加锁,但是在存在外键约束的情况下,依旧会对关联的外键约束记录上加上S型的记录锁,如果关联的外键约束没有被找到,在"可重复读"的隔离级别下,会在外键记录附近加上间隙锁由于这里插入语句的外键都能被找到,因此这里的外键不是产生间隙锁的原因
INSERT语句的加锁单纯的
INSERT语句在插入时加上的是一种特殊的记录锁,不同事务的插入意向锁不会相互阻塞,但是在插入的记录行的所处位置存在间隙锁的情况下,会为当前的INSERT记录加上 插入意向锁在主键重复的情况下,隔离级别为 "可重复读" 或 "串行化" 的情况下,会为插入的记录加上
S型的Next-Key锁。在这种情况下,如果原来待插入行的事务回滚了,由于本身持有的S型锁无法再获取到独占锁,就有可能会引发死锁[2]。由于这里的主键都是
UUID,不存在重复主键,并且结合相关的日志信息,并不是由于单纯的INSERT语句导致的死锁Page 的分裂
当插入的记录的主键不连续时,
MySQL为了维护聚簇索引的顺序,可能会引发页的分裂。在事务隔离级别为 "可重复读"或"串行化" 的情况下,对分裂的数据进行迁移的过程中,相当于对数据执行了更新的操作,按照MySQL对于UPDATE语句的加锁情况[2],会在记录上加上 Next-Key (记录锁和间隙锁),因此事务3860会持有记录40309c91b71f471c9621daeed44fcc的间隙锁;而为了防止在分裂维护过程中重新插入数据,可能不得不为相关页记录的supremum加上间隙锁,以维护页分裂的执行过程这个过程可能如下:
插入记录前:

插入记录导致页分裂后

注意: 这里关于分页而产生的间隙锁为实际实验推断,并无实际文档与之关联。当事务隔离级别为 "读提交" 或插入的记录的主键存在顺序时,都不会出现上文描述的死锁出现
解决方案
实际上,如果事务执行速度特别快,并且在并发量不高的情况下,这种类型的死锁很难被检测到,因为需要处理的事务跨多个页,并且需要关联到两个不同页的锁本身就很难。因此,将逐行的 INSERT 语句替换为批量提交后也可以很大程度上解决这一问题
为了尽可能地避免这一类问题,推荐的一些方案如下:
- 如果没有特殊必要,可以使用隔离级别较低的事务隔离级别,因为这样可以减少实际事务处理过程中锁的数量,降低锁冲突的可能性
- 尽量使用有序的主键,不管是从性能上还是实际业务角度,都没有理由选择
UUID的理由 - 如果可以,适当减少事务的粒度,如:将一个大事务分成几个小事务,在性能和一致性上做一定的权衡
参考:
[1] https://mp.weixin.qq.com/s/9LRFYGquXWpMCeyAonNcMQ
[2] https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html
MySQL INSERT 导致的死锁分析的更多相关文章
- Transaction And Lock--唯一索引下INSERT导致的死锁
背景: 曾经的一位同事问我:"数据库只有并发INSERT 操作,会造成死锁么?",我没有太多思考地回答"不会",但真的不会吗? 测试: --========== ...
- Mysql死锁如何排查:insert on duplicate死锁一次排查分析过程
前言 遇到Mysql死锁问题,我们应该怎么排查分析呢?之前线上出现一个insert on duplicate死锁问题,本文将基于这个死锁问题,分享排查分析过程,希望对大家有帮助. 死锁案发还原 表结构 ...
- Mysql查询语句使用select.. for update导致的数据库死锁分析
近期有一个业务需求,多台机器需要同时从Mysql一个表里查询数据并做后续业务逻辑,为了防止多台机器同时拿到一样的数据,每台机器需要在获取时锁住获取数据的数据段,保证多台机器不拿到相同的数据. 我们My ...
- 线上BUG:MySQL死锁分析实战
原文链接:线上BUG:MySQL死锁分析实战 1 线上告警 我们不需要关注截图中得其他信息,只要能看到打印得org.springframework.dao.DeadlockLoserDataAcces ...
- <转>一个最不可思议的MySQL死锁分析
1 死锁问题背景 1 1.1 一个不可思议的死锁 1 1.1.1 初步分析 3 1.2 如何阅读死锁日志 3 2 死锁原因深入剖析 4 2.1 Delete操作的加锁逻辑 4 2.2 死锁预防策略 5 ...
- MySQL死锁分析
死锁问题背景 做MySQL代码的深入分析也有些年头了,再加上自己10年左右的数据库内核研发经验,自认为对于MySQL/InnoDB的加锁实现了如指掌,正因如此,前段时间,还专门写了一篇洋洋洒洒的文章, ...
- Mysql数据库死锁分析相关概念
参考博客: mysql死锁问题分析(https://www.cnblogs.com/LBSer/p/5183300.html) mysql insert锁机制(http://yeshaoting.cn ...
- 一次线上mysql死锁分析
一.现象 发运车次调用发车接口时发生异常,后台抛出数据库死锁日志. 二.原因分析 通过日志可以看出事务T1等待 heap no 8的行锁 (X locks 排他锁) 事务T2持有heap no 8的行 ...
- 一个最不可思议的MySQL死锁分析
1 死锁问题背景 1 1.1 一个不可思议的死锁 1 1.1.1 初步分析 3 1.2 如何阅读死锁日志 3 2 死锁原因深入剖析 4 2. ...
- MySQL 5.6.35 索引优化导致的死锁案例解析
一.背景 随着公司业务的发展,商品库存从商品中心独立出来成为一个独立的系统,承接主站商品库存校验.订单库存扣减.售后库存释放等业务.在上线之前我们对于核心接口进行了压测,压测过程中出现了 MySQL ...
随机推荐
- luffy
Luffy 本项目使用Vue+Vite+Django+DRF+Elementui-Plus编写 项目需求介绍 # 线上销售课程的 -商城 -知识付费类 # 需求 -首页功能 -轮播图接口 -推荐课程接 ...
- MySQL核心知识学习之路(3)
作为一个后端工程师,想必没有人没用过数据库,跟我一起复习一下MySQL吧,本文是我学习<MySQL实战45讲>的总结笔记的第三篇,总结了MySQL的索引相关知识. 上一篇:MySQL核心知 ...
- java Set HashSet详解
Set集合 就像把对象随意扔进罐子里,无法记住元素的添加顺序.Set某种程度就是Collection,方法没有不同,只是行为稍微不同,(不允许重复元素),如果一定要往里加两个相同元素,添加失败add( ...
- Spring AI 玩转多轮对话
AI "失忆"怎么办?本文带你用 Spring AI 一招搞定多轮对话,让你的 AI 应用拥有超强记忆!从 ChatClient.Advisors 到实战编码,三步打造一个能记住上 ...
- TeX,LaTeX和KaTeX
TeX 计算机排版系统 七十年代末,Donald E. Knuth在看自己的<电脑编程的艺术>校样时,感觉计算机排版的校样质量低到无法忍受,于是决定自己开发一个高质量的计算机排版系统,这样 ...
- MVCC 快照读, readView,
简介 参考链接 以作记录 https://www.jianshu.com/p/8845ddca3b23
- PPM 文件结构介绍
PPM 文件结构介绍 ppm 文件结构很简单分为以下几个 使用记事本打开项目即可. P3 256 256 // 表示 256 * 256 个像素 255 // RGB 每一个色彩范围是 0 - 255 ...
- linux 上的格式放入windows 上报错
简介 vs 上一直说使用unicode编码格式 解决方案 在makefile中添加这两项 format_incode : find . -regex '.*\.\(cpp\|hpp\|cc\|cxx\ ...
- AD 提权-NTLM 中继攻击(强制认证)
爱之则不觉其过,恶之则不知其善. 导航 0 前言 1 委派 2 ADCS 证书申请 3 影子凭证 0.前言 NTLM 中继攻击的流程主要分为三个步骤:触发认证.中继攻击.后利用.其中, 触发认证又分为 ...
- ETLCloud怎么样?深度解析其在数据管理中的表现
在BI或数据大屏等数据分析工具中,经常需要从多个业务系统中提取原始数据,然后对数据进行清洗.处理,以获取高质量.有效且干净的数据以供后续的BI进行数据统计和分析使用,从高质量的实现企业数据的价值变现. ...