Spring @Transactional 只读传播

99

我正在尝试使用命令模式,在单个事务的上下文中允许我的Web层与Hibernate实体一起工作(从而避免懒加载异常)。然而,我现在对如何处理事务感到困惑。

我的命令调用带有 @Transactional 注释的服务层方法。其中一些服务层方法是只读的 - 例如 @Transactional(readOnly = true) - 而另一些则是读/写。

我的服务层公开了一个命令处理程序,代表Web层执行传递给它的命令。

@Transactional
public Command handle(Command cmd) throws CommandException

我认为,在命令处理程序的 handle 方法上加上事务是正确的做法,但这就导致了混淆。如果一个命令的实现调用了多个服务层方法,那么命令处理程序就无法知道在命令内部调用的操作是只读、读写还是两者的组合。

我不理解在这个例子中传播如何工作。如果我将 handle() 方法标记为 readOnly = true,那么如果命令调用了一个使用 @Transactional(realOnly = false) 注解的服务层方法,会发生什么?


2
那么这两个相互矛盾的答案中哪一个是真的呢?有人费心去检查过吗? - LuGo
1
由于handle()可能调用写入方法,因此事务必须允许写入。这将是一个很好且正确的解决方案。如果您真的想要,可以通过Command的属性来编程启动TX并切换为readOnly,但我认为这不值得努力。 - Thomas W
4个回答

120
首先,由于Spring本身不执行持久性操作,因此它无法确定readOnly应该具体意味着什么。这个属性只是对提供程序的暗示,行为取决于,在这种情况下,Hibernate。
如果将readOnly指定为true,则当前Hibernate Session中的刷新模式将设置为FlushMode.NEVER,从而防止Session提交事务。
此外,还会在JDBC连接上调用setReadOnly(true),这也是对底层数据库的提示。如果您的数据库支持它(很可能是这样),那么它基本上具有与FlushMode.NEVER相同的效果,但更强大,因为您甚至不能手动刷新。
现在让我们看看事务传播如何工作。
如果您没有明确将readOnly设置为true,则会拥有读/写事务。根据事务属性(例如REQUIRES_NEW),有时候您的事务会在某个点被挂起,启动一个新的事务,最终提交,然后恢复第一个事务。
好了,我们快到了目的地。让我们看看readOnly如何在此方案中发挥作用。
如果在一个读/写事务中的方法调用需要readOnly事务的方法,则应该挂起第一个事务,否则会在第二个方法结束时发生flush/commit。
相反,如果您从readOnly事务中调用需要读/写的方法,则再次挂起第一个事务,因为它无法刷新/提交,而第二个方法需要它。
在readOnly-to-readOnly和read/write-to-read/write的情况下,外部事务不需要被挂起(除非您明确指定传播方式)。

5
你确定吗?“只读”是否真的会覆盖指定的传播策略?我很难找到相关的参考资料,但至少在这篇文章中找到了相反的说法:http://imranbohoran.blogspot.ch/2011/01/spring-transactions-readonly-whats-it.html - Pierre Henry
37
如果你调用一个具有只读属性的bean,然后这个bean调用另一个具有读写属性的bean,那么不会启动新的事务,第二个bean参与现有的只读事务,并且第二个bean所做的更改不会被提交。 - dan carter
14
错误 - 如@dancarter所说,在readOnly事务中调用的read/write方法会在Spring的Hibernate集成中静默失败,因为最外层的TX拦截器是只读的,Hibernate会话永远不会刷新...也不会执行任何SQL更新。这是使用默认传播属性的情况 - 您可以尝试REQUIRES_NEW,但它并不适用于大多数场景。正确 - 如@dancarter所述,在readOnly事务中调用的read/write方法会在Spring的Hibernate集成中静默失败。由于最外层的事务拦截器为readOnly,Hibernate会话从未被刷新,也不会执行任何SQL更新(这是使用默认的传播属性)。虽然您可以尝试使用REQUIRES_NEW,但对于大多数情况来说,这不是正确的解决方案。 - Thomas W
3
你在这里完全错误,"since Spring doesn't do persistence itself, it cannot specify what readOnly should exactly mean."。实际上,Spring是事务管理器,因此Spring可以并且确实指定readOnly的确切含义。@Transactional注解本身就是Spring的注解。作为API提供者和实现者,Spring有权指定该API的确切含义。 - dan carter
6
你已经对答案进行了一些修改,但我仍然认为它含糊不清且误导性很强。 “反过来,如果你在一个只读事务中调用需要读写的方法,那么第一个事务会被挂起,”不是这样的。相反,第二个读/写注释方法将参与现有的只读事务。事务不会自动暂停。只有当你告诉Spring使用REQUIRES_NEW来暂停它们时,它们才会暂停。 - dan carter
显示剩余2条评论

