每天导入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)滥用
相关推荐
加瓦点灯8 分钟前
面试官: 如何设计一个评论系统?
后端
Ziegler Han13 分钟前
Java的Gradle项目,使用SLF4J+Log4j2+log4j2.xml
java·log4j·slf4j
郡杰20 分钟前
JavaWeb(4-Filter、Listener 和 Ajax)
后端
white camel24 分钟前
重学SpringMVC一SpringMVC概述、快速开发程序、请求与响应、Restful请求风格介绍
java·后端·spring·restful
程序员奈斯27 分钟前
Java后端开发核心笔记:分层架构、注解与面向对象精髓
java
一叶飘零_sweeeet33 分钟前
从基础到进阶:MyBatis-Plus 分页查询封神指南
java·mybatisplus
蓝倾33 分钟前
小红书获取关键词列表API接口详解
前端·后端·fastapi
明天有专业课38 分钟前
想让客户端出口IP变成服务器IP?WireGuard这样配置就行
后端
Smilejudy39 分钟前
在 RDB 上跑 SQL--SPL 轻量级多源混算实践 1
后端
蒟蒻的工具人1 小时前
MySQL学习——面试版
学习·mysql·面试