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
相关产品推荐
相关产品推荐

