目录标题
- 前言
- 一、DAO 和事务管理的牵绊
- 二、应用分层的迷惑
- 三、事务方法嵌套调用的迷茫(事务传播行为)
- 1.Spring 事务传播机制回顾
- 2.相互嵌套的服务方法
- 四、多线程的困惑
- 1. Spring 通过单实例化 Bean 简化多线程问题
- 2.启动独立线程调用事务方法
- 五、联合军种作战的混乱
- 1.Spring 事务管理器的应对
前言
汇总:《精通Spring4.x 企业应用开发实战》
一、DAO 和事务管理的牵绊
很少有使用 Spring 而不使用 Spring 事务管理器的应用,因此常常有人会问:是否用了 Spring, 就一定要用 Spring 事务管理器,否则就无法进行数据的持久化操作呢?事务管理器和 DAO 是什么关系呢?
也许是 DAO 和事务管理如影随形的缘故吧,这个看似简单的问题实实在在地存在着,从初学者心中涌出,萦绕在老把式的脑际。答案当然是否定的。我们都知道,事务管理的目的是保证数据操作的事务性(原子性、一致性、隔离性、持久性,即所谓的ACID)。脱离了事务,DAO 照样可以顺利地进行数据操作
。
二、应用分层的迷惑
1、Web、 Service 及 DAO 三层是Web 应用开发常见的模式,但有些开发人员却错误地认为: 如果要使用 Spring 的事务管理就一定要先进行三层的划分
,更有甚者认为每层一定要先定义一个接口,然后再定义一个实现类。
2、对将“面向接口编程” 奉为圭臬,认为放之四海而皆准的论调,笔者并不是很赞同。
3、是的,“面向接口编程”是 Martin Fowler、Rod Johnson 这些大师所提倡的行事原则。如果拿这条原则去开发框架、产品或大型项目,怎么强调都不为过。
4、但是,对于一般的开发人员来说,也许做的是一个普通工程项目,往往只是一些对数据库增、删、查、改的功能。此时,过分强制“面向接口编程”除了会带来更多的类文件,并不会有什么好处。
三、事务方法嵌套调用的迷茫(事务传播行为)
1.Spring 事务传播机制回顾
Spring 事务的一个被讹传很广的说法是:一个事务方法不应该调用另一个事务方法,否则将产生两个事务。结果造成开发人员在设计事务方法时束手束脚,生怕一不小心就踩到地雷。
其实这是未正确认识 Spring 事务传播机制而造成的误解。Spring 对事务控制的支持统一在TransactionDefinition 类中描述,该类有以下几个重要的接口方法。
● int getPropagationBehavior():事务的传播行为。
● int getlsolationLevel():事务的隔离级别。
● int getTimeout():事务的过期时间。
● boolean isReadOnly():事务的读/写特性。
很明显,除了事务的传播行为,对于事务的其他特性,Spring 是借助底层资源的功能
来完成的,Spring 无非充当了一个代理的角色
。但是事务的传播行为却是 Spring 凭借自身的框架提供的功能
,是 Spring提供给开发者最珍贵的礼物,讹传的说法玷污了 Spring事务框架最美丽的光环。
所谓事务传播行为,就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring 支持以下 7 种事务传播行为:
Spring 默认的事务传播行为是
PROPAGATION_ REQUIRED
,它适合绝大多数情况。如果多个 ServiveX#methodX()均工作在事务环境下(均被 Spring 事务增强),且程序中存在调用链 Service1#method1()->Service2#method2()->Service3# method3(),那么这3个服务类的3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。
2.相互嵌套的服务方法
线程同步场景下,UserService#logon()方法内部调用了 UserService#updateLastLogonTime()和 ScoreService#addScore()方法,这两个类都继承于 BaseService。它们之间的类结构如图 12-1 所示:
同一条线程下,UserService#logon()方法内部调用了 ScoreService#addScore()方法,二者分别通过Spring AOP 进行了事务增强,则它们工作在同一事务中。
同一个线程下,ScoreService#addScore()方法添加到了UserService#logon()方法的事务上下文中,二者共享同一个事务。所以最终的结果是UserService 的 logon()和 updateLastLogonTime()方法及 ScoreService 的 addScore()方法工作在同一个事务中。
具体验证代码,请看书或者语雀版。
四、多线程的困惑
1. Spring 通过单实例化 Bean 简化多线程问题
由于 Spring 的事务管理器是通过线程相关的 ThreadLocal
来保存数据访问基础设施(Connection 实例)的,再结合 IoC 和 AOP 实现高级声明式事务的功能,所以 Spring 的事务天然地和线程有着千丝万缕的联系
。
我们知道 Web 容器本身就是多线程的,Web 容器为一个 HTTP 请求创建一个独立的线程(实际上大多数 Web 容器采用共享线程池),所以由此请求所涉及的 Spring 容器中的 Bean 也运行在多线程环境下。在绝大多数情况下,Spring 的 Bean 都是单实例的(singleton), 单实例 Bean 的最大好处是线程无关性,不存在多线程并发访问的问题,也就是线程安全的。
一个类能够以单实例的方式运行的前提是“无状态”,即一个类不能拥有状态化的成员变量。我们知道,在传统的编程中,DAO 必须持有一个 Connection, 而 Connection就是状态化的对象(非线程安全)。所以传统的 DAO 不能做成单实例的,每次要用时都必须创建一个新的实例。传统的 Service 由于内部包含了若干有状态的 DAO 成员变量,所以其本身也是有状态的。
但在 Spring 中,DAO 和 Service 都以单实例的方式存在。Spring 通过 ThreadLocal将有状态的变量(如 Connection 等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring 不遗余力地将有状态的对象无状态化,就是要达到单实例化Bean 的目的。
由于 Spring 已经通过 ThreadLocal 的设施将 Bean 无状态化,所以 Spring 中的单实例 Bean 对线程安全问题拥有了一种天生的免疫能力。不但单实例的 Service 可以成功运行在多线程环境中,Service 本身还可以自由地启动独立线程以执行其他的 Service。
2.启动独立线程调用事务方法
下面对 logon()方法进行改造,让其在方法内部再启动一个新线程,在这个新线程中执行积分添加的操作,看看究竟会发生哪些事务行为,如代码清单12-12 所示。
package com.smart.multithread;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;
import org.apache.commons.dbcp.BasicDataSource;
/**
* @author 陈雄华
* @version 1.0
*/
@Service("userService")
public class UserService extends BaseService {
private JdbcTemplate jdbcTemplate;
private ScoreService scoreService;
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Autowired
public void setScoreService(ScoreService scoreService) {
this.scoreService = scoreService;
}
@Transactional
public void logon(String userName) {
System.out.println("before userService.updateLastLogonTime method...");
updateLastLogonTime(userName);
System.out.println("after userService.updateLastLogonTime method...");
// ①在同一个线程中调用scoreService#addScore(),将运行在同一个事务中
// scoreService.addScore(userName, 20);
//②在一个新线程中执行scoreService#addscore(),将启动一个新的事务
Thread myThread = new MyThread(this.scoreService, userName, 20);//使用一个新线程运行
myThread.start();
}
// ③负责执行scoreService#addscore () 的线程类
@Transactional
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);
}
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() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("before scoreService.addScor method...");
scoreService.addScore(userName, toAdd);
System.out.println("after scoreService.addScor method...");
}
}
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/multithread/applicatonContext.xml");
UserService service = (UserService) ctx.getBean("userService");
JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
//插入一条记录,初始分数为10
jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10," + System.currentTimeMillis() + ")");
//调用工作在无事务环境下的服务类方法,将分数添加20分
System.out.println("before userService.logon method...");
service.logon("tom");
System.out.println("after userService.logon method...");
jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在①处,在主线程(main)中执行的 UserService#logon0方法的事务启动;在②处,其对应的事务提交。而在子线程(Thread-2)中执行的 ScoreService#addScore()方法的事务在③处启动;在④处提交其对应的事务。
所以可以得出这样的结论:在相同线程中进行相互嵌套调用的事务方法工作在相同的事务中。如果这些相互嵌套调用的方法工作在不同的线程中,则不同线程下的事务方法工作在独立的事务中
。
五、联合军种作战的混乱
1.Spring 事务管理器的应对
让我想起来了在EDM中,基于原先的mybatis,然后我又整合了MongoDB。造成了mybatis事务失效。
Spring 抽象的 DAO 体系兼容多种数据访问技术,它们各有特色、各有干秋。如Hibernate 是一个非常优秀的 ORM实现方案,但对底层 SQL 的控制不太方便; 而 MyBatis则通过模板化技术让用户方便地控制 SQL,但没有 Hibernate 那样高的开发效率;自由度最高的当然是直接使用 Spring JDBC 了,但它也是底层的,灵活的代价是代码的繁复。很难说哪种数据访问技术是最优秀的,只有在某种特定的场景下才能给出答案。所以在一个应用中往往采用多种数据访问技术,一般是两种,一种采用 ORM 技术框架,而另一种采用偏 JDBC 的底层技术;二者珠联璧合,形成联合军种,共同御敌。
但是,这种联合军种如何应对事务管理的问题呢?我们知道,Spring 为每种数据访问技术提供了相应的事务管理器,难道需要分别为它们配置对应的事务管理器吗?它们到底是如何协同工作的呢?这些层出不穷的问题往往压制了开发人员使用联合军种的想法。
其实,在这个问题上,我们低估了 Spring 事务管理的能力。如果用户采用了一种高端的 ORM 技术(Hibernate、JPA、JDO),同时还采用了一种 JDBC 技术(Spring JDBC、MyBatis):由于前者的会话(Session)是对后者连接(Connection)的封装,Spring 会“足够智能地”在同一个事务线程中让前者的会话封装后者的连接。所以,只要直接采用前者的事务管理器就可以了。表12-1 给出了混合数据访问技术框架所对应的事务管理器。