Spring 事务管理高级应用难点剖析: 第 1 部分
?
在 ① 处,我们定义配置了 AnnotationMethodHandlerAdapter,以便启用 Spring MVC 的注解驱动功能。而②和③处通过 Spring 的 aop 及 tx 命名空间,以及 Aspject 的切点表达式语法进行事务增强的定义,对 MixLayerUserService 的所有公有方法进行事务增强。要使程序能够运行起来还必须进行 web.xml 的相关配置:
清单 7.web.xml
?
这个配置文件很简单,唯一需要注意的是 DispatcherServlet 的配置。默认情况下 Spring MVC 根据 Servlet 的名字查找 WEB-INF 下的 <servletName>-servlet.xml 作为 Spring MVC 的配置文件,在此,我们通过 contextConfigLocation 参数显式指定 Spring MVC 配置文件的确切位置。
将 org.springframework.jdbc 及 org.springframework.transaction 的日志级别设置为 DEBUG,启动项目,并访问 http://localhost:8088/logon.do?userName=tom 应用,MixLayerUserService#logon 方法将作出响应,查看后台输出日志:
清单 8 执行日志
?
具体的代码如下所示:
清单 9 UserService.java
?
将日志级别设置为 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 transactionlogon method...updateLastLogonTime... ②直接执行updateLastLogonTime方法16:25:04,781 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update16: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 rows16: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 update16: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 rows16: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 transaction16: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 transactionlogon 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 transactionaddScore...[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 存在哪些特殊的情况。