每天导入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)滥用
相关推荐
indexsunny3 小时前
互联网大厂Java求职面试实战:Spring Boot微服务与Redis缓存场景解析
java·spring boot·redis·缓存·微服务·消息队列·电商
无心水3 小时前
【分布式利器:腾讯TSF】7、TSF高级部署策略全解析:蓝绿/灰度发布落地+Jenkins CI/CD集成(Java微服务实战)
java·人工智能·分布式·ci/cd·微服务·jenkins·腾讯tsf
28岁青春痘老男孩8 小时前
JDK8+SpringBoot2.x 升级 JDK 17 + Spring Boot 3.x
java·spring boot
方璧8 小时前
限流的算法
java·开发语言
元Y亨H8 小时前
Nacos - 服务注册
java·微服务
曲莫终8 小时前
Java VarHandle全面详解:从入门到精通
java·开发语言
独自归家的兔8 小时前
Spring Cloud核心架构组件深度解析(原理+实战+面试高频)
spring cloud·面试·架构
一心赚狗粮的宇叔8 小时前
中级软件开发工程师2025年度总结
java·大数据·oracle·c#
奋进的芋圆9 小时前
DataSyncManager 详解与 Spring Boot 迁移指南
java·spring boot·后端
计算机程序设计小李同学9 小时前
个人数据管理系统
java·vue.js·spring boot·后端·web安全