0028-多个事务互相调用

本文详细解析了Spring框架中的事务传播行为,介绍了7种不同的事务传播类型及其应用场景,特别是通过具体示例展示了PROPAGATION_REQUIRED行为下,不同服务方法如何在同一个事务中协同工作。
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类似的操作。




   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进行了事务增强,则它们工作于同一事务中。来看具体的代码:


 01 <SPAN style="FONT-SIZE: 14px; COLOR: #006600; FONT-FAMILY: Microsoft YaHei" minmax_bound="true">package com.baobaotao.nestcall; 


02 … 


03 @Service("userService") 


04 public class UserService extends BaseService { 


05     @Autowired


06     private JdbcTemplate jdbcTemplate; 


07   


08     @Autowired


09     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     }</SPAN>


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


 01 <SPAN style="FONT-SIZE: 14px; COLOR: #006600; FONT-FAMILY: Microsoft YaHei" minmax_bound="true">package com.baobaotao.nestcall; 


02 … 


03 @Service("scoreUserService") 


04 public class ScoreService extends BaseService{ 


05   


06     @Autowired


07     private JdbcTemplate jdbcTemplate; 


08       


09     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 }</SPAN>


  通过Spring配置为ScoreService及UserService中所有公有方法都添加Spring AOP的事务增强,让UserService的logon()和updateLastLogonTime()及ScoreService的 addScore()方法都工作于事务环境下。下面是关键的配置代码:
 01 <SPAN style="FONT-SIZE: 14px; COLOR: #006600; FONT-FAMILY: Microsoft YaHei" minmax_bound="true"><?xml version="1.0" encoding="UTF-8" ?> 


02 <beans xmlns="http://www.springframework.org/schema/beans"


03        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"


04        xmlns:context="http://www.springframework.org/schema/context"


05        xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop"


06        xmlns:tx="http://www.springframework.org/schema/tx"


07        xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-3.0.xsd


08     http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-3.0.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-3.0.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> 


09     <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></SPAN>


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


引用
before userService.logon method...


     //①创建了一个事务
Creating new transaction with name [com.baobaotao.nestcall.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Acquired Connection [jdbc:mysql://localhost:3306/sampledb, UserName=root@localhost , MySQL-AB JDBC Driver] for JDBC transaction
Switching JDBC Connection [jdbc:mysql://localhost:3306/sampledb, UserName=root@localhost , MySQL-AB JDBC Driver] to manual commit
before userService.updateLastLogonTime...


    <!--②updateLastLogonTime()和logon()在同一个Bean中,并未发生加入已存在事务上下文的
      动作,而是“天然”地工作于相同的事务上下文-->
Executing prepared SQL update
Executing prepared SQL statement [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]
SQL update affected 1 rows
after userService.updateLastLogonTime...
before scoreService.addScore...


//③ScoreService#addScore方法加入到①处启动的事务上下文中
Participating in existing transaction
Executing prepared SQL update
Executing prepared SQL statement [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]
SQL update affected 1 rows
after scoreService.addScore...
Initiating transaction commit
Committing JDBC transaction on Connection [jdbc:mysql://localhost:3306/sampledb, UserName=root@localhost , MySQL-AB JDBC Driver]

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都工作于 同一事务中。
<think>嗯,用户的问题是询问同一个类中事务方法调用另一个事务方法导致事务失效的原因。从提供的参考资料来看,这个问题与Spring的事务代理机制密切相关。用户似乎已经查阅过相关资料,但需要更清晰的解释。 用户可能正在开发Spring应用时遇到了事务不生效的问题,通过错误现象反推原因。ta的引用内容都指向同一个核心机制:Spring的AOP代理在类内部方法调用时不生效。这属于Spring事务管理的经典陷阱,很多中级开发者都会踩坑。 从技术层面看,这个问题涉及三个关键点: 1. Spring事务依赖于动态代理(JDK Proxy或CGLib) 2. 代理对象只拦截外部调用 3. 类内部方法调用绕过代理机制 需要特别注意用户引用的[^3]提到的"无事务方法调用事务方法"场景,这是最常见的失效情况。比如一个未注解的public方法A调用带@Transactional注解的方法B时,实际执行的是this.B()而不是proxy.B(),导致事务切面未被触发。 考虑到用户已经看到过代理机制的描述,解释时需要更强调"为什么代理会被绕过"。可以比喻为:代理对象就像包裹原始对象的包装盒,外部调用先经过盒子(触发事务),而内部方法调用相当于直接从盒子内部拿东西(跳过事务控制)。 另外用户可能隐含的需求是:既然知道原因,如何解决?虽然当前问题没直接问解决方案,但在回答末尾可以稍作延伸,为后续问题铺垫。比如简要提到"通过自注入"或"拆分Service类"等规避方式。 </think>同一个类中事务方法调用另一个事务方法导致事务失效的根本原因在于 **Spring 的事务管理机制基于代理实现**,具体原理如下: ### 关键原因分析 🔍 1. **代理机制限制** Spring 通过动态代理(JDK Proxy 或 CGLib)管理事务。当调用 `@Transactional` 方法时: ```mermaid graph LR A[外部调用] --> B[代理对象] --> C[开启事务] --> D[执行目标方法] ``` 但**同一个类内的方法互相调用**属于 `this.method()` 的内部调用,直接操作目标对象而非代理对象,导致事务拦截器被绕过[^1][^3]。 2. **事务切面失效场景** ```java @Service public class OrderService { // 事务方法A @Transactional public void methodA() { methodB(); // 直接内部调用事务失效 } // 事务方法B @Transactional(propagation = Propagation.REQUIRES_NEW) public void methodB() { // 数据库操作 } } ``` 此时 `methodA` 调用 `methodB` 时: - ❌ 未通过代理对象 - ❌ `REQUIRES_NEW` 不会启动新事务 -事务注解完全失效 3. **底层原理** Spring 事务本质是 AOP 环绕增强: ```java // 伪代码:事务代理逻辑 public Object transactionProxy(MethodInvocation invocation) { TransactionStatus tx = beginTransaction(); // 开启事务 try { Object result = invocation.proceed(); // 执行目标方法 commitTransaction(tx); // 提交事务 return result; } catch (Exception e) { rollbackTransaction(tx); // 回滚事务 throw e; } } ``` **内部调用直接跳过了该代理层**,导致事务控制代码未执行[^2][^3]。 ### 解决方案思路 💡 1. **依赖注入自身代理**(推荐) ```java @Service public class OrderService { @Autowired // 注入自身代理对象 private OrderService selfProxy; public void methodA() { selfProxy.methodB(); // 通过代理调用 } @Transactional public void methodB() { ... } } ``` 2. **拆分Service类** 将事务方法拆分到不同类中,强制走代理机制。 3. **使用 AspectJ 模式** 配置 `@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)` 绕过代理限制(需织入支持)。 --- ### 📚 相关问题 1. **如何在同一个类中让事务方法调用生效?** 2. **Spring 的 `PROPAGATION_REQUIRES_NEW` 在哪些场景会失效?** 3. **AspectJ 与 JDK 动态代理实现事务有何本质区别?** 4. **事务失效的常见场景有哪些?如何系统排查?** [^1]: 事务失效的核心在于代理机制被绕过 [^2]: 内部调用使用 `this` 而非代理对象导致切面失效 [^3]: `@Transactional` 依赖代理对象触发事务控制逻辑
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值