42

如果从readOnly=true调用readOnly=false,则无法正常工作,因为上一个事务将继续。

在您的示例中,服务层的handle()方法正在启动新的读写事务。如果handle方法反过来调用带有readonly注解的服务方法,则readonly不会起作用,因为它们将参与现有的读写事务。

如果这些方法对于只读操作至关重要,则可以使用Propagation.REQUIRES_NEW注释它们,然后它们将开始一个新的只读事务,而不是参与现有的读写事务。

这是一个可行的示例,CircuitStateRepository是Spring Data JPA存储库。

BeanS调用一个transactional=read-only Bean1,它执行查找并调用transactional=read-write Bean2保存一个新对象。

  • Bean1开始一个只读事务。

31 09:39:44.199 [pool-1-thread-1] DEBUG o.s.orm.jpa.JpaTransactionManager - 使用名称[nz.co.vodafone.wcim.business.Bean1.startSomething] 创建新事务: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly; ''

  • Bean2参与其中。

    31 09:39:44.230 [pool-1-thread-1] DEBUG o.s.orm.jpa.JpaTransactionManager - 参与现有事务

    没有向数据库提交任何内容。

现在将Bean2的@Transactional注解更改为添加propagation=Propagation.REQUIRES_NEW

  • Bean1开始一个只读事务。

    31 09:31:36.418 [pool-1-thread-1] DEBUG o.s.orm.jpa.JpaTransactionManager - 使用名称[nz.co.vodafone.wcim.business.Bean1.startSomething] 创建新事务: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly; ''

  • Bean2开始一个新的读写事务

    31 09:31:36.449 [pool-1-thread-1] DEBUG o.s.orm.jpa.JpaTransactionManager - 暂停当前事务,使用名称[nz.co.vodafone.wcim.business.Bean2.createSomething] 创建新事务

现在由Bean2所做的更改已提交到数据库。

这是一个使用Spring Data、Hibernate和Oracle测试过的示例。

@Named
public class BeanS {    
    @Inject
    Bean1 bean1;

    @Scheduled(fixedRate = 20000)
    public void runSomething() {
        bean1.startSomething();
    }
}

@Named
@Transactional(readOnly = true)
public class Bean1 {    
    Logger log = LoggerFactory.getLogger(Bean1.class);

    @Inject
    private CircuitStateRepository csr;

    @Inject
    private Bean2 bean2;

    public void startSomething() {    
        Iterable<CircuitState> s = csr.findAll();
        CircuitState c = s.iterator().next();
        log.info("GOT CIRCUIT {}", c.getCircuitId());
        bean2.createSomething(c.getCircuitId());    
    }
}

@Named
@Transactional(readOnly = false)
public class Bean2 {    
    @Inject
    CircuitStateRepository csr;

    public void createSomething(String circuitId) {
        CircuitState c = new CircuitState(circuitId + "-New-" + new DateTime().toString("hhmmss"), new DateTime());

        csr.save(c);
     }
}

6
很棒的回答,但我认为你应该按以下方式进行总结,因为回答的格式会导致一些初始混淆。
  1. 从readOnly=true调用readOnly=false是行不通的,因为先前的事务将继续。
  2. 调用(propagation = Propagation.REQUIRES_NEW)从readOnly=true可以工作,因为创建了一个新的事务。
- HopeKing

16

默认情况下,事务传播是REQUIRED,这意味着相同的事务将从事务调用者传播到事务被调用者。在这种情况下,只读状态也会传播。例如,如果一个只读事务调用了一个读写事务,则整个事务将变为只读。

你可以使用Open Session in View模式来实现延迟加载。这样一来,你的handle方法就不需要事务处理了。


1
正如@sijk所说,只读状态向内传播--没有警告或任何诊断,为什么Hibernate不提交:( - Thomas W
我正在使用jpa+hibernate+spring,在只读事务调用读写事务并且所有操作都在读写事务中的情况下,被持久化的实体已经提交,但是通过getter/setter修改的实体没有被提交。相当令人困惑。 - Lars Juel Jensen

6
它似乎忽略了当前活动事务的设置,它只将设置应用于新事务:
org.springframework.transaction.PlatformTransactionManager TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException 根据指定的传播行为返回当前活动的事务或创建一个新事务。 请注意,像隔离级别或超时之类的参数仅适用于新事务,并且在参与活动事务时将被忽略。 此外,并非每个事务管理器都支持所有事务定义设置:适当的事务管理器实现应在遇到不受支持的设置时引发异常。 上述规则的一个例外是只读标志,如果不支持显式只读模式,则应忽略它。 实质上,只读标记只是潜在优化的提示。

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