当前位置: 首页 > news >正文

批量更新主键排序防死锁 标准化规范(错误案例+正确案例+原理复盘)

批量更新主键排序防死锁 标准化规范(错误案例+正确案例+原理复盘)

一、核心概述

数据库批量更新、批量删除场景下,90%偶发性死锁、无规律锁超时故障,均源于「多线程事务加锁顺序不一致」。该类问题本地单线程测试100%正常,仅线上高并发场景触发,属于典型的“魔鬼细节隐形故障”。

根治核心方案:所有批量更新/批量删除,执行业务SQL前,必须按照数据表主键ID统一升序排序,强制全局锁获取顺序一致,彻底杜绝循环等待死锁。

本规范适用于:MySQL InnoDB、达梦DM8所有业务项目,统一编码标准。

二、故障底层原理(深度复盘)

2.1 锁竞争核心逻辑

InnoDB 数据库行锁特性:事务执行批量更新时,会按照待更新集合的遍历顺序逐条申请排他行锁。
成功获取一行锁就执行该行数据修改;如果某一行锁获取失败,事务会发生阻塞,已经获取到的行锁会继续保持持有,不会释放。
行锁生命周期绑定事务,所有行锁统一在事务 commit/rollback 时才释放,事务未提交前,已持有的行锁会持续阻塞其他并发事务。

行锁:逐条申请,事务结束统一释放;中间阻塞,已得锁不释放。

2.2 死锁产生的必要条件(缺一不可)

  • 多线程并发执行批量更新

  • 不同线程的待更新数据列表主键顺序不一致

  • 线程互相持有对方需要的行锁,形成循环等待

2.3 场景模拟(真实线上死锁流程)

假设存在两条数据主键:ID=140424、ID=140425,两个业务线程并发批量更新这两条数据

  • 线程A加锁顺序:140424 → 140425

  • 线程B加锁顺序:140425 → 140424

死锁执行链路

  1. 线程A 成功锁定 140424,等待锁定 140425

  2. 线程B 成功锁定 140425,等待锁定 140424

  3. 两个线程互相持有对方所需锁资源,无限等待

  4. 数据库死锁检测机制触发,强制回滚其中一个事务,业务抛出死锁异常

死锁

### 2.4 场景深度分析

场景分析

线程A上锁顺序:140424 → 140425
线程B上锁顺序:140425 → 140424

这个场景,在【应用层for循环逐条执行update】的前提下,真的会产生死锁,线上非常常见。

时序拆解(for循环逐条DML,不是单条IN)

  1. 线程A事务:拿到140424行锁;
  2. 线程B事务:拿到140425行锁;
  3. 线程A想去拿140425锁,被B持有 → A阻塞等待B释放;
  4. 线程B想去拿140424锁,被A持有 → B阻塞等待A释放;

A等B、B等A,形成循环等待,InnoDB检测到死锁,回滚其中一个事务,抛出1213错误
这就是真实线上死锁。

⚠️前提:必须是循环多次独立update语句,每条SQL只更新一行
如果是单条update xxx where id in(140424,140425),不管你in里面id书写顺序怎么写,InnoDB会按照聚簇索引从小到大顺序上锁,两个线程上锁顺序永远是140424→140425不会死锁

为什么线上这个坑特别多

  1. ID集合来源是MQ、前端、RPC返回,ID顺序完全随机
  2. 本地单线程测试,不会出现交错时序,永远复现不了;
  3. 只有高并发多线程,才会出现上面的“交叉拿锁”时序;
  4. 偶发,复现条件苛刻,属于典型“线上才出、本地好好的”隐形bug。

举个极简伪代码,就是这个故障的原型

//线程A List<Long> listA = Arrays.asList(140424L,140425L); for(Long id : listA){ updateById(id); //每条都是独立SQL } //线程B List<Long> listB = Arrays.asList(140425L,140424L); for(Long id : listB){ updateById(id); //每条都是独立SQL }

