每天导入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
- 数据导入不完整,影响业务分析
三、初步分析:死锁的可能原因
死锁通俗讲就是"两个线程互相等着对方释放资源,谁也不退让"。
在我们这种高并发批量导入场景中,主要死锁原因可能有:
- 多线程并发写入相同表,同一索引字段内容冲突
- 更新(或插入)顺序不一致导致锁资源竞争
- 使用了唯一索引冲突导致行锁升级为间隙锁、意向锁
- 插入重复数据时使用了
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 |
✅ 使用临时表 | 先导入临时表,再合并到主表中(最推荐) |
七、推荐方案:临时表 + 分批导入 + 合并更新
我们采用如下方式:
- 将所有外部数据先导入临时表(无唯一索引)
- 再由后台任务单线程合并更新到主表
- 合并时采用批量 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
)滥用