捕获UniqueConstraintViolationException后事务仍回滚,求解决方案
问题原因分析
核心问题:事务回滚标记无法清除
- 事务传播共享上下文:
m2方法的@Transactional默认传播行为是REQUIRED,会直接加入m1开启的同一事务上下文。第一次抛出UniqueConstraintViolationException时,Spring事务管理器会立刻标记当前事务为需回滚状态——哪怕你在m2内部捕获了异常,这个回滚标记也不会被清除。 - 回滚标记不可逆:后续重试保存成功后,
m1执行完毕时Spring仍会按照回滚标记,将整个事务(包括m3的写入操作)全部回滚,最终返回500错误。
解决方案
方案一:给m2设置独立事务(推荐)
将m2的事务传播属性改为REQUIRES_NEW,让每次重试都在独立事务中执行,不会影响m1的主事务:
@Transactional(propagation = Propagation.REQUIRES_NEW) void m2(Event event){ boolean success = false; while(!success){ try{ eventRepo.save(event); success = true; } catch(UniqueConstraintViolationException e){ event.setVersion(++event.getVersion()); } } }
- 原理:
REQUIRES_NEW会暂停主事务,开启全新的独立事务,每次重试的保存操作都在新事务中完成,成功后立即提交,不受主事务的回滚标记影响。 - 效果:
m2的操作提交后,m1的主事务可以正常执行m3的写入,最终整体提交。
方案二:调整异常类型避免触发回滚
Spring默认仅对RuntimeException和Error触发事务回滚,可将异常转换为受检异常,避免事务管理器标记回滚:
@Transactional void m2(Event event){ boolean success = false; while(!success){ try{ eventRepo.save(event); success = true; } catch(UniqueConstraintViolationException e){ event.setVersion(++event.getVersion()); // 包装为受检异常,避免触发事务回滚 throw new CheckedVersionConflictException("版本冲突,重试中", e); } } } // 自定义受检异常 class CheckedVersionConflictException extends Exception { public CheckedVersionConflictException(String message, Throwable cause) { super(message, cause); } }
同时在m1中捕获该受检异常:
@Transactional void m1(Event event){ try { m2(event); } catch (CheckedVersionConflictException e) { // 兜底逻辑,理论上因m2重试成功不会走到这里 } m3(); }
内容的提问来源于stack exchange,提问作者Satej
相关产品推荐
相关产品推荐

