在Spring应用程序中,在事务内部使用异步

8
我有一个Spring应用程序,使用@Transactional方法更新MySQL DB中特定实体的详细信息。在同一个方法中,我试图使用@Async调用另一个端点(即另一个Spring应用程序),该端点从MySql DB中读取相同的实体并将值更新到Redis存储中。
现在的问题是,每当我更新实体的某个值时,它有时会在redis中更新,有时则不会。
当我尝试进行调试时,我发现有时第二个应用程序从MySql中读取实体时会选择旧值而不是更新后的值。
有人能建议我如何避免这种情况,并确保第二个应用程序始终从Mysql中选择实体的更新值吗?

11
只有在@Transactional方法完成后,事务才会被提交。因此,根据@Async方法执行的速度,事务可能(或不可能)已经被提交。为了保持一致的行为,不要在@Transactional方法中调用@Async方法,而是创建另一个类,该类先调用@Transactional方法,然后再调用@Async方法。 - M. Deinum
那个评论与我的评论/回答有什么关系?它没有增加任何内容。 - M. Deinum
默认情况下@M.Deinum的Transactional(propagation = Propagation.REQUIRED)意味着通过调用该方法会启动新线程,因此编译器不会等待完成并转到下一行是异步方法。我认为它无法保证顺序执行。 - Pasha
@Pasha,你提到“编译器”等待的说法让人担忧。我不认为你的评论有任何意义。 - Boris the Spider
那么为什么?@BoristheSpider - Pasha
显示剩余3条评论
2个回答

24

M. Deinum的答案很好,但还有另一种方法可以实现这一点,可能更简单,具体取决于您当前应用程序的状态。

您只需在事件中包装对异步方法的调用,该事件将在当前交易提交后处理,因此每次都可以正确地从数据库中读取更新后的实体。

这很简单,让我向您展示:

import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

 @Transactional
public void doSomething() {

    // application code here

    // this code will still execute async - but only after the
    // outer transaction that surrounds this lambda is completed.
    executeAfterTransactionCommits(() -> theOtherServiceWithAsyncMethod.doIt());

    // more business logic here in the same transaction
}

private void executeAfterTransactionCommits(Runnable task) {
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        public void afterCommit() {
            task.run();
        }
    });
}

这里基本上发生的情况是,我们提供了当前事务回调的实现,并且仅覆盖了afterCommit方法 - 还有其他可能有用的方法,请查看它们。为了避免在其他地方使用此方法或使方法更易读而输入相同的样板代码,我将其提取到辅助方法中。


1
太棒了,解决了我的问题!让我感到无比开心。 - Max Binnewies
1
@uneq95 当然不会,您添加的回调函数仅与当前事务相关联。您可以查看registerSynchronization方法的javadoc(以及实现),它说:“为当前线程注册新的事务同步。”。因此,它当然不能影响其他方法中发生的情况,而且,您可以通过条件注册同步。将其视为在提交当前事务后运行的代码片段即可。 - cristian.andrei.stan
1
@ArunGowda 嗯,我没有注意到你的第二个问题。如果您使用声明方法的接口,并从该接口创建匿名类(这里正在发生什么),那么您将被迫实现该接口中定义的所有方法,显然是正确的。但是,如果您查看实际的TransactionSynchronization代码或javadoc,您会发现您不必这样做,因为所有方法都声明为默认值。因此,它们不是需要实现的声明,而是可以由您覆盖的空方法定义。 - cristian.andrei.stan
2
这是一个杰作解决方案! - Alain Cruz
你救了我的一天!谢谢! - undefined
显示剩余5条评论

3
解决方案并不难,很显然你希望在数据写入数据库后触发更新。 @Transactional 仅在方法执行完成后提交。如果在方法结束时调用另一个 @Async 方法,则取决于提交的持续时间(或实际的 REST 调用),事务可能已经提交或者未提交。
由于事务之外的东西只能查看已提交的数据,因此它可能会看到更新后的数据(如果已经提交)或旧数据。这还取决于事务的序列化级别,但通常出于性能原因,您不想在数据库上使用独占锁。
要解决此问题,@Async 方法不应从 @Transactional 内部调用,而应在其后立即调用。这样,数据总是被提交,其他服务将看到更新后的数据。
@Service
public class WrapperService {

    private final TransactionalEntityService service1;
    private final AsyncService service2;

    public WrapperService(TransactionalEntityService service1, AsyncService service2) {
        this.service1=service1;
        this.service2=service2;
    }

    public updateAndSyncEntity(Entity entity) {
       service1.update(entity); // Update in DB first
       service2.sync(entity); // After commit trigger a sync with remote system
    }
}

这项服务是非事务性的,因此假定service1.update@Transactional,它将更新数据库。完成后,您可以触发外部同步。


3
需要注意外部事务作用域,添加“never propagate”注释将防止将来出现问题。否则,某人更改代码以添加封闭的事务将导致同样不可预测的行为 - 这可能无法被检测到。 - Boris the Spider

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