每天导入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)滥用
相关推荐
Configure-Handler29 分钟前
buildroot System configuration
java·服务器·数据库
测试涛叔30 分钟前
金三银四软件测试面试题(800道)
软件测试·面试·职场和发展
:Concerto1 小时前
JavaSE 注解
java·开发语言·sprint
电商API_180079052472 小时前
第三方淘宝商品详情 API 全维度调用指南:从技术对接到生产落地
java·大数据·前端·数据库·人工智能·网络爬虫
一点程序2 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
C雨后彩虹2 小时前
计算疫情扩散时间
java·数据结构·算法·华为·面试
2601_949809592 小时前
flutter_for_openharmony家庭相册app实战+我的Tab实现
java·javascript·flutter
vx_BS813302 小时前
【直接可用源码免费送】计算机毕业设计精选项目03574基于Python的网上商城管理系统设计与实现:Java/PHP/Python/C#小程序、单片机、成品+文档源码支持定制
java·python·课程设计
2601_949868362 小时前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
蒹葭玉树3 小时前
【C++上岸】C++常见面试题目--操作系统篇(第二十八期)
linux·c++·面试