当前位置: 首页 > news >正文

实战记录:Java 高并发插入 MySQL 唯一索引表引发死锁的排查与解决

一、业务背景:数据产品的 "定时任务之痛"

我们团队负责一款 ToB 数据产品,核心逻辑是将实时计算平台(如 ODPS)的分析结果同步到 MySQL,供前端 BI 工具快速查询。其中有一张核心表tkn_tb_cinema_show_data(影院排片数据表),表结构如下(关键部分):

sql

CREATE TABLE tkn_tb_cinema_show_data (id BIGINT PRIMARY KEY AUTO_INCREMENT,cinema_id BIGINT COMMENT '影院ID',show_id BIGINT COMMENT '影片ID',now_date VARCHAR(32) COMMENT '统计日期',UNIQUE KEY uid_cinema_show_date (cinema_id, show_id, now_date) -- 联合唯一索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

问题爆发:定时任务的 "死亡循环"

每天凌晨的定时同步任务是数据链路的核心,但最近运维同学反馈:当任务并发量提升至 200 + 线程时,插入成功率从 99% 骤降至 60%,日志中大量出现:

plaintext

com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: 
Deadlock found when trying to get lock; try restarting transaction

DBA 拉取SHOW ENGINE INNODB STATUS死锁日志,关键信息如下:

plaintext

*** (1) TRANSACTION:
TRANSACTION 73278630816, ACTIVE 1 sec inserting
LOCK WAIT 622 lock struct(s), heap size 79400, 743 row lock(s)
LOCKED RECORD indices: uid_cinema_show_date
*** (2) TRANSACTION:
TRANSACTION 73278630826, ACTIVE 0 sec inserting
WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 42 page no 5 n bits 72 index uid_cinema_show_date of table `tkn`.`tkn_tb_cinema_show_data` 
trx id 73278630826 lock_mode X locks gap before rec insert intention waiting

现象矛盾点:明明插入的是不同now_date(统计日期)的记录,为何会出现锁冲突?

二、问题复现:用 Java 代码模拟死锁场景

为定位问题,我们用 Java+Spring Boot 模拟线上环境:

java

// 模拟定时任务的并发插入服务
@Service
public class CinemaDataService {@Autowiredprivate JdbcTemplate jdbcTemplate;// 原始插入逻辑(问题代码)@Transactional(isolation = Isolation.REPEATABLE_READ) // 默认可重复读隔离级别public void batchInsert(List<CinemaShowData> dataList) {String sql = "INSERT INTO tkn_tb_cinema_show_data " +"(cinema_id, show_id, now_date) VALUES (?,?,?)";dataList.forEach(data -> jdbcTemplate.update(sql, data.getCinemaId(), data.getShowId(), data.getNowDate()));}
}// 压测代码(模拟200线程并发)
@Test
void testDeadLock() throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(200);// 构造2000条不同now_date的记录(cinema_id=1001, show_id=2001)List<CinemaShowData> dataList = IntStream.range(0, 2000).mapToObj(i -> new CinemaShowData(1001L, 2001L, "2025-05-15-" + i)).collect(Collectors.toList());// 拆分200线程并发插入for (int i = 0; i < 200; i++) {executor.submit(() -> batchInsert(dataList.subList(i*10, (i+1)*10)));}executor.shutdown();executor.awaitTermination(10, TimeUnit.MINUTES);
}

压测结果:10 分钟内抛出 127 次死锁异常,与线上现象完全一致。

三、原理深挖:InnoDB 锁机制的 "隐形冲突"

通过分析 InnoDB 锁机制(参考《MySQL 技术内幕:InnoDB 存储引擎》),问题根源在于唯一索引的插入意向锁(Insert Intention Lock)与 Next-Key Lock 的冲突

1. 关键锁机制回顾

  • 插入意向锁(X IK):插入新记录前,InnoDB 会在目标间隙(Gap)加插入意向锁,表示 "我要插入这里",与其他插入意向锁兼容(不同间隙)。
  • Next-Key Lock(S NK):唯一索引插入时,InnoDB 会先执行唯一性检查(防止重复),此时会加 S 型 Next-Key Lock(记录锁 + 间隙锁),锁范围为(上一记录, 当前记录]

2. 死锁场景推演

假设唯一索引(cinema_id=1001, show_id=2001)已有记录now_date='2025-05-15-0',两个并发事务 T1、T2 分别插入now_date='2025-05-15-1'now_date='2025-05-15-2'

  1. T1 执行唯一性检查,加 S 型 Next-Key Lock 锁间隙('2025-05-15-0', +∞)
  2. T2 同时执行唯一性检查,同样尝试加 S 型 Next-Key Lock 锁同一间隙;
  3. InnoDB 中 S 型 Next-Key Lock 在间隙锁场景下互斥(防止幻读),T1 和 T2 互相阻塞;
  4. 此时 T1 和 T2 都等待对方释放锁以获取插入意向锁,形成循环等待,触发死锁。

四、Java 层面的终极解决方案

针对上述原理,我们从数据库配置、Java 代码、业务逻辑三端协同优化,最终将死锁率降至 0.01% 以下。

方案 1:调整事务隔离级别(核心优化)