上面代码高并发跑,就有概率触发死锁。解决办法:循环前把list按主键升序排序。排序后两个线程上锁顺序完全一致,就破坏死锁的必要条件。

容易混淆的关键点总结

执行模式是否会出现截图里的死锁解决手段
Java for循环逐条update(多条独立SQL)✅会,线上高频业务层对ID集合主键升序排序
单条SQLupdate ... id in(?,?)❌不会,内核自动按主键排序上锁业务层排序ID无意义,不需要处理

补充思考

很多网上错误Demo,错误拿in()去演示这个死锁场景,这就是我们上一篇md博文勘误的那个点。
死锁模型本身没问题,但是演示工具选错了,用in()是复现不出来这个交叉锁;必须用循环多条DML。

三、错误代码案例(线上高频坑)

核心问题:直接使用上游传入的无序List批量更新,未做主键排序,完全依赖上游数据顺序,高并发必触发死锁。

/** * 【错误示例】批量更新告警数据(高危!会产生死锁) * 问题点:未排序,上游返回的list顺序随机,多线程并发锁顺序混乱 */ @Transactional(rollbackFor = Exception.class) public void batchUpdateAlarm(List<AlarmInfo> updateList) { // 直接执行批量更新,无任何排序处理 alarmInfoMapper.batchUpdate(updateList); }

错误代码风险说明

  • 前端、MQ、上游接口返回的数据列表顺序完全随机

  • 单线程测试无任何问题,无法复现bug

  • 线上高并发场景下,随机触发死锁、锁超时,日志偶发报错,极难排查

  • MySQL、达梦数据库均存在该问题,无数据库版本豁免

四、正确代码案例(生产级标准写法)

核心优化:批量操作前,强制根据数据表主键ID升序排序,统一全局加锁顺序,彻底消灭循环等待。

/** * 【正确示例】批量更新告警数据(防死锁标准写法) * 优化点:批量更新前强制主键升序排序 */ @Transactional(rollbackFor = Exception.class) public void batchUpdateAlarm(List<AlarmInfo> updateList) { // 核心必写:根据数据库主键ID 升序排序,统一全局锁顺序 updateList.sort(Comparator.comparing(AlarmInfo::getId)); // 排序后执行批量更新,杜绝死锁 alarmInfoMapper.batchUpdate(updateList); }

五、进阶增强版(生产终极方案:排序+去重+分批+降级)

适配大数据量、超高并发场景,除排序防死锁外,规避重复更新、大批量锁堆积、批量全败问题。

/** * 【生产终极方案】批量更新 防死锁+防重复+防锁堆积+异常降级 */ @Transactional(rollbackFor = Exception.class) public void safeBatchUpdateAlarm(List<AlarmInfo> updateList) { // 1. 根据主键去重(防止同事务重复锁同一行,触发约束冲突) List<AlarmInfo> distinctList = updateList.stream() .collect(Collectors.toMap(AlarmInfo::getId, Function.identity(), (k1, k2) -> k1)) .values() .stream() .collect(Collectors.toList()); // 2. 主键升序排序【防死锁核心代码】 distinctList.sort(Comparator.comparing(AlarmInfo::getId)); // 3. 小批量拆分(单次50条,避免一次性持有大量行锁,减少锁超时) List<List<AlarmInfo>> partitionList = ListUtils.partition(distinctList, 50); // 4. 分批执行,异常降级单条重试,避免整批回滚 for (List<AlarmInfo> partList : partitionList) { try { alarmInfoMapper.batchUpdate(partList); } catch (DataIntegrityViolationException | LockTimeoutException e) { // 批量冲突降级单条更新,保证大部分数据执行成功 log.warn("批量更新锁冲突,降级单条执行", e); partList.forEach(info -> { try { alarmInfoMapper.updateById(info); } catch (Exception ex) { log.error("单条告警更新失败,id:{}", info.getId(), ex); } }); } } }

六、关键知识点答疑(开发高频疑问)

6.1 为什么排序就能彻底杜绝死锁?

所有线程、所有批次统一按照主键从小到大加锁,所有人抢锁顺序完全一致,只会出现「后线程等待前线程」,不会出现双向循环等待,从算法层面直接消灭死锁必要条件。

6.2 批量新增需要排序吗?

不需要。批量新增是写入新行、加短暂行锁,不存在互相锁对方数据的场景,无死锁风险,无需排序。

6.3 批量删除需要排序吗?

必须排序。批量删除同样是逐条加行锁,锁机制和批量更新完全一致,无序删除同样会触发死锁,需按主键升序排序。

6.4 非主键唯一字段可以排序吗?

不建议。必须使用物理主键ID排序,唯一索引、业务ID无法替代,保证锁顺序绝对唯一、稳定。

七、强制编码规范(纳入代码评审卡点)

  1. 所有batchUpdate / 批量update / 批量delete代码,必须存在主键升序排序逻辑,无排序一律打回。

  2. 禁止直接使用上游原始List执行批量DML操作,必须经过排序、去重处理。

  3. 高并发业务批量操作,强制使用「排序+去重+小批量拆分+异常降级」终极方案。

  4. 该规范同时适配 MySQL、达梦DM8,全项目统一执行。

八、故障总结

批量更新死锁不属于数据库bug,是典型的编码细节缺失导致的人为故障。问题隐蔽性极强、复现难度极高、线上危害极大,仅需一行排序代码即可100%根治,是所有后端开发必须掌握的标准化细节规范。

九、场景复现

更新死锁问题

http://www.jsqmd.com/news/1400927/

相关文章:

  • ModHeader插件实战:HTTP请求头修改在Web开发调试中的六大核心应用
  • 日志审计系统构建指南:从ELK实战到安全运营中心演进
  • STM32驱动W25Q64JV SPI Flash:从基础SPI到Quad SPI的实战指南
  • 安卓Jellyfin连接失败?解析SSL证书链问题与解决方案
  • 【RAG】知识图谱 RAG 与可验证引用案例讲解
  • 结构体与函数:从数据封装到模块化编程的核心实践
  • day22-全流程01
  • 一台电脑四人开黑:Nucleus Co-op实现PC游戏本地分屏多人游玩的完整上手指南
  • MySQL增删改查实战:从基础语法到性能优化全解析
  • Linux命令高效学习心法:从场景驱动到组合实战
  • Altium Designer高效建库:Mouser Library Loader自动化转换实战指南
  • 分布式AI计算网络:从算力浪费到高效志愿计算的架构优化与伦理实践
  • AI Agent白手起家75: 使用 CrewAI 构建游戏开发助手
  • 前端调试利器:Whistle+SwitchyOmega本地Web代理环境搭建与高阶玩法
  • 思源宋体CN全套7字重免费商用:中文排版专业级提升,一步到位
  • Maven手动处理JAR包实战:离线环境、私有依赖与构建难题解决方案
  • 2026甄选:广东通信工程监理甲级资质代办服务公司实力与高效选择 - 卓企推荐
  • Python csv.reader() 核心原理与实战:从编码乱码到海量数据处理
  • 10 万亿参数大模型,注定要被关进笼子里
  • Git Rebase 完全指南:从原理到实战,打造线性提交历史
  • Node.js实现Markdown标题自动化转换:正则表达式与文件处理实战
  • 能量结构化世界模型与神经时间场:开放世界物理一致运动规划实践
  • 福州函授机构数据深度比对,依托公开数据看百闽教育核心优势 - 讲清楚了
  • 从按键精灵到按键猫咪:全键盘自动化脚本实战指南
  • MHY_Scanner 扫码登录器实战指南:从屏幕监控到直播抢码的自动化方案
  • CRMEB 首届主题设计大赛,奖金20000元
  • 吾爱视频解析下载器
  • 钉钉直播教学全流程26个常见问题解决方案与实战指南
  • 基于MiniCPM5-1B构建本地研究智能体:从模型部署到ReAct框架实战
  • Agent评测体系:量化工具调用准确率与任务轨迹质量,驱动AI智能体从演示走向落地