英文:
Concurrency errors when using Spring Transaction management in Tomcat
问题
I am currently working on retrofitting Spring-based declarative transactions to a legacy application.
The application is deployed on Tomcat and uses JPA/Hibernate to access a PostgreSQL database, and a homegrown web framework (so switching e.g. to Spring Boot is at this time not an option).
My problem is that after changing all DAOs to use an injected EntityManager, everything works when there is only a single user, but with multiple users, I get exceptions that indicate concurrency problems:
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)
and
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]
and
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]
This is the superclass all DAOs inherit from to get their EntityManager
instance:
public abstract class AbstractDao {
protected EntityManager entityManager;
protected AbstractDao() {
}
@PersistenceContext(type = PersistenceContextType.EXTENDED)
@Scope("request") // just an experiment, should not be necessary AFAIK
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
}
This is the persistence configuration:
@Configuration
@EnableAsync
@EnableTransactionManagement
public class PersistenceConfiguration {
@Bean
public EntityManagerFactory getEntityManagerFactory() {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("my.package");
return entityManagerFactory;
}
@Bean
public PlatformTransactionManager initTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(getEntityManagerFactory());
transactionManager.setJpaDialect(new HibernateJpaDialect());
return transactionManager;
}
}
Here is the persistence.xml
<?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="hibernate.connection.driver_class" value="org.postgresql.Driver" />
<property name="show_sql"
<details>
<summary>英文:</summary>
I am currently working on retrofitting Spring-based declarative transactions to a legacy application.
The application is deployed on Tomcat and uses JPA/Hibernate to access a PostgreSQL database, and a homegrown web framework
(so switching e.g. to Spring Boot is at this time not an option).
My problem is that after changing all DAOs to use an injected EntityManager, everything works when there is only a single user,
but with multiple users, I get exceptions that indicate concurrency problems:
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)
and
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]
and
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]
This is the superclass all DAOs inherit from to get their `EntityManager` instance:
public abstract class AbstractDao {
protected EntityManager entityManager;
protected AbstractDao() {
}
@PersistenceContext(type = PersistenceContextType.EXTENDED)
@Scope("request") // just an experiment, should not be necessary AFAIK
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
}
This is the persistence configuration:
@Configuration
@EnableAsync
@EnableTransactionManagement
public class PersistenceConfiguration {
@Bean
public EntityManagerFactory getEntityManagerFactory() {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("my.package");
return entityManagerFactory;
}
@Bean
public PlatformTransactionManager initTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(getEntityManagerFactory());
transactionManager.setJpaDialect(new HibernateJpaDialect());
return transactionManager;
}
}
Here is the `persistence.xml`
<?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="hibernate.connection.driver_class" value="org.postgresql.Driver" />
<property name="show_sql" value="false"/>
<property name="hibernate.show_sql" value="false" />
<property name="hibernate.format_sql" value="false" />
<property name="hibernate.cache.use_second_level_cache" value="false"/>
<property name="tomee.jpa.factory.lazy" value="true"/>
</properties>
</persistence-unit>
</persistence>
And the configuration in `web.xml` seems to be exactly what is needed to support the request scope, according to [the Spring documentation][1] - I have tried configuring either a `RequestContextListener` or a `RequestContextFilter`, and can confirm via debugger that they are called, but neither seems to have an effect.
My understanding, according to [this article][2] is that the `@PersistenceConfig` annotation should
provide a proxy which forwards calls to a request-scoped entity manager instance that is set up by the `RequestContextListener` or `RequestContextFilter`.
For some reason, that doesn't seem to be working and instead every request goes to the same entity manager (or at least the same Hibernate session).
Or is the problem the `transaction-type="RESOURCE_LOCAL"` in the `persistence.xml`?
According to [this Stack Overflow answer][3]
it sounds like I have to use `transaction-type="JTA"` to use `@PersistenceContext` and have multiple `EntityManager` instances,
but that seems to be copied verbatim from [the TomEE documentation][4]
(a JEE container), and it is contradicted [this article][5]
which says that "by default, Spring applications use RESOURCE_LOCAL transactions".
What am I missing?
[1]: https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/core.html#beans-factory-scopes-other-web-configuration
[2]: https://dzone.com/articles/how-does-spring-transactional
[3]: https://stackoverflow.com/questions/17331024/persistence-xml-different-transaction-type-attributes
[4]: https://tomee.apache.org/jpa-concepts.html
[5]: https://by%20https://vladmihalcea.com/jpa-persistence-xml/
</details>
# 答案1
**得分**: 1
上述的异常表示您在多个线程中同时使用相同的 Hibernate 会话。
在 Tomcat 中,任何 servlet 请求都在单独的线程中处理,因此当您使用 Spring 时,它会注入一个特殊的代理而不是简单的 EntityManager(参见 [SharedEntityManagerCreator][1])。该代理会自动重用现有的 `EntityManager`,或者根据当前事务(再次是线程范围的)创建一个新的。
`EntityManager` 本身代表默认绑定到特定事务的持久性上下文。但是在您的代码中,您使用了扩展作用域的持久性上下文,可以跨多个事务使用。
这似乎是您情况的罪魁祸首:对于一个用户,您有一个线程且没有数据竞争,对于多个用户,您的 `EntityManager` 被以竞争的方式访问,导致 `CME`。
要解决这个问题,请将您的代码重写为:
```java
public abstract class AbstractDao {
@PersistenceContext
protected EntityManager entityManager;
protected AbstractDao() {
}
}
另外,一旦您使用了 @EnableTransactionManagement
,我建议使用 Java 配置来代替 persistence.xml
。请参见此处的示例 here。
英文:
The upper exception indicates that you are using the same Hibernate Session in multiple threads concurrently.
In Tomcat any servlet request is handled in a separate thread, so when you are on Spring it injects a special proxy instead of a simple EntityManager (see SharedEntityManagerCreator). This proxy will automatically either reuse the existing EntityManager
or create a new one depending on the current transaction (which is again thread-scoped).
EntityManager
itself represent persistence context which is by default bound to a particular transaction. But in your code you use extended-scoped persistence context which can be used across multiple transactions.
@PersistenceContext(type = PersistenceContextType.EXTENDED)
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
This seems to be a culprit of your case: with one user you have one thread and no data races, with multiple ones you EntityManager
is accessed in racy way resulting in CME
.
To fix the issue rewrite your code as:
public abstract class AbstractDao {
@PersistenceContext
protected EntityManager entityManager;
protected AbstractDao() {
}
}
Also as soon as your are using @EnableTransactionManagement
I'd suggest to rid persistence.xml
in favor of Java config. See the example here.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论