Spring Boot JPA事务提交失败伴ConcurrentModificationException问题排查
在Spring Boot + JPA项目中,事务提交失败,嵌套异常为java.util.ConcurrentModificationException。场景是从一个带@Transactional注解的服务方法调用另一个服务的@Transactional方法,相关代码如下:
服务代码
Service 1
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void save1(ObjetType1 obj1) { ObjetType2 obj2=new ObjectType2(); service2.save2(obj2); repsitory1.save(obj1); }
Service 2
@Transactional public void save2(ObjetType2 obj2) { repsitory2.save(obj2); }
EntityListener与日志服务代码
两个实体都配置了EntityListener:
public class AuditListener { private static final Logger logger = LoggerFactory.getLogger(AuditListener.class); static private LogService logService; @Autowired public void init(LogService logService) { CertificateAuditListener.logService = logService; logger.info("Initializing with dependency [" + logService + "]"); } @PostPersist private void beforeAnySaveOrUpdate(Object obj) { // 从obj生成eventLog if (logService != null) { try { logService.save(eventLog); } catch (Exception e) { logger.warn("CertificateAuditListener Error {}", e); } } } }
LogService伪代码:
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void save(EventLog entity) throws Exception { logRepository.save(entity); }
报错信息
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:571) ... 省略中间栈帧 ... Caused by: javax.persistence.RollbackException: Error while committing the transaction at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:81) ... 省略中间栈帧 ... Caused by: java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997) at java.base/java.util.Collections$UnmodifiableCollection$1.next(Collections.java:1047) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:602) ... 省略中间栈帧 ...
Hibernate ActionQueue并发修改:Hibernate的
ActionQueue用于存储当前会话需要执行的数据库操作(如插入、更新),事务提交前会遍历该队列执行操作。而AuditListener在@PostPersist事件中调用的LogService.save方法使用Propagation.REQUIRES_NEW事务传播级别——这会暂停当前事务,开启新事务并提交,新事务中的日志插入操作会修改当前会话的ActionQueue,但此时原事务的队列正在被遍历,直接触发ConcurrentModificationException。EntityListener静态字段风险:
AuditListener用静态字段持有LogService,多线程环境下可能出现依赖注入不完整或线程安全问题,虽不是本次报错直接原因,但会引发其他潜在问题。
针对带EntityListener的嵌套实体持久化(需在Listener中持久化日志),可采用以下方案:
方案1:调整日志服务的事务传播级别
将LogService.save的事务传播级别改为Propagation.MANDATORY或Propagation.REQUIRED,避免开启新事务:
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void save(EventLog entity) throws Exception { logRepository.save(entity); }
- 优点:日志操作加入当前事务,保证数据一致性;不会触发ActionQueue并发修改问题。
- 缺点:主事务回滚时日志也会被回滚,若需日志独立于主事务则不适用。
方案2:在Listener中直接使用EntityManager持久化日志
不通过带事务的Service,直接在Listener中注入EntityManager持久化日志(需手动刷新):
public class AuditListener { private static final Logger logger = LoggerFactory.getLogger(AuditListener.class); @PersistenceContext private EntityManager entityManager; @PostPersist private void afterPersist(Object obj) { EventLog eventLog = // 从obj生成日志 try { entityManager.persist(eventLog); entityManager.flush(); // 立即执行持久化操作 } catch (Exception e) { logger.warn("AuditListener Error {}", e); } } }
- 优点:避免事务嵌套带来的问题,直接操作当前会话。
- 缺点:日志操作与主事务绑定,主事务回滚时日志也会回滚。
方案3:异步处理日志持久化
使用Spring的@Async注解异步保存日志,脱离当前事务上下文:
- 给
LogService.save添加@Async注解:
@Async @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void save(EventLog entity) throws Exception { logRepository.save(entity); }
- 在配置类添加
@EnableAsync开启Spring异步支持。
- 优点:日志操作在独立线程和事务中执行,不影响主事务,也不会修改主事务的ActionQueue。
- 缺点:日志与主事务的一致性无法保证(主事务回滚时日志已保存),需根据业务场景判断是否接受。
方案4:调整EntityListener的事件时机
将@PostPersist改为Hibernate专属的@PostCommitInsert(事务提交后执行日志保存):
import org.hibernate.annotations.PostCommitInsert; public class AuditListener { // ... 注入LogService ... @PostCommitInsert private void afterCommit(Object obj) { // 生成并保存日志 logService.save(eventLog); } }
- 优点:事务提交后再执行日志操作,完全避免ActionQueue的并发修改问题。
- 缺点:仅Hibernate支持该注解,不具备JPA通用性;日志保存失败无法回滚主事务。
方案5:修复Listener的静态字段问题
将LogService的静态字段改为非静态,通过Spring依赖注入保证每个Listener实例持有正确的服务引用:
public class AuditListener { private static final Logger logger = LoggerFactory.getLogger(AuditListener.class); private final LogService logService; @Autowired public AuditListener(LogService logService) { this.logService = logService; logger.info("Initializing with dependency [" + logService + "]"); } // ... 事件方法 ... }
- 说明:这是修复潜在线程安全问题的必要操作,需配合上述方案一起使用。
内容的提问来源于stack exchange,提问作者sharif2008

