spring 使用 JPA 事务的异步方法调用

spring async method call with JPA transactions

我正在使用 Spring Boot 实现后端服务。此服务接收 REST 请求并执行一些数据库操作并最终更新记录的状态。

之后,我想启动一个新的异步进程并以这种方式对同一条记录执行另一个数据操作:

@Service
public class ClassA {

    @Autowired
    private ClassB classB;

    @Autowired
    private MyEntityRepository repo;

    @Transactional
    public void doSomething(Long id) {
        // executing the business logic
        if (isOk()) {
            repo.updateStatus(id, Status.VERIFIED)
        }

        // I need to commit this DB transaction and return.
        // But after this transaction is committed, I need
        // to start an async process that must work on the
        // same record that was updated before.
        classB.complete(id);
    }
}

这是我的异步方法:

@Service
public class ClassB {

    @Autowired
    private MyEntityRepository repo;

    @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void complete(Long id) {
        Optional<MyEntity> myEntity = repo.findById(id);
        if (myEntity.isPresent() && myEntity.get().getStatus == Status.VERIFIED) {
            // execute 'business logic B' 
        }
    }
}

classA.doSomething()id被多次调用,但business logic B必须在DB中的记录状态为VERIFIED.[=26时执行=]

上述解决方案工作正常。

但我担心的是:我的测试数据库很小,classA.doSomething() 方法总是在 classB.complete() 开始检查数据库中同一记录的状态之前完成并关闭其事务.我在日志中看到 SQL 以正确的顺序执行:

* UPDATE STATUS FROM TABLE ... WHERE ID = 1 // doSomething()
* COMMIT
* SELECT * FROM TABLE WHERE ID = 1          // complete()

但是是否可以 100% 保证第一个 classA.doSomething() 方法总是在第二个 classB.complete() 异步调用检查同一记录的状态之前完成并提交事务?

如果异步方法 classB.complete() 将在 classA.doSomething() 完成并执行其数据库提交之前执行,那么我将破坏业务逻辑并且 business logic B 将被跳过(新数据库交易还不会看到更新的状态),这将导致一个大问题。如果数据库很大并且提交花费的时间比我的小型测试数据库花费的时间长,则可能会发生这种情况。

也许我可以使用 here 描述的数据库事务隔离级别进行操作,但更改此级别可能会导致应用程序的另一部分出现另一个问题。

正确实现此逻辑以保证异步方法正确执行顺序的最佳方法是什么?

不保证“第一个 classA.doSomething() 方法将始终在第二个 classB.complete() 异步调用检查同一记录的状态之前完成并提交事务".

事务被实现为某种适合框架的拦截器(对于 CDI 也是如此)。标记为@Transactional的方法被框架拦截,所以在方法关闭}之前事务不会结束。事实上,如果事务是由堆栈中更高层的另一个方法启动的,它会更晚结束。

因此,ClassB 有足够的时间 运行 并查看不一致的状态。

我会将 doSomething 的第一部分放在单独的 REQUIRES_NEW 事务方法中(您可能需要将其放在不同的 class 中,具体取决于您如何配置事务拦截器; 如果你使用 AOP,Spring 可能会拦截对同一对象的方法的调用,否则它依赖于注入的代理对象来进行拦截并且通过 this 调用方法将不会激活拦截器;这同样适用于其他框架,如 CDI 和 EJB)。方法 doSomething 调用第一部分方法,该方法在新事务中完成,然后 ClassB 可以异步继续。

现在,在那种情况下(正如评论中正确指出的那样),有可能第一次交易成功而第二次交易失败。如果是这种情况,您将不得不在系统中加入有关如何补偿这种不一致状态的逻辑。框架无法处理它,因为没有一个配方,它是针对每个案例的“处理”。一些想法,以防它们有所帮助:确保第一次交易后的系统状态清楚地表明第二次交易应该“很快”完成。例如。保留“第一笔交易提交于”字段;计划任务可以检查此时间戳并在过去太久时采取措施。 JMS 为您提供了所有这些 - 您可以重试和失败案例的死信队列。