@Transactional(REQUIRES_NEW)事务被父事务回滚问题排查
嘿,这个问题我之前帮不少开发者排查过,本质是Spring事务传播机制和JPA/Hibernate一级缓存的冲突,结合你的代码和场景,给你拆解下核心原因,再给几个适配遗留项目的修复方案:
你的猜想方向是对的,但还有个关键细节没戳中:
对象的“托管归属”问题
你在父事务里用crudService.findByProperty加载的Object o,是被父事务的EntityManager托管的(也就是在一级缓存里)。调用y.changeStatus(o, ERROR)时,虽然这个方法开了REQUIRES_NEW的独立事务,但传入的o仍然属于父事务的缓存上下文,内部事务的EntityManager根本没“接管”这个对象。
更重要的是,你的changeStatus只是改了o的属性值,没执行任何持久化操作(比如save/merge)——内部事务的EntityManager不知道这个对象被修改了,所以就算事务提交,也不会把变更写到数据库。父事务回滚的状态重置
当你抛RuntimeException触发父事务回滚时,父事务的EntityManager会把它托管的o重置成加载时的初始状态。而因为内部事务没真正持久化修改,数据库自然看不到状态变化;就算内部事务侥幸把修改写到数据库,父事务的缓存对象也可能在后续流程里把数据库的修改覆盖掉。
你说调试时看到新事务提交逻辑执行了但修改没持久化,这是因为Spring确实开了新事务并提交,但事务里没有实际要flush的变更,提交只是个空操作而已。
因为不能做架构大改,我们只需要调整Y.changeStatus方法,确保内部事务能真正把状态改写到数据库,同时避开父事务缓存的干扰:
方案1:内部事务重新加载对象(推荐)
在changeStatus里,用o的唯一标识重新查询一次对象,这样新加载的对象属于内部事务的EntityManager托管,修改后会自动被持久化:
public class Y implements IY { private final CrudService crudService; // 需要注入你的CrudService public Y(CrudService crudService) { this.crudService = crudService; } @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void changeStatus(Object o, String status) { // 用唯一标识重新加载,让内部事务接管这个对象 Object managedO = crudService.findByProperty(o.getObjectUuid()); managedO.status = status; // 如果是JPA实现,托管对象在事务提交时会自动flush,不需要手动save; // 如果是MyBatis这类需要手动保存的,这里加crudService.save(managedO); } }
这个方案的好处是彻底隔离了父事务和内部事务的对象,父事务回滚完全不会影响内部事务的持久化结果,也避开了缓存的所有坑。
方案2:手动合并对象到内部事务上下文
如果重新加载对象的成本太高,可以用EntityManager.merge把传入的o合并到内部事务的上下文中,让内部事务接管这个对象的修改:
public class Y implements IY { @PersistenceContext private EntityManager entityManager; @Override @Transactional(propagation = Propagation.REQUIRES_NEW) public void changeStatus(Object o, String status) { o.status = status; // 把对象合并到内部事务的EntityManager,让它托管这个修改 entityManager.merge(o); // 事务提交时会自动flush这个变更 } }
这个方案不需要重新查询,只需要注入EntityManager,适合对象加载成本高的场景。
验证效果
修改后,当触发校验失败分支时:
- 内部事务会把
o的状态变更持久化到数据库(不管是重新加载还是merge) - 父事务抛异常回滚,但已经提交的内部事务结果不受影响
- 事件会因为异常失败触发重试,此时重试查询到的
o已经是ERROR状态,能按你的业务逻辑处理(比如跳过或执行其他操作)
你提到“移除RuntimeException的话修改能正常持久化”,这是因为此时父事务会正常提交,父事务的EntityManager会把它托管的o的修改flush到数据库——但这不是内部事务的功劳,是父事务提交的结果。
内容的提问来源于stack exchange,提问作者MrM




