尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

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

批量更新主键排序防死锁 标准化规范(错误案例+正确案例+原理复盘) 批量更新主键排序防死锁 标准化规范错误案例正确案例原理复盘一、核心概述数据库批量更新、批量删除场景下90%偶发性死锁、无规律锁超时故障均源于「多线程事务加锁顺序不一致」。该类问题本地单线程测试100%正常仅线上高并发场景触发属于典型的“魔鬼细节隐形故障”。根治核心方案所有批量更新/批量删除执行业务SQL前必须按照数据表主键ID统一升序排序强制全局锁获取顺序一致彻底杜绝循环等待死锁。本规范适用于MySQL InnoDB、达梦DM8所有业务项目统一编码标准。二、故障底层原理深度复盘2.1 锁竞争核心逻辑InnoDB 数据库行锁特性事务执行批量更新时会按照待更新集合的遍历顺序逐条申请排他行锁。成功获取一行锁就执行该行数据修改如果某一行锁获取失败事务会发生阻塞已经获取到的行锁会继续保持持有不会释放。行锁生命周期绑定事务所有行锁统一在事务 commit/rollback 时才释放事务未提交前已持有的行锁会持续阻塞其他并发事务。行锁逐条申请事务结束统一释放中间阻塞已得锁不释放。2.2 死锁产生的必要条件缺一不可多线程并发执行批量更新不同线程的待更新数据列表主键顺序不一致线程互相持有对方需要的行锁形成循环等待2.3 场景模拟真实线上死锁流程假设存在两条数据主键ID140424、ID140425两个业务线程并发批量更新这两条数据线程A加锁顺序140424 → 140425线程B加锁顺序140425 → 140424死锁执行链路线程A 成功锁定 140424等待锁定 140425线程B 成功锁定 140425等待锁定 140424两个线程互相持有对方所需锁资源无限等待数据库死锁检测机制触发强制回滚其中一个事务业务抛出死锁异常死锁### 2.4 场景深度分析场景分析线程A上锁顺序140424 → 140425线程B上锁顺序140425 → 140424这个场景在【应用层for循环逐条执行update】的前提下真的会产生死锁线上非常常见。时序拆解for循环逐条DML不是单条IN线程A事务拿到140424行锁线程B事务拿到140425行锁线程A想去拿140425锁被B持有 → A阻塞等待B释放线程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不会死锁。为什么线上这个坑特别多ID集合来源是MQ、前端、RPC返回ID顺序完全随机本地单线程测试不会出现交错时序永远复现不了只有高并发多线程才会出现上面的“交叉拿锁”时序偶发复现条件苛刻属于典型“线上才出、本地好好的”隐形bug。举个极简伪代码就是这个故障的原型//线程A ListLong listA Arrays.asList(140424L,140425L); for(Long id : listA){ updateById(id); //每条都是独立SQL } //线程B ListLong 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(ListAlarmInfo updateList) { // 直接执行批量更新无任何排序处理 alarmInfoMapper.batchUpdate(updateList); }错误代码风险说明前端、MQ、上游接口返回的数据列表顺序完全随机单线程测试无任何问题无法复现bug线上高并发场景下随机触发死锁、锁超时日志偶发报错极难排查MySQL、达梦数据库均存在该问题无数据库版本豁免四、正确代码案例生产级标准写法核心优化批量操作前强制根据数据表主键ID升序排序统一全局加锁顺序彻底消灭循环等待。/** * 【正确示例】批量更新告警数据防死锁标准写法 * 优化点批量更新前强制主键升序排序 */ Transactional(rollbackFor Exception.class) public void batchUpdateAlarm(ListAlarmInfo updateList) { // 核心必写根据数据库主键ID 升序排序统一全局锁顺序 updateList.sort(Comparator.comparing(AlarmInfo::getId)); // 排序后执行批量更新杜绝死锁 alarmInfoMapper.batchUpdate(updateList); }五、进阶增强版生产终极方案排序去重分批降级适配大数据量、超高并发场景除排序防死锁外规避重复更新、大批量锁堆积、批量全败问题。/** * 【生产终极方案】批量更新 防死锁防重复防锁堆积异常降级 */ Transactional(rollbackFor Exception.class) public void safeBatchUpdateAlarm(ListAlarmInfo updateList) { // 1. 根据主键去重防止同事务重复锁同一行触发约束冲突 ListAlarmInfo 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条避免一次性持有大量行锁减少锁超时 ListListAlarmInfo partitionList ListUtils.partition(distinctList, 50); // 4. 分批执行异常降级单条重试避免整批回滚 for (ListAlarmInfo 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无法替代保证锁顺序绝对唯一、稳定。七、强制编码规范纳入代码评审卡点所有batchUpdate / 批量update / 批量delete代码必须存在主键升序排序逻辑无排序一律打回。禁止直接使用上游原始List执行批量DML操作必须经过排序、去重处理。高并发业务批量操作强制使用「排序去重小批量拆分异常降级」终极方案。该规范同时适配 MySQL、达梦DM8全项目统一执行。八、故障总结批量更新死锁不属于数据库bug是典型的编码细节缺失导致的人为故障。问题隐蔽性极强、复现难度极高、线上危害极大仅需一行排序代码即可100%根治是所有后端开发必须掌握的标准化细节规范。九、场景复现更新死锁问题
返回列表