Spring Dao编程基础-事务传播行为

从 0 开始深入学习 Spring 专栏目录总览

学完了两种声明式事务的编写方式,接下来咱要学习 SpringFramework 事务中的另一个比较复杂的知识点:事务传播行为。这个概念乍听起来不大好理解,所以小册在这里先来解释一下它的含义。

1. 如何理解事务传播行为【理解】

简单的说,事务传播行为 = 事务的传播 + 传播的行为(听着像废话 2333 )。要了解它,就要先接触一种场景:事务的嵌套运行。

1.1 代码演绎事务的嵌套

我们可以先来用一段简单的代码来演绎一个场景:注册用户充值积分。用户在一个平台上注册后,会默认向用户充值一定数量的积分。相信会有不少小伙伴接触过这种场景,那咱用代码的形式简单编写一下。

首先来一个积分的 Service ,这里面我们就不搞 Dao 的那一套逻辑了,还要建表添加数据什么的,很麻烦,咱这里重在业务和设计的理解。

@Service
public class PointService {
    
    public void addPoint() {
        System.out.println("addPoint 添加积分 ......");
    }
}

然后是一个用户的 Service ,由于用户注册与充值积分是一个原子操作,所以这里需要在 UserService 中注入 PointService :

@Service
public class UserService {
    
    @Autowired
    PointService pointService;
    
    public void register() {
        // 持久化操作 ......
        System.out.println("register 注册用户 ......");
        pointService.addPoint();
    }
}

如果只是在 UserService 的 register 方法上标注 @Transactional 注解,这个代码的编写是没有任何问题的。但话又说回来,充值积分的动作也有可能很复杂,那 PointService 的 addPoint 方法自然也会添加事务。那既然 register 方法上有事务,addPoint 方法也有事务,这样就形成了事务的嵌套

1.2 传播行为的引入

事务的嵌套现象产生了,下面就出现一个问题:事务与事务之间,如何决定事务的行为?换一种说法,如果 addPoint 发现 register 方法声明了事务控制,它对此会持有什么态度?

emmm 貌似都不大好理解是吧,那没关系,我们再举例子。

情人节或者七夕的时候,你去找你女朋友 / 男朋友(可能你没有女朋友 / 男朋友,那抱歉打扰了),此时会有两种前提:你带了一支玫瑰花 / 你空着手去。咱先假设你带着玫瑰花去了,你的女朋友看到你带着玫瑰花后,可能会有如下几种反应:

Spring Dao编程基础-事务传播行为

Spring Dao编程基础-事务传播行为

Spring Dao编程基础-事务传播行为

如果你没有带玫瑰花,你的女朋友也可能产生以下反应:

Spring Dao编程基础-事务传播行为

Spring Dao编程基础-事务传播行为

有没有隐隐的产生一点感觉?如果将你和你女朋友看作两个 Service 的方法执行,玫瑰花代表你方法上的事务标识,则女朋友的行为会根据你是否持有玫瑰花(事务)而作出相应的反应。

回到代码中:

@Transactional
public void register() {
    // 持久化操作 ......
    System.out.println("register 注册用户 ......");
    pointService.addPoint();
}

如果 register 方法开启了事务,当执行 PointService 的 addPoint 方法时,是让它加入到当前事务呢?还是重新开一个事务?还是利用保存点的方案?等等等等,这些行为都是外层的事务传播到内层的事务后,内层的事务作出的行为(持有的态度),这就是事务传播行为。

2. 事务传播行为的7种策略【掌握】

事务的传播行为,可以由传播属性指定,根据 xml 配置文件,与声明式注解的方式不同,它们的声明方式有所不同,但策略都是同样的 7 种,下面咱先来学习这 7 种策略。

2.1 REQUIRED:必需的【默认值】

这是 SpringFramework 中事务传播行为的默认行为,它的定义是:如果当前没有事务运行,则会开启一个新的事务;如果当前已经有事务运行,则方法会运行在当前事务中。简单的概括:你没有,我开启;你有了,我加入

2.2 REQUIRES_NEW :新事务

新事务,顾名思义,它必须要一个全新的事务,那它的定义就可以描述为:如果当前没有事务运行,则会开启一个新的事务;如果当前已经有事务运行,则会将原事务挂起(暂停),重新开启一个新的事务。当新的事务运行完毕后,再将原来的事务释放。简单的概括:你没有,我开启;你有了,我造新的

2.3 SUPPORTS :支持

支持,这个词跟必需一对比,轻重程度一目了然。支持的定义是:如果当前有事务运行,则方法会运行在当前事务中;如果当前没有事务运行,则不会创建新的事务(即不运行在事务中)。很明显,支持更倾向于一种无所谓的态度,所以简单概括就是:有就有,没有拉倒

2.4 NOT_SUPPORTED :不支持

不支持,显然跟上面是完全相反的,它的定义是:如果当前有事务运行,则会将该事务挂起(暂停);如果当前没有事务运行,则它也不会运行在事务中。这态度更无所谓了,有事务它反而不稀罕,简单概括下就是:有我不要,没有正好

2.5 MANDATORY :强制

强制,听起来这个态度就很着急很强硬,它表示的意思是:当前方法必须运行在事务中,如果没有事务,则直接抛出异常。好家伙这也太凶了,如果当前方法执行的时候没有事务,它直接不干活了。所以咱简单概括下:要干活就必须有,没有就打死不干

