坑惨了!Hibernate NonUniqueObjectException 偶发报错,最后一条明细必现?

发布时间:2026/8/3 13:07:39
坑惨了!Hibernate NonUniqueObjectException 偶发报错,最后一条明细必现? 关于《运维踩坑记》这是一个没有固定更新计划的系列。每一次遇到值得记录的异常、报错或诡异现象处理完之后就随手记下来——可能是一个 SQL 的语法陷阱可能是一次网络抖动的排查也可能是一个配置参数的误解。没有刻意安排遇到了就写写完了就沉淀。如果这些记录能帮你在未来的某个深夜少走一段弯路那这个系列就有了它存在的意义。本期是第 13 期Hibernate NonUniqueObjectException 偶发报错的“排雷”实录。欢迎阅读也欢迎交流。摘要生产环境 WMS 系统收货作业平时一切正常却在最后一条明细时偶发抛出NonUniqueObjectException日志中还夹杂着第三方接口的 WSDL 警告极易误判方向。本文完整记录了从现象复现、日志分析、Hibernate Session 缓存机制解读、到业务代码逐层排查的全过程。最终定位根因同一个 Session 中两个 ID 相同的实体实例导致saveOrUpdate失败。解决方案未改动底层 DAO而是巧妙地将风险操作替换为HQL 直接更新既保留了原有的父子级联完成业务又彻底绕开了缓存冲突。文章适合所有使用 Hibernate/Spring 生态进行企业级开发的技术人员阅读。一、事发背景某仓储管理系统的RF 手持终端收货功能平时运行基本正常。但近期测试人员反馈个别单据在收货到最后一条明细时后台偶尔会抛出异常前台表现为操作失败。由于是偶发开发组一开始并未重视直到生产环境也出现了同样的报错日志。二、案发现场日志分析以下是从日志文件中截取的关键片段敏感信息已脱敏[system][WARN] 2026-08-03 08:55:43 - Could not find a matching method for operation {http://xxx.com/service}GetContainerBankLocation. Operation will be unavailable. [system][WARN] 2026-08-03 08:55:43 - Could not find a matching method for operation {http://xxx.com/service}GetContainerBankInfoAndAuthorization. Operation will be unavailable. [system][ERROR] 2026-08-03 08:55:55 - start execute rule 批次号规则 [system][ERROR] 2026-08-03 08:55:55 - 批次号规则end, use time 41 [system][ERROR] 2026-08-03 08:55:55 - start execute rule 库位包装级别规则 [system][ERROR] 2026-08-03 08:55:55 - 库位包装级别规则end, use time 23 org.springframework.orm.hibernate3.HibernateSystemException: a different object with the same identifier value was already associated with the session: [com.xxx.model.receiving.BookingOrder#123196]; nested exception is org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session: [com.xxx.model.receiving.BookingOrder#123196]初步观察前面几条 WSDL 警告来自第三方系统接口后来确认与此异常无关。真正的异常是 Hibernate 抛出的NonUniqueObjectException。异常指向实体BookingOrderID 123196。三、第一次误判是数据库主键冲突吗很多同学第一反应是数据库里是不是已经有这条记录了但仔细检查后发现数据库中存在id 123196的BookingOrder记录这是正常的预约单主数据。而且业务语义是“更新这条记录的状态为已完成”并不是插入新数据。所以问题不发生在数据库层而是发生在 Hibernate 的 Session 缓存层。四、核心原理Hibernate Session 缓存机制NonUniqueObjectException的官方解释是在同一个 Session 中尝试保存/更新一个标识符ID相同但实例不同的对象时抛出。简单理解就是Session 是一个一级缓存。同一个 Session 中不允许存在两个ID 相同但内存地址不同的实体对象。当调用saveOrUpdate()时Hibernate 会检查缓存中是否已存在该 ID 的对象。如果存在且不是当前这个实例就直接报错。五、代码追踪找到「作案现场」通过分析堆栈调用链如下TerminalReceiveShell.mainProcess → PutawayManager.addReceiveRecord → ReceivingManager.detailReceive → OrderManager.receive → OrderManager.completeBooking ← 异常抛出点关键代码原始版本已脱敏privatevoidcompleteBooking(OrderDetaildetail){BookingOrderbookingdetail.getBooking();if(bookingnull)return;// 特殊业务处理略if(someSpecialCondition){bookingthis.commonDao.get(BookingOrder.class,booking.getId());}// 查询是否还有未收完的明细StringhqlSELECT count(*) FROM OrderDetail detail WHERE detail.booking.id :bookingId AND detail.expectedQuantity detail.receivedQuantity;Longcount(Long)commonDao.findByQueryUniqueResult(hql,bookingId,booking.getId());if(countnull||count.equals(0L)){// ⚠️ 问题就出在这里booking.setFinishTime(newDate());booking.setStatus(BookingStatus.FINISH);commonDao.store(booking);// 底层是 saveOrUpdate// 同时更新所有关联的子单hqlUPDATE BookingOrder SET finishTime :finishTime, status :status WHERE preId :preId;commonDao.executeByHql(hql,...);}}问题点分析count 0意味着所有明细都已收货完成此时需要标记预约单为FINISH。但在调用completeBooking之前同一个事务中booking对象可能已经被规则引擎批次号规则、库位包装级别规则或其他查询加载过并存在于 Session 缓存中。而当前传入的booking可能是通过detail.getBooking()获取的另一个实例脱管态与缓存中的实例不是同一个对象。因此saveOrUpdate执行时Hibernate 检测到两个 ID 相同的实例直接抛出异常。六、为什么「最后一条」才触发因为completeBooking中的if (count null || count.equals(0L))分支只要还有任意一条明细expectedQuantity receivedQuantitycount 0不会进入完成逻辑。只有最后一条明细处理完后count 0才会进入该分支执行store。所以异常只在最后一条明细时出现前 N-1 条永远不会触发。为什么是偶发因为不是每次收货都会刚好满足“全部完成”条件可能一次性全收完可能分批收货也可能有驳回/异常。只有业务上触发完成逻辑时才会复现。七、表结构辅助分析为了验证preId的含义查看表结构已脱敏commentontableBOOKING_ORDERis预约表;commentoncolumnBOOKING_ORDER.idisID;commentoncolumnBOOKING_ORDER.pre_idis原拆分ID;commentoncolumnBOOKING_ORDER.statusis状态;commentoncolumnBOOKING_ORDER.finish_timeis完成时间;-- 其他字段省略确认pre_id为自关联外键指向父预约单 ID。因此原代码中UPDATE ... WHERE preId :preId的目的是将当前单及其所有子单一并标记为完成。八、解决方案不改底层 DAO用 HQL 绕开缓存核心思路既然saveOrUpdate会触发缓存冲突那我们直接绕过 Session 缓存使用 HQL 进行数据库更新。修改前有风险的代码booking.setFinishTime(newDate());booking.setStatus(BookingStatus.FINISH);commonDao.store(booking);// ← 这里抛出 NonUniqueObjectExceptionStringhqlUPDATE BookingOrder SET finishTime :finishTime, status :status WHERE preId :preId;commonDao.executeByHql(hql,...);修改后安全的代码// 1. 更新当前预约单本身替代有风险的 storeStringhqlSelfUPDATE BookingOrder SET finishTime :finishTime, status :status WHERE id :id;commonDao.executeByHql(hqlSelf,newString[]{finishTime,status,id},newObject[]{newDate(),BookingStatus.FINISH,booking.getId()});// 2. 更新所有 preId 指向当前单的子预约单保留原业务逻辑StringhqlPreUPDATE BookingOrder SET finishTime :finishTime, status :status WHERE preId :preId;commonDao.executeByHql(hqlPre,newString[]{finishTime,status,preId},newObject[]{newDate(),BookingStatus.FINISH,booking.getId()});为什么这样改是安全的对比项saveOrUpdateHQL 直接更新是否经过 Session 缓存检查✅ 是会检查并可能抛出异常❌ 否直接生成 SQL 执行是否可能触发NonUniqueObjectException✅ 可能❌绝对不会是否更新内存对象状态✅ 会同步更新缓存❌ 不会但后续不再使用该对象是否触发拦截器/监听器✅ 会触发❌ 绕过由于代码执行到此处时整个收货流程即将结束内存中的booking对象后续不再有任何操作因此内存状态与数据库短暂不一致是完全可接受的。九、验证结果修改后经过多轮测试✅ 正常收货流程无影响。✅ 最后一条明细触发完成逻辑时异常不再复现。✅ 数据库状态更新正确当前单及所有关联单均置为完成。✅ 对原有业务逻辑零侵入。十、经验总结与避坑指南理解 Hibernate Session 生命周期同一个事务中尽量避免对同一个 ID 的实体进行多次加载尤其要避免人为new出脱管态对象再saveOrUpdate。mergevssaveOrUpdate如果必须处理脱管对象优先使用merge()它会自动合并缓存中的数据避免冲突。HQL 是绕过缓存的利器在批量更新、状态标记等“终点操作”场景中使用 HQL 直接更新数据库可以规避很多缓存问题。不要被无关日志干扰本例中 WSDL 警告虽然出现在异常附近但经过分析确认无关。排查问题时要紧盯堆栈分清主次。偶发≠不严重偶发异常往往是最隐蔽的因为它们只在特定条件下触发容易被忽略。一定要结合业务逻辑找到“触发条件”才能精准定位。写在最后“当 Session 缓存跟你较劲的时候别硬扛——绕开它用 HQL 直捣数据库世界瞬间清净。”《运维踩坑记》系列索引排查 2 小时改代码 5 分钟一行沉睡 10 年的 Log4j 配置差点让我怀疑人生别让一个空格搞垮你的 WMS 报表——ORA-01722“无效数字”排查实战与终极防御能 ping 通却端口不通跨网段虚拟机故障复盘别只会重启救急别被 Excel“骗”了明明显示整数导入系统却报错原来是它在捣鬼跨越数据库的“隐形地雷”一次 ORA-22992 引发的跨库 LOB 问题彻底剖析JUnit 测试中的常见异常一Before/After方法为何导致“No tests found”悲剧就因为一个“yyyy-MM-dd”我的跨年加班费没了——日期格式化的那些天坑一次Oracle会话爆满的惊魂时刻Spring Boot MyBatis连接池配置救场WMS 拣货任务“投线”之谜从一次诡异的 Bug 到架构重构Tomcat 严重警告JDBC 驱动未注销 工作线程泄漏 —— 原因、影响与彻底修复一条 SQL 的“CASE 陷阱”与跨库优化实践一次 Oracle 复杂 SQL 的“排雷”实录Hibernate NonUniqueObjectException 偶发报错的“排雷”实录 本文折哥于 2026年8月 记录本文属于《运维踩坑记》系列第 13 期希望这篇文章能帮助到正在被NonUniqueObjectException折磨的你。如果这些记录能帮你在未来的某个深夜少走一段弯路那这个系列就有了它存在的意义。如果觉得有用欢迎点赞、收藏、评论交流