将事务隔离级别从默认的REPEATABLE READ改为READ COMMITTED(读已提交),Java 中通过@Transactional注解配置:

java

// 优化后:降低隔离级别减少锁范围
@Transactional(isolation = Isolation.READ_COMMITTED) 
public void batchInsert(List<CinemaShowData> dataList) {// 插入逻辑不变
}

原理READ COMMITTED下,InnoDB 仅保留记录锁(释放间隙锁),且唯一性检查仅加记录锁(非 Next-Key Lock),大幅减少锁冲突。

方案 2:拆分批量插入(缩短事务生命周期)

将单次插入 2000 条改为每 100 条提交一次,避免长事务长时间持锁:

java

// 优化后:拆分批量插入
public void batchInsert(List<CinemaShowData> dataList) {int batchSize = 100;for (int i = 0; i < dataList.size(); i += batchSize) {List<CinemaShowData> batch = dataList.subList(i, Math.min(i + batchSize, dataList.size()));doInsert(batch); // 单独事务提交}
}@Transactional(isolation = Isolation.READ_COMMITTED)
private void doInsert(List<CinemaShowData> batch) {String sql = "INSERT INTO tkn_tb_cinema_show_data (cinema_id, show_id, now_date) VALUES (?,?,?)";jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {@Overridepublic void setValues(PreparedStatement ps, int i) throws SQLException {CinemaShowData data = batch.get(i);ps.setLong(1, data.getCinemaId());ps.setLong(2, data.getShowId());ps.setString(3, data.getNowDate());}@Overridepublic int getBatchSize() {return batch.size();}});
}

方案 3:添加死锁重试逻辑(兜底保障)

通过 Spring Retry 实现死锁自动重试(需引入spring-retry依赖):

java

// 依赖配置(pom.xml)
<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId>
</dependency>// 启用重试注解
@Service
@EnableRetry
public class CinemaDataService {@Retryable(value = MySQLTransactionRollbackException.class, maxAttempts = 3, backoff = @Backoff(delay = 100))@Transactional(isolation = Isolation.READ_COMMITTED)public void batchInsert(List<CinemaShowData> dataList) {// 插入逻辑}@Recoverpublic void recover(MySQLTransactionRollbackException e, List<CinemaShowData> dataList) {log.error("最终重试失败,数据:{}", dataList, e);// 可选:将失败数据写入补偿队列}
}

方案 4:按索引顺序排序插入(减少锁竞争)

插入前按唯一索引(cinema_id, show_id, now_date)排序,避免随机插入导致多事务竞争同一间隙:

java

// 插入前排序(Java 8 Stream)
List<CinemaShowData> sortedData = dataList.stream().sorted(Comparator.comparingLong(CinemaShowData::getCinemaId).thenComparingLong(CinemaShowData::getShowId).thenComparing(CinemaShowData::getNowDate)).collect(Collectors.toList());

五、效果验证:从 "死亡循环" 到 "丝滑插入"

优化后压测(200 线程并发插入 2000 条数据):

  • 死锁次数:0 次(原 127 次 / 10 分钟);
  • 插入耗时:从 82s 降至 15s(因事务提交次数增加,需平衡批量大小);
  • 业务成功率:从 60% 提升至 99.99%。

总结

高并发插入唯一索引表的死锁问题,本质是 InnoDB 锁机制与业务场景的碰撞。通过调整隔离级别、缩短事务、添加重试等组合拳,可有效解决此类问题。关键是要理解数据库底层锁机制,结合业务场景做针对性优化。

http://www.xdnf.cn/news/490933.html

相关文章:

  • Windows 本地部署MinerU详细教程
  • 厂房气楼做法
  • [Lc] 5.16 One question a day周总结
  • 项目管理进阶:全文解读企业IT系统全生命周期管理与运营平台建设方案【附全文阅读】
  • 【RTMP】RTMP协议的详细介绍
  • SpringBoot3.4.5下Lombok的@Data注解不生效,解决方案
  • Gmsh 读取自定义轮廓并划分网格:深入解析与实践指南
  • 金格iWebOffice控件在新版谷歌Chrome中不能加载了怎么办?
  • 数字电路的发展历史
  • PT2031单触控单输出触摸IC
  • MySQL8.x新特性:窗口函数(Window Functions)
  • 【Linux】ssh命令 – 安全的远程连接服务
  • GPU Runtime Suspend 调试与验证:从 sysfs 到 perf 分析
  • ROS2学习(5)------ROS2 功能包介绍
  • 【python】直线型雷达图
  • 前端流行框架Vue3教程:18. _组件数据传递
  • 关于“集合框架底层原理”的一些问题
  • Ceisum 展示——智能巡检制作
  • Vue3封装公共图片组件
  • 深入探索 OpenSPG:下一代知识图谱构建与推理框架
  • Java(基础) day01 初识Java
  • 职教实训室中的写实数字人:技术与应用方案
  • 遇到Linux系统网络连接丢包的问题如何解决?
  • 54. 螺旋矩阵
  • redis缓存实战
  • 地球系统模式(CESM)实践技术应用
  • Ubuntu系统安装docker仓库教程
  • C#学习教程(附电子书资料)
  • Excel MCP: 自动读取、提炼、分析Excel数据并生成可视化图表和分析报告
  • day 25