每天导入100万数据导致数据库死锁?

每天导入100万数据导致数据库死锁?

作者:Java后端开发8年,从CRUD到高并发,踩过不少坑。


一、业务背景:为什么要每天导入100万数据?

在我负责的一个营销大数据平台中,运营团队每天都需要将外部渠道的用户行为数据批量导入到系统中,供后续分析和营销使用。

  • 每天导入量:约100万条记录
  • 数据来源:CSV、JSON、第三方接口同步
  • 导入方式:后台任务 + 多线程并发入库(Spring Batch + Executor)
  • 数据库:MySQL 8.x,InnoDB引擎

二、问题出现:数据库频繁死锁

导入程序上线初期运行良好,但很快我们发现:

  • 导入任务经常失败
  • 报错信息频繁出现 Deadlock found when trying to get lock; try restarting transaction
  • 数据导入不完整,影响业务分析

三、初步分析:死锁的可能原因

死锁通俗讲就是"两个线程互相等着对方释放资源,谁也不退让"。

在我们这种高并发批量导入场景中,主要死锁原因可能有:

  1. 多线程并发写入相同表,同一索引字段内容冲突
  2. 更新(或插入)顺序不一致导致锁资源竞争
  3. 使用了唯一索引冲突导致行锁升级为间隙锁、意向锁
  4. 插入重复数据时使用了 INSERT ... ON DUPLICATE KEY UPDATE

四、死锁复现与日志分析

我们打开 MySQL 日志(或使用 SHOW ENGINE INNODB STATUS)看到如下片段:

sql 复制代码
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
UPDATE user_behavior SET ... WHERE channel_id = 'A' AND user_id = 123
*** (2) TRANSACTION:
UPDATE user_behavior SET ... WHERE channel_id = 'B' AND user_id = 123

结论:

  • 多线程并发更新同一个 user_id
  • 行锁顺序不一致(一个先锁A,一个先锁B)
  • 导致互相等待,最终死锁

五、优化目标与原则

我们要做到:

  • 避免死锁:即使高并发导入也不能失败
  • 提升导入性能:保持整体入库速度
  • 保障数据准确性:不能重复、不能缺失

六、优化策略汇总

优化点 说明
✅ 拆分批处理 每批 1000 条,避免大事务
✅ 单线程处理同一主键 用哈希分区将相同 user_id 的数据路由到同一线程
✅ 严格控制锁顺序 多线程中按主键排序后再执行
✅ 避免使用 ON DUPLICATE KEY UPDATE 改为 SELECT 后判断执行 INSERT 或 UPDATE
✅ 使用临时表 先导入临时表,再合并到主表中(最推荐)

七、推荐方案:临时表 + 分批导入 + 合并更新

我们采用如下方式:

  1. 将所有外部数据先导入临时表(无唯一索引)
  2. 再由后台任务单线程合并更新到主表
  3. 合并时采用批量 UPDATE + INSERT IGNORE

八、核心代码实现(Spring Boot + MyBatis + MySQL)

1. 创建临时表结构(与主表结构一致)

sql 复制代码
CREATE TABLE temp_user_behavior (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT,
    channel_id VARCHAR(20),
    action_type VARCHAR(50),
    action_time DATETIME
) ENGINE=InnoDB;

2. 批量导入临时表(避免唯一索引冲突)

typescript 复制代码
public void batchInsertTemp(List<UserBehavior> dataList) {
    // 使用 MyBatis 批量插入
    userBehaviorMapper.batchInsertTemp(dataList);
}
ini 复制代码
<insert id="batchInsertTemp">
    INSERT INTO temp_user_behavior (user_id, channel_id, action_type, action_time)
    VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.userId}, #{item.channelId}, #{item.actionType}, #{item.actionTime})
    </foreach>
</insert>

3. 合并主表数据(避免死锁)

scss 复制代码
@Transactional
public void mergeToMainTable() {
    // 通过 MERGE / INSERT IGNORE / ON DUPLICATE KEY UPDATE 实现
    userBehaviorMapper.mergeData();
    
    // 清空临时表
    userBehaviorMapper.clearTemp();
}
sql 复制代码
<update id="mergeData">
    INSERT INTO user_behavior (user_id, channel_id, action_type, action_time)
    SELECT user_id, channel_id, action_type, action_time
    FROM temp_user_behavior
    ON DUPLICATE KEY UPDATE
        action_type = VALUES(action_type),
        action_time = VALUES(action_time)
</update>

<delete id="clearTemp">
    DELETE FROM temp_user_behavior
</delete>

九、结果验证:死锁消失,性能提升

优化后效果:

  • 死锁:0 次
  • 导入成功率:100%
  • 平均耗时:从 15 分钟降到 2 分钟
  • 系统稳定性明显提升

十、总结

在高并发批量导入数据时,数据库死锁是一个非常常见但容易被忽视的问题 。我们作为后端开发者,不能只会写 SQL,还需要理解数据库的锁机制并发控制事务边界

✅ 实战经验总结:

  • 死锁不一定是 bug,但一定是设计问题
  • 多线程写数据库时要控制主键访问顺序
  • 临时表 + 合并更新是最安全的方式
  • 避免使用"万能 SQL" (如 ON DUPLICATE KEY UPDATE)滥用
相关推荐
AntBlack1 小时前
虽迟但到 :盘一盘 SpringAI 现在发展得怎么样了?
后端·spring·openai
聪明的笨猪猪1 小时前
Java JVM “调优” 面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
重整旗鼓~2 小时前
28.redisson源码分析分布式锁
java·开发语言
Query*2 小时前
Java 设计模式——工厂模式:从原理到实战的系统指南
java·python·设计模式
懒羊羊不懒@2 小时前
Java基础语法—最小单位、及注释
java·c语言·开发语言·数据结构·学习·算法
ss2732 小时前
手写Spring第4弹: Spring框架进化论:15年技术变迁:从XML配置到响应式编程的演进之路
xml·java·开发语言·后端·spring
DokiDoki之父2 小时前
MyBatis—增删查改操作
java·spring boot·mybatis
兩尛3 小时前
Spring面试
java·spring·面试
舒一笑3 小时前
🚀 PandaCoder 2.0.0 - ES DSL Monitor & SQL Monitor 震撼发布!
后端·ai编程·intellij idea
Java中文社群3 小时前
服务器被攻击!原因竟然是他?真没想到...
java·后端