You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)
    ... 省略中间栈帧 ...

错误原因分析
  1. Hibernate ActionQueue并发修改:Hibernate的ActionQueue用于存储当前会话需要执行的数据库操作(如插入、更新),事务提交前会遍历该队列执行操作。而AuditListener在@PostPersist事件中调用的LogService.save方法使用Propagation.REQUIRES_NEW事务传播级别——这会暂停当前事务,开启新事务并提交,新事务中的日志插入操作会修改当前会话的ActionQueue,但此时原事务的队列正在被遍历,直接触发ConcurrentModificationException。

  2. 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注解异步保存日志,脱离当前事务上下文:

  1. 给LogService.save添加@Async注解:
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void save(EventLog entity) throws Exception {
    logRepository.save(entity);
}
  1. 在配置类添加@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 19:02:12