是否应将托管实体传递给需要新事务的方法?

我的应用程序加载应处理的实体列表。这发生在使用调度程序的类中

@Component
class TaskScheduler {

    @Autowired
    private TaskRepository taskRepository;

    @Autowired
    private HandlingService handlingService;

    @Scheduled(fixedRate = 15000)
    @Transactional
    public void triggerTransactionStatusChangeHandling() {
        taskRepository.findByStatus(Status.OPEN).stream()
                               .forEach(handlingService::handle);
    }
}

在我的流程中,每个任务都在使用传播级别。HandlingServiceREQUIRES_NEW

@Component
class HandlingService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handle(Task task) {
        try {
            processTask(task); // here the actual processing would take place
            task.setStatus(Status.PROCCESED);
        } catch (RuntimeException e) {
            task.setStatus(Status.ERROR);
        }
    }
}

代码之所以有效,只是因为我在类上启动了父事务。如果我删除注释,则不再管理实体,并且对任务实体的更新不会传播到数据库。我发现使计划方法成为事务性方法并不自然。TaskScheduler@Transactional

从我所看到的,我有两个选择:

1. 保持代码不变。

  • 也许这只是我,这是一个正确的aproach。
  • 此变体具有最少的数据库访问次数。

2. 从计划程序中删除@Transactional注释,传递任务的 ID,然后在 HandlingService 中重新加载任务实体。

@Component
class HandlingService {

    @Autowired
    private TaskRepository taskRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handle(Long taskId) {
        Task task = taskRepository.findOne(taskId);
        try {
            processTask(task); // here the actual processing would take place
            task.setStatus(Status.PROCCESED);
        } catch (RuntimeException e) {
            task.setStatus(Status.ERROR);
        }
    }
}
  • 具有更多数据库行程(一个额外的查询/元素)
  • 可以使用以下命令执行@Async

你能不能就哪种是解决这类问题的正确方法发表意见,也许用另一种我不知道的方法?


答案 1

如果您打算在单独的事务中处理每个任务,那么您的第一种方法实际上不起作用,因为所有内容都在调度程序事务结束时提交。

这样做的原因是,在嵌套事务中,实例基本上是分离的实体(在嵌套事务中启动的实例不知道这些实例)。在计划程序事务结束时,休眠对托管实例执行脏检查,并将更改与数据库同步。TaskSession

这种方法也非常危险,因为如果您尝试访问嵌套事务中实例上的未初始化代理,则可能会遇到麻烦。如果通过向嵌套事务中添加一些在嵌套事务中加载的其他实体实例来更改嵌套事务中的对象图,则可能会遇到麻烦(因为当控件返回到计划程序事务时,该实例现在将被分离)。TaskTask

另一方面,您的第二种方法是正确而直接的,有助于避免上述所有陷阱。只是,我会读取id并提交事务(在处理任务时无需将其挂起)。实现它的最简单方法是从调度程序中删除注释,并使存储库方法具有事务性(如果它已经不是事务性的)。Transactional

如果(并且仅当)第二种方法的性能是一个问题,正如您已经提到的,您可以使用异步处理,甚至可以在某种程度上并行化处理。另外,您可能想看看扩展会话(对话),也许您会发现它适合您的用例。


答案 2

当前代码处理嵌套事务中的任务,但更新外部事务中任务的状态(因为 Task 对象由外部事务管理)。由于这些是不同的事务,因此一个事务可能成功,而另一个事务失败,从而使数据库处于不一致的状态。特别是,使用此代码,如果处理另一个任务引发异常,或者服务器在处理完所有任务之前重新启动,则已完成的任务将保持状态打开状态。

如您的示例所示,将托管实体传递给另一个事务会使哪个事务应更新这些实体变得模糊不清,因此最好避免使用。相反,您应该传递 ID(或分离的实体),并避免不必要的事务嵌套。


推荐