事务方法嵌套调用的迷茫
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#updateLastLogonTime() 和 ScoreService#addScore() 方法,这两个类都继承于 BaseService。它们之间的类结构说明如下:
图 1. UserService 和 ScoreService
具体的代码如下所示:
清单 9 UserService.java
@Service("userService")
public class UserService extends BaseService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ScoreService scoreService;
public void logon(String userName) {
updateLastLogonTime(userName);
scoreService.addScore(userName, 20);
}
public void updateLastLogonTime(String userName) {
String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
}
}
UserService 中注入了 ScoreService 的 Bean,ScoreService 的代码如下所示:
清单 10 ScoreService.java
@Service("scoreUserService")
public class ScoreService extends BaseService{
@Autowired
private JdbcTemplate jdbcTemplate;
public void addScore(String userName, int toAdd) {
String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";
jdbcTemplate.update(sql, toAdd, userName);
}
}
通过 Spring 的事务配置为 ScoreService 及 UserService 中所有公有方法都添加事务增强,让这些方法都工作于事务环境下。下面是关键的配置代码:
清单 11 事务增强配置
<!-- 添加Spring事务增强 -->
<aop:config proxy-target-class="true">
<aop:pointcut id="serviceJdbcMethod"
<!-- 所有继承于BaseService类的子孙类的public方法都进行事务增强-->
expression="within(user.nestcall.BaseService+)"/>
<aop:advisor pointcut-ref="serviceJdbcMethod"
advice-ref="jdbcAdvice" order="0"/>
</aop:config>
<tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">
<tx:attributes>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
将日志级别设置为 DEBUG,启动 Spring 容器并执行 UserService#logon() 的方法,仔细观察如下的输出日志:
清单 12 执行日志
16:25:04,765 DEBUG (AbstractPlatformTransactionManager.java:365) -
Creating new transaction with name [user.nestcall.UserService.logon]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①为UserService#logon方法启动一个事务
16:25:04,765 DEBUG (DataSourceTransactionManager.java:205) -
Acquired Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]
for JDBC transaction
logon method...
updateLastLogonTime... ②直接执行updateLastLogonTime方法
16:25:04,781 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update
16:25:04,781 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement
[UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]
16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows
16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:470) - Participating
in existing transaction ③ScoreService#addScore方法加入到UserService#logon的事务中
addScore...
16:25:04,828 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update
16:25:04,828 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement
[UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]
16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows
16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:752) -
Initiating transaction commit
④提交事务
16:25:04,828 DEBUG (DataSourceTransactionManager.java:265) - Committing JDBC transaction
on Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]
16:25:04,828 DEBUG (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
[org.apache.commons.dbcp.PoolableConnection@32bd65] after transaction
16:25:04,828 DEBUG (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource
从上面的输入日志中,可以清楚地看到 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 的事务管理器是通过线程相关的 ThreadLocal 来保存数据访问基础设施,再结合 IOC 和 AOP 实现高级声明式事务的功能,所以 Spring 的事务天然地和线程有着千丝万缕的联系。
我们知道 Web 容器本身就是多线程的,Web 容器为一个 Http 请求创建一个独立的线程,所以由此请求所牵涉到的 Spring 容器中的 Bean 也是运行于多线程的环境下。在绝大多数情况下,Spring 的 Bean 都是单实例的(singleton),单实例 Bean 的最大的好处是线程无关性,不存在多线程并发访问的问题,也即是线程安全的。
一个类能够以单实例的方式运行的前提是“无状态”:即一个类不能拥有状态化的成员变量。我们知道,在传统的编程中,DAO 必须执有一个 Connection,而 Connection 即是状态化的对象。所以传统的 DAO 不能做成单实例的,每次要用时都必须 new 一个新的实例。传统的 Service 由于将有状态的 DAO 作为成员变量,所以传统的 Service 本身也是有状态的。
但是在 Spring 中,DAO 和 Service 都以单实例的方式存在。Spring 是通过 ThreadLocal 将有状态的变量(如 Connection 等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring 不遗余力地将状态化的对象无状态化,就是要达到单实例化 Bean 的目的。
由于 Spring 已经通过 ThreadLocal 的设施将 Bean 无状态化,所以 Spring 中单实例 Bean 对线程安全问题拥有了一种天生的免疫能力。不但单实例的 Service 可以成功运行于多线程环境中,Service 本身还可以自由地启动独立线程以执行其它的 Service。下面,通过一个实例对此进行描述:
清单 13 UserService.java 在事务方法中启动独立线程运行另一个事务方法
@Service("userService")
public class UserService extends BaseService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ScoreService scoreService;
//① 在logon方法体中启动一个独立的线程,在该独立的线程中执行ScoreService#addScore()方法
public void logon(String userName) {
System.out.println("logon method...");
updateLastLogonTime(userName);
Thread myThread = new MyThread(this.scoreService,userName,20);
myThread.start();
}
public void updateLastLogonTime(String userName) {
System.out.println("updateLastLogonTime...");
String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
}
//② 封装ScoreService#addScore()的线程
private class MyThread extends Thread{
private ScoreService scoreService;
private String userName;
private int toAdd;
private MyThread(ScoreService scoreService,String userName,int toAdd) {
this.scoreService = scoreService;
this.userName = userName;
this.toAdd = toAdd;
}
public void run() {
scoreService.addScore(userName,toAdd);
}
}
}
将日志级别设置为 DEBUG,执行 UserService#logon() 方法,观察以下输出的日志:
清单 14 执行日志
[main] (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name
[user.multithread.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①
[main] (DataSourceTransactionManager.java:205) - Acquired Connection
[org.apache.commons.dbcp.PoolableConnection@1353249] for JDBC transaction
logon method...
updateLastLogonTime...
[main] (JdbcTemplate.java:785) - Executing prepared SQL update
[main] (JdbcTemplate.java:569) - Executing prepared SQL statement
[UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]
[main] (JdbcTemplate.java:794) - SQL update affected 0 rows
[main] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit
[Thread-2](AbstractPlatformTransactionManager.java:365) -
Creating new transaction with name [user.multithread.ScoreService.addScore]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT ②
[main] (DataSourceTransactionManager.java:265) - Committing JDBC transaction
on Connection [org.apache.commons.dbcp.PoolableConnection@1353249] ③
[main] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
[org.apache.commons.dbcp.PoolableConnection@1353249] after transaction
[main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource
[Thread-2] (DataSourceTransactionManager.java:205) - Acquired Connection
[org.apache.commons.dbcp.PoolableConnection@10dc656] for JDBC transaction
addScore...
[main] (JdbcTemplate.java:416) - Executing SQL statement
[DELETE FROM t_user WHERE user_name='tom']
[main] (DataSourceUtils.java:112) - Fetching JDBC Connection from DataSource
[Thread-2] (JdbcTemplate.java:785) - Executing prepared SQL update
[Thread-2] (JdbcTemplate.java:569) - Executing prepared SQL statement
[UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]
[main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource
[Thread-2] (JdbcTemplate.java:794) - SQL update affected 0 rows
[Thread-2] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit
[Thread-2] (DataSourceTransactionManager.java:265) - Committing JDBC transaction
on Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] ④
[Thread-2] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
[org.apache.commons.dbcp.PoolableConnection@10dc656] after transaction
在 ① 处,在主线程(main)执行的 UserService#logon() 方法的事务启动,在 ③ 处,其对应的事务提交,而在子线程(Thread-2)执行的 ScoreService#addScore() 方法的事务在 ② 处启动,在 ④ 处对应的事务提交。
所以,我们可以得出这样的结论:在 相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果这些相互嵌套调用的方法工作在不同的线程中,不同线程下的事务方法工作在独立的事务中。
回页首
小结
Spring 声明式事务是 Spring 最核心,最常用的功能。由于 Spring 通过 IOC 和 AOP 的功能非常透明地实现了声明式事务的功能,一般的开发者基本上无须了解 Spring 声明式事务的内部细节,仅需要懂得如何配置就可以了。
但是在实际应用开发过程中,Spring 的这种透明的高阶封装在带来便利的同时,也给我们带来了迷惑。就像通过流言传播的消息,最终听众已经不清楚事情的真相了,而这对于应用开发来说是很危险的。本系列文章通过剖析实际应用中给开发者造成迷惑的各种难点,通过分析 Spring 事务管理的内部运作机制将真相还原出来。
在本文中,我们通过剖析了解到以下的真相:
在没有事务管理的情况下,DAO 照样可以顺利进行数据操作;
将应用分成 Web,Service 及 DAO 层只是一种参考的开发模式,并非是事务管理工作的前提条件;
Spring 通过事务传播机制可以很好地应对事务方法嵌套调用的情况,开发者无须为了事务管理而刻意改变服务方法的设计;
由于单实例的对象不存在线程安全问题,所以进行事务管理增强的 Bean 可以很好地工作在多线程环境下。
在 下一篇 文章中,笔者将继续分析 Spring 事务管理的以下难点:
混合使用多种数据访问技术(如 Spring JDBC + Hibernate)的事务管理问题;
进行 Spring AOP 增强的 Bean 存在哪些特殊的情况。
分享到:
相关推荐
在定义方法时,一个方法内不能再定义另一个方法,即不能嵌套定义,但是在调用一个方法的过程中,还可以调用另一个方法,这是方法的嵌套调用。 方法的嵌套调用 假设main方法中调用a方法,a 方法中调用b方法,具体流程...
2. **递归方法(含嵌套调用)**:在这种方法中,我们使用函数自身来调用自身,直到达到基本情况(通常是n=1)。这种方法虽然直观,但可能会导致栈溢出,尤其是对于大数的阶乘。以下是一个使用嵌套调用的阶乘函数示例...
Dll调用与嵌套调用 一.Win32动态链接库 1.制作的步骤: (1)新建WIN32 Dynamic-link Library工程,工程名为MyDll,选择A simple DLL project类型。 (2)MyDll.h的内容如下: 以下是引用片段: extern "C" _...
Oracle 数据完整性嵌套事务调用分析研究 Oracle 数据库中,数据完整性是指数据的正确性、完整性和一致性。为了保护数据的完整性,我们可以使用多种方法,例如数据表的主键约束、外键约束、触发器等等。在处理数据...
WINCE 下 dll 嵌套调用 a.exe里面调用b.dll 然后再 b.dll调用c.dll 不过指针传递还未成功 谁成功了告诉下我 谢谢
数据库的嵌套调用,让初学者很好的学习数据库的使用和掌握一些调用方法
C语言函数的嵌套调用和递归调用 本文主要介绍C语言函数的嵌套调用和递归调用,包括函数的递归调用、变量的作用域和存储类型等知识点。 函数的递归调用 函数的递归调用是指函数直接或间接地自我调用的一种调用方式...
C语言函数的嵌套调用和递归调用学习教案.pptx
程序设计-函数的嵌套调用 函数的嵌套调用是程序设计中的一种重要概念。根据C语言规定,函数的定义不能嵌套,也就是说,在一个函数内不能定义其他函数,但是函数的调用可以嵌套。 函数的嵌套调用可以让程序呈现出...
### Java方法调用详解 #### 一、方法调用概览 在Java编程语言中,方法(也称为函数)是程序的基本构建块之一,用于封装特定功能以便在需要时重复使用。方法调用是实现这一功能的关键步骤。通过调用方法,我们可以...
springboot mybatis多数据源加事务嵌套 事务之间的调用 回滚 亲测可用 定义2个库分别建立 CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户编号', `user_name` varchar(25) ...
通过分析和运行这些示例,开发者可以更直观地了解和掌握动态库嵌套调用的实践方法。 总结来说,C/C++的动态库嵌套调用是一项强大的技术,它使得软件组件能够灵活地交互和扩展。通过正确地创建、加载和管理动态库,...
当我们遇到"AOP实现自我调用的事物嵌套问题"时,这通常涉及到Spring框架中的事务管理,特别是自调用方法在事务处理时可能会引发的问题。 首先,让我们理解Spring AOP的事务管理是如何工作的。Spring使用代理模式来...
本教学单元将深入探讨两个关键概念——函数的嵌套调用和递归调用,它们是C语言高级编程技巧的基础。 1. **函数的嵌套调用**: 函数嵌套调用是指在一个函数的执行过程中调用了另一个函数。这种调用方式使得程序结构...
2. 开启嵌套事务:在已经开启的事务中,你可以再次调用`Tx.Begin()`来创建一个新的子事务。子事务将继承父事务的上下文,这意味着如果父事务回滚,所有的子事务也会回滚。 3. 提交和回滚:在嵌套事务中,每个子事务...
UG对话框多重调用和嵌套调用在模具CAD中的应用.pdf.crdownload
本章主要探讨了函数的两种特殊调用方式:嵌套调用和递归调用,特别在C语言程序设计的上下文中。 1. **函数的嵌套调用**:在C语言中,虽然函数不能定义在另一个函数内部,但可以在一个函数的执行过程中调用另一个...
在小学信息技术课程中,"过程的嵌套调用"是一个重要的概念,它涉及到编程思维的培养和逻辑结构的理解。本节课主要目的是让学生理解什么是过程的嵌套调用,并能熟练运用这一技巧进行图形绘制。 1. **过程(Procedure...
C语言函数的嵌套调用和递归调用 本学习教案主要介绍了C语言函数的嵌套调用和递归调用,包括函数的递归调用概念、变量的作用域和存储类型等知识点。 一、函数的递归调用 递归函数是指函数直接或间接地自我调用的...