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

Tomcat中使用Spring事务管理出现并发错误排查

多用户并发下JPA/Hibernate事务异常排查与解决方案

问题背景

部署在Tomcat上的遗留系统,采用自研Web框架(暂无法切换到Spring Boot),改造为Spring声明式事务+JPA/Hibernate访问PostgreSQL数据库。单用户场景运行正常,但多用户访问时出现并发异常,包括ConcurrentModificationException、Hibernate AssertionFailure等。

异常栈信息

异常1:ConcurrentModificationException(资源释放阶段)

Caused by: java.util.ConcurrentModificationException
    at java.base/java.util.HashMap.forEach(HashMap.java:1424)
    at org.hibernate.resource.jdbc.internal.ResourceRegistryStandardImpl.releaseResources(ResourceRegistryStandardImpl.java:328)
    at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.afterTransaction(AbstractLogicalConnectionImplementor.java:60)
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.afterTransaction(LogicalConnectionManagedImpl.java:167)
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.afterCompletion(LogicalConnectionManagedImpl.java:293)
    at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.commit(AbstractLogicalConnectionImplementor.java:95)
    at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:282)
    at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)

异常2:ConcurrentModificationException(ActionQueue执行阶段)

Caused by: java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013) ~[?:?]
    at java.util.ArrayList$Itr.next(ArrayList.java:967) ~[?:?]
    at java.util.Collections$UnmodifiableCollection$1.next(Collections.java:1054) ~[?:?]
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:602) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at java.util.LinkedHashMap.forEach(LinkedHashMap.java:721) ~[?:?]
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:344) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1407) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]

异常3:Hibernate AssertionFailure(非线程安全的Session访问)

Caused by: org.hibernate.AssertionFailure: possible non thread safe access to session
    at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:215) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at java.util.LinkedHashMap.forEach(LinkedHashMap.java:721) ~[?:?]
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:344) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1407) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]
    at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1394) ~[hibernate-core-5.6.8.Final.jar:5.6.8.Final]

核心问题分析

所有异常的根源是多线程共享了同一个EntityManager实例。EntityManager(底层对应Hibernate Session)本身不是线程安全的,每个请求/事务必须使用独立的实例。现有配置中存在多处导致EntityManager共享的错误:

  • 持久化上下文类型错误:使用PersistenceContextType.EXTENDED,该上下文绑定到会话而非请求/事务,导致多请求共享同一实例。
  • DAO注入配置错误:给setEntityManager方法添加@Scope("request")无效,DAO默认是Spring单例Bean,方法级Scope注解无法改变字段作用域。
  • EntityManagerFactory初始化方式错误:手动用Persistence.createEntityManagerFactory创建实例,Spring无法生成请求作用域的EntityManager代理,导致所有请求共用同一实体管理器。

解决方案

1. 修正DAO的EntityManager注入

去掉EXTENDED类型和无效的@Scope注解,直接在字段上使用@PersistenceContext,Spring会自动注入请求/事务作用域的EntityManager代理:

public abstract class AbstractDao {
    
    @PersistenceContext
    protected EntityManager entityManager;

    protected AbstractDao() {
    }
}

2. 改用Spring托管的EntityManagerFactory

替换手动创建EntityManagerFactory的方式,使用LocalContainerEntityManagerFactoryBean,让Spring完全接管实体管理器的生命周期和代理生成:

@Configuration
@EnableAsync
@EnableTransactionManagement
public class PersistenceConfiguration {

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws NamingException {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        // 从Tomcat JNDI获取数据源
        Context ctx = new InitialContext();
        DataSource dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/myapp");
        em.setDataSource(dataSource);
        em.setPersistenceUnitName("de.lexcom.agroparts.persistence");
        // 指定Hibernate作为JPA供应商
        em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        return em;
    }

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(emf);
        transactionManager.setJpaDialect(new HibernateJpaDialect());
        return transactionManager;
    }
}

3. 调整persistence.xml配置

保留transaction-type="RESOURCE_LOCAL"(Tomcat非JTA环境下正确),移除与DataSource重复的配置项:

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd" version="2.1">
    <persistence-unit name="de.lexcom.agroparts.persistence" transaction-type="RESOURCE_LOCAL">     
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <non-jta-data-source>java:comp/env/jdbc/myapp</non-jta-data-source>
        <shared-cache-mode>NONE</shared-cache-mode>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
            <property name="show_sql" value="false"/>
            <property name="hibernate.format_sql" value="false"/>
            <property name="hibernate.cache.use_second_level_cache" value="false"/>
        </properties>
    </persistence-unit>
</persistence>

4. 确保自研Web框架的请求上下文生效

如果配置了RequestContextListener/RequestContextFilter仍无效,可在自研框架的请求入口手动初始化Spring请求上下文:

// 在每个请求处理前调用
ServletRequestAttributes attributes = new ServletRequestAttributes(request);
RequestContextHolder.setRequestAttributes(attributes);

// 请求处理完成后清理
RequestContextHolder.resetRequestAttributes();

关键说明

  • transaction-type="RESOURCE_LOCAL"不是问题根源,它适用于Tomcat这类非JTA容器,Spring的JpaTransactionManager可以正确管理本地事务。
  • 必须保证每个请求/事务使用独立的EntityManager实例,Spring通过代理实现请求作用域的EntityManager,无需手动创建和销毁。

内容的提问来源于stack exchange,提问作者Michael Borgwardt

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 11:27:02