2.6 NEVER :不允许

又是一对完全相反的设计,NEVER 定义的是:当前方法不允许运行在事务中,如果当前已经有事务运行,则抛出异常。这家伙跟上面的 MANDATORY 一个货色,只不过两个态度是完全相反的,它的简单概括是:要干活就不准有,有的话就不干活

2.7 NESTED :嵌套

这个 NESTED 是最特殊的,它就是基于保存点 SavePoint 的传播行为。它的定义是:如果当前没有事务运行,则开启一个新的事务;如果当前已经有事务运行,则会记录一个保存点,并继续运行在当前事务中。如果子事务运行中出现异常,则不会全部回滚,而是回滚到上一个保存点。可以发现,这个设计就是保存点的设计,所以简单概括就可以是:你没有,我开启,你有了,你记下;我走了,你再走,我挂了,就当无事发生

由于在 NESTED 的执行需要依赖关系型数据库的 SavePoint 机制,所以这种传播行为只适用于 DataSourceTransactionManager (即基于数据源的事务管理器)。

而且 NESTED 通常都在同一个数据源中实现,对于多数据源,或者分布式数据库的话,NESTED 是搞不定的(假设两个 Service 依赖的 Dao 分别操作不同的数据库,那实际上已经形成分布式事务了,Spring 搞不定的)。

2.8 小结

上面的 7 种传播行为,说实在的,用得最多的是前两种,中间的四种几乎用不到,最后一种嵌套事务也只能在单数据源下才有可能用得上,所以咱的重点要放在前两种。

3. 事务传播行为的使用【掌握】

接下来,咱实际的学习一下如何在 xml 配置文件,和事务注解中配置事务传播行为。

3.1 xml配置

上一章的声明式事务中,咱特意留了 propatation 属性放到这一章讲,咱这里就来写一下。

xml 配置文件中,绝大多数内容与上一章的 declarative-transaction.xml 文件一致,这里咱直接抄过整个配置文件吧,只需要改几个地方即可:

<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!-- 记得改方法名 -->
        <tx:method name="register" propagation="REQUIRED"/>
        <tx:method name="addPoint" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <!-- 记得改包名 -->
    <aop:advisor advice-ref="transactionAdvice"
                 pointcut="execution(* com.linkedbear.spring.transaction.e_spread.service.*.*(..))"/>
</aop:config>

然后,由于默认的 propagation 都是 REQUIRED ,咱是看不出效果的,所以咱可以给 addPoint 方法的传播行为改为 NEVER ,这样 register 方法带事务,进到 addPoint 中就应该抛出异常了。

<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="register" propagation="REQUIRED"/>
        <tx:method name="addPoint" propagation="NEVER"/>
    </tx:attributes>
</tx:advice>

改完之后,编写测试启动类,运行一把:

public class TransactionSpreadApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("tx/transaction-spread.xml");
        UserService userService = ctx.getBean(UserService.class);
        userService.register();
    }
}

运行 main 方法,控制台会抛出 IllegalTransactionStateException 不合法的事务状态异常

Exception in thread "main" org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'

可见事务传播行为已经生效了。

3.2 事务注解配置

注解声明式事务的配置方法就更简单了,只需要在方法的 @Transactional 注解上,声明 propagation 属性即可:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addPoint() {
    System.out.println("addPoint 添加积分 ......");
}

随后换用注解驱动,测试效果,可以发现效果是一样的。小伙伴们可以自行测试一把,小册就不带大家测了。

3.3 感知事务状态的办法【番外】

话说回来,如果是测试 REQUIRED 或者 REQUIRES_NEW 这样的传播行为,我们根本没有办法感知到是加入到已有的事务,还是开启了新的事务。不过好在 SpringFramework 给我们提供了一个 API 可以获取到当前事务开启的方法名:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addPoint() {
    System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
    System.out.println("addPoint 添加积分 ......");
}

利用 TransactionSynchronizationManager ,可以获取到当前事务的名称,而当前事务的名称就是开启事务时触发的方法。

同样的,咱在 UserService 的 register 方法中也加上 TransactionSynchronizationManager.getCurrentTransactionName() 的打印:

@Transactional
public void register() {
    System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
    // 持久化操作 ......
    System.out.println("register 注册用户 ......");
    pointService.addPoint();
}

此时配置的事务传播行为是:register – REQUIRED ,addPoint – REQUIRES_NEW 。

重新运行 main 方法,控制台打印了两个不同的方法名:

com.linkedbear.spring.transaction.e_spread.service.UserService.register
register 注册用户 ......
com.linkedbear.spring.transaction.e_spread.service.PointService.addPoint
addPoint 添加积分 ......

可见 addPoint 确实新开了一个事务,它没有参与到 UserService 的 register 方法开启的事务中。

然后,咱把 addPoint 方法中的 propagation 改为 REQUIRED ,之后再运行一次 main 方法:

com.linkedbear.spring.transaction.e_spread.service.UserService.register
register 注册用户 ......
com.linkedbear.spring.transaction.e_spread.service.UserService.register
addPoint 添加积分 ......

可以发现,这次 addPoint 方法所处的事务是与 register 方法的事务一致了,由此也体现出了 “你有了,我加入” 的传播行为特征。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring Dao编程基础-事务传播行为