Spring Data JPA存储库、Spring事务和事件

3
在过去的几天中,尝试解决以下问题时,出现了大量的灰色头发。我正在自定义事件监听器中使用Spring Data JPA存储库,这些存储库利用简单的Spring 3.2事件机制。我遇到的问题是,如果ListenerA创建实体并调用assetRepository.save(entity)assetRepository.saveAndFlash(entity),则从另一个侦听器检索此相同实体的后续调用将失败。原因似乎是ListenerB无法在数据库中找到原始实体,它似乎仍然在Hibernate的缓存中。 触发ListenerB锁定实体的是作为线程池中可运行任务执行结果而触发的事件。 我的配置如下:
<bean id="entityManagerFactory"
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="persistenceUnitName" value="spring-jpa" />
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            <property name="generateDdl" value="false" />
            <property name="database" value="#{appProps.database}" />
        </bean>
    </property>
    <property name="jpaProperties">
        <props>
            <prop key="hibernate.dialect">org.hibernate.dialect.Oracle10gDialect</prop>
            <prop key="hibernate.hbm2ddl.auto">#{appProps['hibernate.hbm2ddl.auto']}</prop>
            <prop key="hibernate.show_sql">#{appProps['hibernate.show_sql']}</prop>
            <prop key="hibernate.format_sql">#{appProps['hibernate.format_sql']}</prop>
            <prop key="hibernate.search.default.directory_provider">org.hibernate.search.store.impl.FSDirectoryProvider</prop>
            <prop key="hibernate.search.default.indexBase">#{appProps.indexLocation}</prop>
            <prop key="hibernate.search.lucene_version">#{appProps['hibernate.search.lucene_version']}</prop>
        </props>
    </property>
</bean>

<tx:annotation-driven transaction-manager="transactionManager" />

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
    <property name="jpaDialect">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />
    </property>
</bean>

我忽略了dataSource配置,它是一个ComboPooledDataSource实例,用于定义与Oracle数据库的连接。额外说明一下,组件扫描被使用,并且该项目是Spring MVC。
现在开始讲Java类。 ListenerA
@Sevice
public class ListenerA implements ApplicationListener<FileUploadedEvent> {

    @Autowired
    private AssetRepository assetRepository;

    @Autowired
    private ExecutorService executor; // Triggers runnable task on a Job in Spring's TaskExecutor

    @Override
    @Transactional
    public void onApplicationEvent(FileUploadedEvent event) {

    Asset target = event.getTarget();
    Job job = new Job(target);
    assetRepository.save(job);

    executor.execute(job);
}

ListenerB

@Sevice
public class ListenerB implements ApplicationListener<JobStartedEvent> {

    @Autowired
    private AssetRepository assetRepository;


    @Override
    @Transactional
    public void onApplicationEvent(JobStartedEvent event) {

    String id = event.getJobId();
    Job job = assetRepository.findOne(id); // at this point we can not find the job, returns null
    job.setStartTime(new DateTime());
    job.setStatus(Status.PROCESSING);

    assetRepository.save(job);
}

JobStartedEvent 是从 TaskExecutor 中的可运行任务触发的事件。 我在这里做错了什么?我尝试使用自定义事件发布程序,该程序具有事务感知能力,但似乎并没有解决问题。我还尝试将适当的服务连接而不是数据存储库,并从侦听器中删除 @Transactional 注释,但也失败了。欢迎提出任何合理的建议以解决问题。


你确定 onapplicationevent 方法上的事务注解在起作用吗?如果将作业的创建放入服务 bean 中并将事务注解移动到那里,会发生什么? - Martin Frey
我倾向于认为注释是有效的,因为Job嵌入了在执行期间所需的延迟加载集合。关于第二个问题,我已经通过用服务替换Spring Data存储库来解决了这个问题。但是,为了使其工作,我必须使用@Transactional(propagation=Propagation.REQUIRES_NEW)注释初始化方法(其中首先创建和保存Job对象)。我不确定这是否是解决此问题的正确方法。为什么我不能在我的监听器中使用存储库? - pilot
这可能不是原因,但无论如何 - 您似乎在退出事务块并提交事务之前调用executor.execute(job)。您不能100%保证在执行onApplicationEvent之前提交事务。 - Krešimir Nesek
2个回答

2

感谢@Kresimir Nesek的提示,我已经解决了问题。因此,解决方案是使用适当的服务替换Spring Data repositories。

以下是修改后的类。

A监听器

@Sevice
public class ListenerA implements ApplicationListener<FileUploadedEvent> {

    @Autowired
    private JobService service;

    @Autowired
    private ExecutorService executor; // Triggers runnable task on a Job in Spring's TaskExecutor

    @Override
    public void onApplicationEvent(FileUploadedEvent event) {

    Job job = service.initJobForExecution(event.getTarget());

    executor.execute(job);
    }
 }

JobService方法中,必须使用@Transactional(propagation=Propagation.REQUIRES_NEW)注释来使initJobForExecution(Asset target)方法正常工作。 监听器B
@Sevice
public class ListenerB implements ApplicationListener<JobStartedEvent> {

@Autowired
private JobService service;


@Override
public void onApplicationEvent(JobStartedEvent event) {
    service.updateStatus(event.getJobId(), Status.PROCESSING); 
 }
}

1

虽然这篇文章有点旧,但我遇到了同样的问题,现在是使用Spring 4.1.1.RELEASESpring Data JPA 1.7.0Hibernate 4.3.5.Final

我的情境是在测试中出现一些失败,而我们的问题是由单连接模式下的H2、广播异步事件以及事件事务性引起的。

解决方案

  1. 第一个问题是由于事务超时引起的,通过在H2 URL字符串中添加 MVCC=true 解决。请参见:https://dev59.com/sW855IYBdhLWcg3wvnOE#6357183

  2. 异步事件在测试期间会导致问题,因为它们在不同的线程上执行。在事件配置中,使用了任务执行器和线程池。为了解决问题,只需提供一个覆盖配置bean,将 SyncTaskExecutor 作为任务执行器。这将导致所有事件同步发生。

  3. 事件的事务性很棘手。在我们的框架中,事件从事务 (@Transactional) 中广播。然后,在事务上下文之外的另一个线程上处理该事件。这引入了竞争条件,因为处理程序通常依赖于事务对象已经提交。我们没有在 Windows 开发机上注意到这个问题,但当部署到 Linux 上时就变得明显了。解决方案使用 TransactionSynchronizationManager.registerSynchronization()TransactionSynchronization.afterCommit() 的实现,在提交后广播事件。有关更多信息和示例,请参见 http://www.javacodegeeks.com/2013/04/synchronizing-transactions-with-asynchronous-events-in-spring.html

  4. 与 #3 相关,我们不得不为一些从某些事件处理程序调用的服务方法添加 @Transactional(propagation = REQUIRES_NEW)

希望这能帮助一些迟到的人。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接