每天导入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)滥用
相关推荐
怕浪猫11 小时前
RAG 面试 6 连问,从原理到优化全部覆盖
python·算法·面试
daidaidaiyu12 小时前
ThingsBoard 集群的核心逻辑源码分析
java
To_OC12 小时前
从一头雾水到跑通全流程:我用一个周末啃透了JWT登录鉴权
前端·后端·http
我可能是个假开发13 小时前
FTP Unicode 文件名导致 `550` 的问题分析与解决方案
java·开发语言·spring
caoerzhong13 小时前
中小企业上 WMS 该先上哪几块:JeeWMS 开源 Java 仓库管理系统的分批上线清单
java·python·开源
前端兰博13 小时前
05-Redis
redis·后端
databook14 小时前
从手动检查到自动监控:一个数据质量工作流的实现
后端·python·数据分析
【JAVA】玩家14 小时前
Spring核心原理全解析:从零到生产实战
java·后端·spring
东风破_14 小时前
从 Neo4j 到 GraphRAG:用 Text-to-Cypher 构建图检索 RAG
人工智能·后端
Wang's Blog14 小时前
Java框架 SpringCloud 快速入门: 实现 Feign 最佳实践(抽取方式)
java·spring cloud