【问题标题】:Should I pass a managed entity to a method that requires a new transaction?我应该将托管实体传递给需要新事务的方法吗?
【发布时间】:2016-07-26 22:50:02
【问题描述】:

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

@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);
    }
}

在我的HandlingService 中,使用REQUIRES_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.保持现在的代码。

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

2。从Scheduler中移除@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执行

您能否就解决此类问题的正确方法提供您的意见,也许是我不知道的另一种方法?

【问题讨论】:

    标签: java hibernate jpa spring-data spring-data-jpa


    【解决方案1】:

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

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

    这种方法也有很大的风险,因为如果你尝试在嵌套事务中访问Task 实例上未初始化的代理,可能会出现问题。如果您通过添加嵌套事务中加载的其他实体实例来更改嵌套事务中的Task 对象图,则可能会出现麻烦(因为当控件返回调度程序事务时,该实例现在将被分离)。

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

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

    【讨论】:

    • 在这个例子中,嵌套事务的会话实体缓存是否会与外部事务的会话同步?例如,如果“任务”实体在嵌套事务中发生更改,那么该更改是否也适用于 out 事务的会话?
    • 跟进我的问题,随着外部事务的会话被刷新,这是否意味着任务更改将被视为“陈旧”更改?
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      假设processTask(task);HandlingService 类中的一个方法(与handle(task) 方法相同),那么由于Spring 动态代理的自然行为,删除HandlingService 中的@Transactional 将不起作用。

      引用spring.io forum:

      当 Spring 加载您的 TestService 时,它​​会使用代理对其进行包装。如果您在 TestService 之外调用 TestService 的方法,则会改为调用代理,并且您的事务将得到正确管理。但是,如果您在同一对象的方法中调用事务方法,您将不会调用代理,而是直接调用代理的目标,并且您不会执行包装在服务周围的代码来管理事务。

      This is one of SO thread关于这个话题,这里有一些关于这个的文章:

      1. http://tutorials.jenkov.com/java-reflection/dynamic-proxies.html
      2. http://tutorials.jenkov.com/java-persistence/advanced-connection-and-transaction-demarcation-and-propagation.html
      3. http://blog.jhades.org/how-does-spring-transactional-really-work/

      如果你真的不喜欢在 @Scheduled 方法中添加 @Transaction 注释,你可以从 EntityManager 获取事务并以编程方式管理事务,例如:

      UserTransaction tx = entityManager.getTransaction();
      try {
        processTask(task);
        task.setStatus(Status.PROCCESED);
        tx.commit(); 
      } catch (Exception e) {
        tx.rollback();
      }
      

      但我怀疑你会采取这种方式(好吧,我不会)。最后,

      您能否就解决此类问题的正确方法发表您的意见

      在这种情况下没有正确的方法。我个人的看法是,注解(例如@Transactional)只是一个标记,你需要一个注解处理器(本例中为spring)来使@Transactional 工作。没有它的处理器,注释将完全没有影响。

      我会更担心,例如,为什么我有 processTask(task)task.setStatus(Status.PROCESSED); 住在 processTask(task) 之外,如果看起来做同样的事情,等等。

      HTH。

      【讨论】:

      • 即使 processTask 方法是 HandlingService 中的私有方法,所有更新都应该传播到数据库,因为在调用 handle 方法时打开了一个新事务(尽管弹簧代理)。在第二个变体中,我建议从调度程序中删除 @Transctional 并将 id`s 传递给处理服务(这将为每个处理的任务打开事务)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-12-29
      • 2015-10-28
      • 2014-07-05
      • 2011-07-21
      • 2012-11-18
      • 1970-01-01
      相关资源
      最近更新 更多