使用Tomcat时,Spring事务管理的并发错误

huangapple go评论63阅读模式
英文:

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(&quot;request&quot;) // 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(&quot;my.package&quot;);
			
			return entityManagerFactory;
		}
	
		@Bean
		public PlatformTransactionManager initTransactionManager() {
			JpaTransactionManager transactionManager = new JpaTransactionManager();
			transactionManager.setEntityManagerFactory(getEntityManagerFactory());
			transactionManager.setJpaDialect(new HibernateJpaDialect());
			
			return transactionManager;
		}
	}
	
Here is the `persistence.xml`

	&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
	&lt;persistence xmlns=&quot;http://xmlns.jcp.org/xml/ns/persistence&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd&quot; version=&quot;2.1&quot;&gt;
		&lt;persistence-unit name=&quot;de.lexcom.agroparts.persistence&quot; transaction-type=&quot;RESOURCE_LOCAL&quot;&gt;		
			&lt;provider&gt;org.hibernate.jpa.HibernatePersistenceProvider&lt;/provider&gt;
			&lt;non-jta-data-source&gt;java:comp/env/jdbc/myapp&lt;/non-jta-data-source&gt;
			&lt;shared-cache-mode&gt;NONE&lt;/shared-cache-mode&gt;
			&lt;properties&gt;
				&lt;property name=&quot;hibernate.dialect&quot; value=&quot;org.hibernate.dialect.PostgreSQLDialect&quot;/&gt;
				&lt;property name=&quot;hibernate.connection.driver_class&quot; value=&quot;org.postgresql.Driver&quot; /&gt;
				&lt;property name=&quot;show_sql&quot; value=&quot;false&quot;/&gt;
				&lt;property name=&quot;hibernate.show_sql&quot; value=&quot;false&quot; /&gt;
				&lt;property name=&quot;hibernate.format_sql&quot; value=&quot;false&quot; /&gt;
				&lt;property name=&quot;hibernate.cache.use_second_level_cache&quot; value=&quot;false&quot;/&gt;
				&lt;property name=&quot;tomee.jpa.factory.lazy&quot; value=&quot;true&quot;/&gt;
			&lt;/properties&gt;
		&lt;/persistence-unit&gt;
	&lt;/persistence&gt;


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&#39;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=&quot;RESOURCE_LOCAL&quot;` in the `persistence.xml`? 
According to [this Stack Overflow answer][3] 
it sounds like I have to use `transaction-type=&quot;JTA&quot;` 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 &quot;by default, Spring applications use RESOURCE_LOCAL transactions&quot;.

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.

huangapple
  • 本文由 发表于 2023年4月19日 22:37:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/76055790.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定