• Spring事务传播特性的浅析——事务方法嵌套调用的迷茫


    Spring事务传播机制回顾 


       Spring事务一个被讹传很广说法是:一个事务方法不应该调用另一个事务方法,否则将产生两个事务。结果造成开发人员在设计事务方法时束手束脚,生怕一不小心就踩到地雷。 
    其实这是不认识Spring事务传播机制而造成的误解,Spring对事务控制的支持统一在TransactionDefinition类中描述,该类有以下几个重要的接口方法: 

    • int getPropagationBehavior():事务的传播行为
    • int getIsolationLevel():事务的隔离级别
    • int getTimeout():事务的过期时间
    • boolean isReadOnly():事务的读写特性



       很明显,除了事务的传播行为外,事务的其他特性Spring是借助底层资源的功能来完成的,Spring无非只充当个代理的角色。但是事务的传播行为却是Spring凭借自身的框架提供的功能,是Spring提供给开发者最珍贵的礼物,讹传的说法玷污了Spring事务框架最美丽的光环。 
       
       所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring支持以下7种事务传播行为。 

    • PROPAGATION_REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务,就加入到这个事务中。这是最常见的选择。
    • PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
    • PROPAGATION_MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常。
    • PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起,就是不关外面有没有事务,都是新开启一个事务,和外面的事务是隔离的。
    • PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
    • PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
    • PROPAGATION_NESTED: 如果当前线程中没有事务, 则按照PROPAGATION_REQUIRED来执行; 如果当前线程中存在事务, 则开始一个 "嵌套的" 事务, 它是已经存在事务的一个真正的子事务.嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 嵌套事务将回滚到此 savepoint. 外部事务可通过配置或捕获内部事务抛出的Exception来决定是回滚还是继续往下执行. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交, 外部事务回滚则内部事务也会回滚, 不管内部事务有没有提交。



       Spring默认的事务传播行为是PROPAGATION_REQUIRED,它适合绝大多数的情况,如果多个ServiveX#methodX()均工作在事务环境下(即均被Spring事务增强),且程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这3个服务类的3个方法通过Spring的事务传播机制都工作在同一个事务中。 

    相互嵌套的服务方法 
       

       我们来看一下实例,UserService#logon()方法内部调用了UserService#updateLastLogon Time()和ScoreService#addScore()方法,这两个类都继承于BaseService。它们之间的类结构如下图所示:

       UserService#logon()方法内部调用了ScoreService#addScore()的方法,两者都分别通过spring AOP进行了事务增强,则它们工作于同一事务中。来看具体的代码: 

    [java] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. package com.baobaotao.nestcall;  
    2. …  
    3. @Service("userService")  
    4. public class UserService extends BaseService {  
    5.     @Autowired  
    6.     private JdbcTemplate jdbcTemplate;  
    7.   
    8.     @Autowired  
    9.     private ScoreService scoreService;  
    10.       
    11.     //①该方法嵌套调用了本类的其他方法及其他服务类的方法  
    12.     public void logon(String userName) {  
    13.         System.out.println("before userService.updateLastLogonTime...");  
    14.         updateLastLogonTime(userName);//①-1本服务类的其他方法  
    15.         System.out.println("after userService.updateLastLogonTime...");  
    16.           
    17.         System.out.println("before scoreService.addScore...");  
    18.         scoreService.addScore(userName, 20); //①-2其他服务类的其他方法  
    19.         System.out.println("after scoreService.addScore...");  
    20.   
    21.     }  
    22.     public void updateLastLogonTime(String userName) {  
    23.         String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";  
    24.         jdbcTemplate.update(sql, System.currentTimeMillis(), userName);  
    25.     }  


    UserService中注入了ScoreService的Bean,而ScoreService的代码如下所示: 

    [java] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. package com.baobaotao.nestcall;  
    2. …  
    3. @Service("scoreUserService")  
    4. public class ScoreService extends BaseService{  
    5.   
    6.     @Autowired  
    7.     private JdbcTemplate jdbcTemplate;  
    8.       
    9.     public void addScore(String userName, int toAdd) {  
    10.         String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";  
    11.         jdbcTemplate.update(sql, toAdd, userName);  
    12.     }  
    13. }  


      通过Spring配置为ScoreService及UserService中所有公有方法都添加Spring AOP的事务增强,让UserService的logon()和updateLastLogonTime()及ScoreService的addScore()方法都工作于事务环境下。下面是关键的配置代码: 

    [java] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. <?xml version="1.0" encoding="UTF-8" ?>  
    2. <beans xmlns="http://www.springframework.org/schema/beans"  
    3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    4.        xmlns:context="http://www.springframework.org/schema/context"  
    5.        xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop"  
    6.        xmlns:tx="http://www.springframework.org/schema/tx"  
    7.        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  
    8.     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">  
    9.     <context:component-scan base-package="com.baobaotao.nestcall"/>  
    10.      …  
    11.     <bean id="jdbcManager"  
    12.           class="org.springframework.jdbc.datasource.DataSourceTransactionManager"  
    13.           p:dataSource-ref="dataSource"/>  
    14.   
    15.     <!--①通过以下配置为所有继承BaseService类的所有子类的所有public方法都添加事务增强-->  
    16.     <aop:config proxy-target-class="true">  
    17.         <aop:pointcut id="serviceJdbcMethod"  
    18.                       expression="within(com.baobaotao.nestcall.BaseService+)"/>  
    19.         <aop:advisor pointcut-ref="serviceJdbcMethod" advice-ref="jdbcAdvice" order="0"/>  
    20.     </aop:config>  
    21.     <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">  
    22.         <tx:attributes>  
    23.             <tx:method name="*"/>  
    24.         </tx:attributes>  
    25.     </tx:advice>  
    26. </beans>  


       将日志级别设置为DEBUG,启动Spring容器并执行UserService#logon()的方法,仔细观察如下输出日志: 

    [plain] view plain copy
     
     print?在CODE上查看代码片派生到我的代码片
    1. before userService.logon method...   
    2.   
    3.      //①创建了一个事务   
    4. Creating new transaction with name [com.baobaotao.nestcall.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT   
    5. Acquired Connection [jdbc:mysql://localhost:3306/sampledb, UserName=root@localhost, MySQL-AB JDBC Driver] for JDBC transaction   
    6. Switching JDBC Connection [jdbc:mysql://localhost:3306/sampledb, UserName=root@localhost, MySQL-AB JDBC Driver] to manual commit   
    7. before userService.updateLastLogonTime...   
    8.   
    9.     <!--②updateLastLogonTime()和logon()在同一个Bean中,并未发生加入已存在事务上下文的   
    10.       动作,而是“天然”地工作于相同的事务上下文-->   
    11. Executing prepared SQL update   
    12. Executing prepared SQL statement [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]   
    13. SQL update affected 1 rows   
    14. after userService.updateLastLogonTime...   
    15. before scoreService.addScore...   
    16.   
    17. //③ScoreService#addScore方法加入到①处启动的事务上下文中   
    18. Participating in existing transaction   
    19. Executing prepared SQL update   
    20. Executing prepared SQL statement [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]   
    21. SQL update affected 1 rows   
    22. after scoreService.addScore...   
    23. Initiating transaction commit   
    24. Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/sampledb, UserName=root@localhost, MySQL-AB JDBC Driver]   
    25. …   
    26. after userService.logon method...   


        从上面的输出日志中,可以清楚地看到Spring为UserService#logon()方法启动了一个新的事务,而UserSerive#updateLastLogonTime()和UserService#logon()是在相同的类中,没有观察到有事务传播行为的发生,其代码块好像“直接合并”到UserService#logon()中。 
        然而在执行到ScoreService#addScore()方法时,我们就观察到发生一个事务传播的行为:" Participating in existing transaction ",这说明ScoreService#addScore()添加到UserService#logon()的事务上下文中,两者共享同一个事务。所以最终的结果是UserService的logon()、updateLastLogonTime()以及ScoreService的addScore都工作于同一事务中。 

    注:以上内容摘自《Spring 3.x企业应用开发实战》 

  • 相关阅读:
    龙东平:持续提升个人认知的三大底层逻辑
    【科创人独家】美信拓扑创始人一乐:如何登山不是最重要的问题,山峰才是
    【科创人独家】搜狐快站金庸:有情有义,90后技术创业者的问剑之路
    【科创人独家】军哥手记程军:我的2020,先打个60分吧
    【科创人独家】云风:从创业到招安,自由的游戏玩家+务实的程序员
    C语言--->指针
    位运算(一)
    平方根倒数快速算法
    IntelliJ IDEA 配置《算法》(第四版)
    深度学习(一):Python神经网络——手写数字识别
  • 原文地址:https://www.cnblogs.com/winkey4986/p/5801834.html
Copyright © 2020-2023  润新知