当前位置: 首页 > 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 锁机制与业务场景的碰撞。通过调整隔离级别、缩短事务、添加重试等组合拳,可有效解决此类问题。关键是要理解数据库底层锁机制,结合业务场景做针对性优化。

相关文章:

  • 【51单片机中断】
  • 二维数组以及C99中的变长数组(如何在VS2022中使用苹果的clang编译器)
  • 基于MCP的桥梁设计规范智能解析与校审系统构建实践
  • 翼兴消防监控 – 大数据可视化HTML源码
  • python可视化:北方省市人口流动与春运数据综合分析5
  • 校园社区小程序源码解析
  • 记参加一次数学建模
  • 云基内容中台构建企业智慧实践
  • Java与C/C++跨平台互操作深度解析:Project Panama技术实战
  • ssh 配置了.ssh/authorized_keys 依旧需要密码的问题
  • 【Linux】序列化与反序列化、会话与进程组、守护进程
  • Fabric 服务端插件开发简述与聊天事件监听转发
  • 【C++ 基础数论】质数判断
  • AI大模型中系统化的KV Cache加速方案,减少KV Cache显存占用的优化方法
  • AI推介-大语言模型LLMs论文速览(arXiv方向):2024.11.25-2024.11.30
  • 【打破信息差】萌新认识与入门算法竞赛
  • QBasic 一款古老的编程语言在现代学习中的价值(附程序)
  • 刷leetcodehot100返航版--双指针5/16
  • 西安前端面试
  • 机器学习中的特征工程:解锁模型性能的关键
  • 《歌手》回归,人均技术流,00后整顿职场
  • 终于,俄罗斯和乌克兰谈上了
  • 蒋圣龙突遭伤病出战世预赛存疑,国足生死战后防线严重减员
  • 为什么越来越多景区,把C位留给了书店?
  • 金砖国家召开经贸联络组司局级特别会议,呼吁共同抵制单边主义和贸易保护主义
  • 前四个月社会融资规模增量累计为16.34万亿元,比上年同期多3.61万亿元