数据库事务的基础知识
事务在数据库上的展现就是:多条SQL语句,要么所有执行成功,要么所有执行失败。
数据库事务有严格的定义:它必须同时满足四个特性:
- 原子性(Atomic):表示组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的操作执行成功,整个事务才提交。数据中的任何一个数据库操作失败,已经执行的任何操作都必须撤销,让数据库返回初始状态。
- 一致性(Consistency):事务操作成功后,数据库所处的状态和它的业务规则是一致的,即数据不会被破坏。例如从A账户转账100元到B账户,不管操作成功与否,A账户和B账户的存款总额是不变的。
- 隔离性(Isolation):在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。准确的说,并非要求做到完全无干扰。数据库规定了多种事务隔离级别,不同的隔离级别对应不同的干扰程度,隔离级别越高,数据的一致性越好,但并发性越弱。
- 持久性(Durabiliy):一旦事务提交成功后,事务中所有的数据操作都必须被持久化到数据库中,即使在提交事务后,数据库马上崩溃,也必须保证能够通过某种机制恢复数据。
在这些事务的特性中,数据的“一致性”是最终的目标,其他的特性都是为了达到这个目标而采取的手段或措施。
数据库管理系统一般采用重执行日志来保证原子性、一致性和持久性。重执行日志记录了数据库变化的每一个动作,数据库在一个事务中执行一部分操作后发生错误退出,数据库即可根据重执行日志撤销已经执行的操作,此外,对于已经提交的事务,及时数据库崩溃,再重启数据库时,也能根据日志 对尚未持久化的数据进行响应的重执行操作。
和java程序采用对象锁机制进行线程同步类似,数据库管理系统采用数据库锁机制保证事务的隔离性,当多个事务对试图对相同的数据进行操作时,只有持有锁的事务才能操作数据,知道前一个事务完成后,后面的事务才有机会对数据进行操作。Orcale数据库还使用了数据版本机制,在回滚段为数据的每一个变化都保存一个版本,使数据更改不影响数据的读取。
数据并发问题
一个数组库可能拥有多个访问客户端,这些客户端都可以用并发的方式访问数据库,数据库中相同的数据可能同时被多个数据访问,如果没有采取必要的隔离措施会导致各种并发问题,破坏数据的完整性。这些问题可以归结为5类,其中3类数据问题(脏读、不可重复读、幻读)及两类数据更新问题(第一类丢失更新和第二类丢失更新)
Oracle数据库不会发生脏读的现象。
幻象读和不可重复读是两个容易混淆的概念,前者是指读到了其它已经提交事务的新增数据。
而后者表示读到了已经提交事务的更改数据(更改或删除)。为了避免这两种情况,采取的对策是不同的,阻止读到更改数据只需要操作数据添加行级锁。而防止读到新增数据,则往往需要添加表级锁,即将整个一张表锁定防止新增数据。(Oracle使用多版本数据的方式实现)。
数据库锁机制
数据并发会引起很多问题,在一些场合下有些问题是允许的,但在另一些场合下可能是致命的,数据库通过锁机制解决并发访问的问题,虽然不同数据库在实现的细节上,存在差别,但原理基本是一样的。
按锁定的对象不同,一般可以分为表锁定和行锁定,前者对整张表进行锁定,而后者对表中特定的行进行锁定,从并发事务锁定的关系来看,可以分为共享锁定和独占锁定,也防止其它的共享锁定。为了更改数据,为了更改数据,数据库必须在进行更改的行上施加行独占锁定,INSERT、UPDATE、DETELE和SELECT FOR UPDATE都会隐式采用必要的行锁定。
Oracle数据库常用的五种锁定
- 行共享锁定:行共享锁定并不防止对数据行进行修改操作,但是可以防止其它会话获取独占性数据表锁定,允许进行多个并发的行共享和行独占锁定,还允许进行数据表的共享或者采用共享行独占锁。
- 行独占锁定:这种锁可以防止其它会话获取一个共享锁定、共享行独占锁定、独占锁定。
- 表共享锁定:这种锁可以防止其它会话获取行独占锁,或者防止其它表共享行独占锁定或表独占锁定,但它允许在表中拥有多个行共享和表共享锁定,该锁定可以让会话具有对表事务级一致性,因为其它会话在用户提交或者回滚事务并释放对该表的锁定之前不能更改这张被锁定的表。
- 表共享行独占锁定:这种锁定可以防止其它会话获取一个表共享、行独占或者表独占锁定。但允许其它行共享锁定,这种锁定类似于表共享锁定,只是一次只能对一张表放置一个表共享行独占锁定,如果A对话拥有该锁定,则B对话可以执行SELECT FOR UPDATE操作,但如果B对话试图更新选择的行,则需要等待。
- 表独占锁定:这种锁定可以,放置其他会话对该表的任何其它锁定。
事务隔离级别
尽管数据库为用户提供了锁的DML操作方式,但直接使用锁管理是非常麻烦的,因此数据库为用户提供了自动锁机制,只要用户指定会话的事务隔离级别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源添加适合的锁。此外,数据库还会维护这些锁,当一个资源上的锁数目太多时,自动进行锁升级以提高系统的运行性能,而这一过程对用户来说是完全透明的。
有四个等级的标准事务级别,在相同的事务环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可能导致不同的结果,不同的事务隔离级别,能够解决的数据并发问题的能力是不同的。
事务隔离级别和数据库并发性是对立的
一般来说,使用READ UNCOMMITED隔离级别的数据库拥有最高的并发性和吞吐量,而使用SERIALIZABLE隔离级别的数据库并发性最低。
READ UNCOMMITED主要是为了提供非阻塞读的能力。Oracle虽然也支持READ UNCOMMITED,但它不支持脏读,因为Oracle使用多版本机制彻底解决了在非阻塞读时读到脏数据的问题并保证读的一致性,所以Oracle级别的READ UNCOMMITED就已经满足了REPEATABLE READ隔离级别。
推荐使用REPEATABLE READ事务级别以保证数据的一致性。不过用户也可以根据应用的需要选择适合的隔离等级。
JDBC对事务的支持
并不是所有的数据库都支持事务,即使支持事务的数据库也并非支持所有的事务隔离级别。
用户可以通过Connnection#getMetaData()方法获取DatabaseMetaData对象,并通过该对象的supportsTransactions()、supportsTransactionIsolationLevel(int level)方法查看底层数据库的事务支持情况。
Connection默认情况下是自动提交的,即每条执行的SQL语句都对应一个事务,为了将多条SQL语句当成一个事务执行,必须先通过Connection#setAutoCommit(false)阻止Connetion自动提交,并通过Connection#setTransactionIsolation()设置事务的隔离级别。Connection中定义了对应标准的四个事务隔离级别常量。通过Connection#commit()提交事务,通过Connection#rollback()回滚事务。
事务只有两个操作:提交和回滚,有些可能需要进行更多的事务控制,而不是简单的提交和回滚。
SavePoint接口允许用户将事务分割为多个阶段,用户可以指定回滚到事务的特定保存点。
并非所有事务都支持保存点功能,用户可以通过DatabaseMetaData#supportsSavepoints()方法查看是否支持。
ThreadLocal基础知识
前面我们知道,Spring通过各种模板类降低了开发者使用各种数据持久化技术难度,这些模板类都是线程安全的,也就是说,多个DAO可以复用同一模板实例而不会发生冲突,使用模板类访问底层数据,根据持久化技术不同,模板类需要绑定数据连接或会话资源。这些资源本身是非线程安全的。也就是说它们不能在同一时刻被多个线程共享,虽然模板类通过资源池获取数据连接或会话,但资源本身解决的是数据连接或会话缓存问题,并非数据库链接或会话的线程安全问题。
按照传统的经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步,但模板类并未采用线程同步机制,因为线程同步会降低并发性,影响系统性能。此外,通过代码同步解决线程安全的挑战性很大,可能会增加几倍的实现难度。
那么模板到底怎么实现线程安全的问题呢?答案就是TreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块中都出现了它的身影,要想了解Spring事务管理的底层技术,必须掌握TreadLocal。
ThreadLocal是什么?
TreadLocal为了解决多线程程序的并发问题提供了一种新的思路,使用这个工具类可以很简洁的编写出优美的多线程程序,线程局部变量并不是java的新发明,很多语言在语法层面就提供了线程局部变量。在java中没有提供语言级支持,而是以一种变通的方法,通过TreadLocal类提供支持。所以在java中编写线程局部变量代码相对来说要笨拙一些,这也是为什么线程局部变量没有在java开发者中得到很好普及的原因。
ThreadLocal它不是一个线程,而是保存线程本地化对象的容器,当运行于多线程环境的某个对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本,所以每个线程都可以独立的改变自己的副本,而不会影响其它线程所对应的副本,从线程角度上看,这个变量就像线程专有的本地变量,这也是类名中Local所要表达的意思。
InheritableTreadLocal继承于TreadLocal它自动为子线程复制一份从父线程那里继承而来的本地变量:在创建子线程时,子线程会接收所有可继承的线程本地变量的初始值,当必须将本地线程变量自动传送给所有创建的子线程时,应尽可能的使用InheritableTreadLocal,而非ThreadLocal。
ThreadLocal的接口方法:
ThreadLocal类接口很简单,只有四个方法:
- void set(Object value):设置当前线程的线程局部变量的值。
- public Object get():返回当前线程所对应的线程局部变量。
- public void remove():将当前线程局部变量删除,目的是为了减少内存的占用。需要指出的是:当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显示调用该方法清除线程的局部变量并不是必要的操作,但它可以加快内存回收机制。
- protected Object initialValue:返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的,这个方法是一个延时调用方法,在线程第一次调用get()或set(Object)时才执行,并且仅执行一次。
ThreadLocal中的默认实现直接返回一个null。
ThreadLocal新版也是支持泛型T的。
ThreadLocal是如何做到为每个线程维护一份独立的变量副本呢,其实实现思路很简单,在TreadLocal类有一个Map,用户存储每个线程变量的副本,Map中元素的键为线程对象,值为对应线程变量的副本。
与Thread同步机制的比较
ThreadLoacl和线程同步机制都是为了解决多线程中相同变量的访问冲突问题,那么,ThreadLoacl和线程同步机制相比有什么优势?
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序缜密的分析什么时候对变量进行读或写,什么时候需要锁定某个对象、什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLoacl从另一个角度来解决多线程的并发访问,ThreadLoacl为每个线程提供了一个独立的变量副本,从而隔离的多个线程对访问数据的冲突,因为每个线程都拥有自己的变量副本,因而也就没有必要对对该变量进行同步,ThreadLoacl提供了线程安全的对象封装,在编写多线程代码时,可以把不安全的变量封装进ThreadLoacl中。
概括而言,对于多线程 资源共享问题,同步机制采用了“以时间换空间”的方式,访问串行化,对象共享化,而ThreadLoacl采用以空间换时间的方式,访问并行化,对象独享化。前者仅提供一份变量,让不同的线程排队访问,而后者为每个线程都提供了一份变量,因此可以同时访问互不影响。
Spring采用ThreadLoacl解决线程安全问题
在一般情况下,只有无状态的bean,才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,正是因为Spring对一些Bean(如RequestContextHolder)中,非线程安全的状态性对象,采用ThreadLoacl进行封装,让它们成为线程安全的状态性对象,因此,有状态的bean就能够以singleton的方式在多线程中正常工作。
一般的WEB应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。从接收请求到返回响应所经过的所有程序调用都属于一个线程。
在学习Java的时候经常会发现有很多名称相似的类,比如HashMap和Hashtable,StringBuffer和StringBuilder等等,他们的名称相似,功能也有相似的地方,所以初学者在学习之中往往会很疑惑,他们都有哪些不同呢?而在深入研究这个问题的时候,就会发现他们都有这样两个概念,线程安全和线程不安全,这也就是本文主要讲的内容。
1.什么是线程安全和线程不安全呢?
线程安全是指多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性。
线程不安全就是不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
2.是什么决定的线程安全问题呢?
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
3.有哪些可以解决多线程并发访问资源的安全问题呢?
有三种方式:分别是 同步代码块 、同步方法和锁机制(Lock)
- (1)同步代码块:
synchronized(同步锁)
{
//方法体
}- (2)同步方法:给多线程访问的成员方法加上synchronized修饰符
public synchronized void test(){
//方法体
}
以上两种该方法都用到了Java语言的关键字synchronized,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
(3)锁机制(Lock)
//Java提供的同步代码块的另一种机制,比synchronized关键字更强大也更加灵活。
//这种机制基于Lock接口及其实现类(例如:ReentrantLock)
//它比synchronized关键字好的地方:
//1、提供了更多的功能。tryLock()方法的实现,这个方法试图获取锁,
//如果锁已经被其他线程占用,它将返回false并继续往下执行代码。
//2、Lock接口允许分离读和写操作,允许多个线程读和只有一个写线程。
//3、具有更好的性能
public class PrintQueue {
private final Lock A=new ReentrantLock();
//…
}
4、线程的同步:
当两个或两个以上的线程需要共享资源,他们需要某种方法来确定资源在某一刻仅被一个线程占用,达到此目的的过程叫做同步(synchronization)。
同步代码块:synchronized(对象){},将需要同步的代码放在大括号中,括号中的对象即为锁。
同步函数:放于函数上,修饰符之后,返回类型之前。
5、wait和sleep的区别:(执行权和锁区分)
wait:可指定等待的时间,不指定须由notify或notifyAll唤醒。
线程会释放执行权,且释放锁。
sleep:必须制定睡眠的时间,时间到了自动处于临时(阻塞)状态。
即使睡眠了,仍持有锁,不会释放执行权。
这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象访问的同一ThreadLocal变量都是当前线程所绑定的。
public class Connect {
//一个非线程安全变量
private Connection conn;
public void addTopic() throws SQLException {
//引用非线程安全变量
Statement state=conn.createStatement();
}
}
conn是非线程安全的成员变量,因此addTopic()方法也是非线程安全的,所以必须在使用时创建一个新的
Connect实例(非singleton)
用ThreadLocal修改后的:
public class Connect {
//用ThreadLocal保存Connection变量
private static ThreadLocal<Connection> connThreadLoacl=
new ThreadLocal<Connection>();
public static Connection getConnection() {
//如果connThreadLoacl没有本线程对应的Connection,
//则创建一个新的Connection,并将其保存到线程本地变量中
if(connThreadLoacl.get()==null) {
Connection conn=ConnectionManager.getConnection();
connThreadLoacl.set(conn);
return conn;
}else {
return connThreadLoacl.get();
}
}
//从ThreadLocal中获取线程对应的Connection
public void addTopic() throws SQLException {
Statement state=getConnection().createStatement();
}
}
Spring对事务管理的支持
Spring为事务管理提供了一致的编程模板,在高层次建立了统一的事务抽象,也就是说,不管是选择SpringJDBC、Hibernate、JPA还是选择Mybatis,Spring都可以让用户用统一的编程模型,进行事务管理。
像Spring DAO为不同的持久化实现提供了模板类一样,Spring事务管理继承了这一风格,也提供了事务模板TranscationTemplate。通过TranscationTemplate并配合使用事务回调TranscationCallback指定具体的持久化操作,就可以通过编程的方式,实现事务管理,而无需关注资源获取、复用、释放、事务同步和异常处理等操作。
Spring事务管理的亮点在于声明式事务管理,Spring允许通过声明方式,在Ioc配置中指定事务的边界和事务的属性,Spring自动在指定的事务边界上应用事务属性。
EJB事务建立在JTA的基础上,而JTA又必须通过JNDI获取。这意味着不管用户的应用是跨数据源的应用,还是单数据源的应用,EJB都要求使用全局事务的方式加以处理。即基于EJB的应用无法脱离应用服务器所提供的容器环境,这种不加区分一概而论的做法无异于杀鸡杀牛都用一把刀。
Spring深刻的意识到大部分应用都是基于单数据源的,只有为数不多的应用需要使用多数据源的JTA事务。因此在单数据源的情况下,Spring直接使用底层的数据源管理事务,只有在面对多数据源的应用时,Spring才寻求javaEE应用服务器的支持,通过引用应用服务器中的JNDI资源完成JTA事务,Spring让人印象深刻的地方在于,不管用户使用何种持久化技术,也不管用户是否使用了JTA事务,都可以采用相同的事务管理模型,这种统一的处理方式所带来的好处是不可估量的用户完全可以抛开事务管理的问题编写程序,并在Spring中通过配置完成事务的管理工作。
事务管理关键抽象
在Spring事务管理SPI的抽象层主要包括三个接口,分别是:PlatformTransactionManager、TransactionDefinition和TransactionStatus,它们位于org.springframework.transaction包中。
TransactionDefinition用于描述事务的隔离级别、超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性,这些事务属性可以通过XML配置或注解描述提供,也可以通过手工编程的方式设置。
PlatformTransactionManager根据TransactionDefinition提供的事务属性配置信息创建事务,并用TransactionStatus描述这个激活事务的状态。
TransactionDefinition
TransactionDefinition定义了Spring兼容的事务属性,这些属性对事务管理控制的若干方面进行配置
- 事务隔离:当前事务和其它事务的隔离程度,共四个隔离级别,之前讲过。TransactionDefinition还定义了一个默认的隔离级别,ISOLATION_DEFAULT,它表示使用底层数据库的默认隔离级别。
- 事务传播:通常在一个事务中,执行的所有代码都会运行于同一个事务上下文中,但Spring也提供了可选的事务类型。
- 事务超时:事务在超时前能运行多久,超过时间后事务被回滚,有些事务管理器不支持事务过期的功能,这时,如果设置TIMEOUT_DEFAULT之外的其它值,则将抛出异常。
- 只读事务:不修改任何数据,资源事务管理者可以针对只读事务应用一些优化措施,提高运行性能,只读事务在某些情况下是一种非常有用的优化,试图在只读事务中更改数据将引发异常。
TransactionStatus
TransactionStatus代表一个事务的具体运行状态。事务管理器可以通过该接口获取事务运行期的状态信息,也可以通过该接口间接的回滚事务。它相比于在抛出异常时回滚事务的方式更具有可控性。该接口继承于SavepointManager接口
PlatformTransactionManager
Spring事务管理器的实现
Spring将事务管理委托给底层具体的持久化实现框架来完成,因此,Spring为不同的持久层框架提供了PlatformTransactionManager接口的实现类。
这些事务管理器都是对特定事务实现框架的代理,这样就可以通过Spring所提交的高级抽象对不同种类的事务实现使用相同的方式进行管理,而不用关心具体的实现。
要实现事务管理首先要在Spring中配置好相应的数据管理器,为数据管理器指定数据资源及一些其它事务管理控制属性。
Spring JDBC和MyBatis
如果要使用MyBatis或SpringJDBC,由于他们都是基于数据源的,Connection访问数据库,所以可以使用,DataSourceTranscationManager,只要在Spring中进行如下配置就可以:
<!-- 定义一个使用DBCP实现的数据源 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="${driverClassName}"
p:url="${url}"
p:username="${username}"
p:password="${password}" />
<!-- 定义JDBC模板Bean -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"
p:dataSource-ref="dataSource" />
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource="dataSource"/>
在幕后,DataSourceTransactionManager使用DataSource的Connection的commit()/rollback()
等方法管理事务。
JPA
JPA通过javax.persistence.EntityTransaction管理JPA事务,EntityTransaction对象可以通过javax.persistence.EntityManager#getTransaction方法获得,而EntityManager又通过一个工厂类方法javax.persistence.EntityManagerFactory#createEntityManager()获取。
在底层JPA依然通过JDBC的Connection的事务方法完成最终的控制。因此要配置一个JPA事务管理器,必须先提供一个DataSource,然后配置一个EntityManagerFactory,最后才配置JpaTransactionManager。
Hibernate
大部分ORM框架都拥有自己事务管理的API,它们对DataSource和Connection进行了封装,Hibernate使用org.hibernate.Session封装Connection,所以需要一个能够创建Session的SessionFactory。
JTA
如果希望在javaEE容器里面使用JTA,则将通过JNDI和Spring的JtaTransactionManager获取一个容器管理的DataSource。JtaTransactionManager不需要知道DataSource和其它特定的资源,因为它引用提供的全局事务管理。
事务同步管理器
Spring将JDBC的Connection、Hibernate的Session等访问数据库的连接或会话对象统称为资源,这些资源在同一时刻是不能多线程共享的。为了让DAO、Service类可能做到singleton,Spring的事务同步管理器类SynchronizationManager使用ThreadLoacl为不同事务线程提供了独立的资源副本,同时维护事务的属性和运行的状态信息。事务同步管理器是Spring事务管理的基石,不管用户使用的是编程式事务管理,还是声明式事务管理,都离不开事务同步管理器。
Spring框架为不同的持久化技术提供了一套从TransactionSynchronizationManager中获取对应线程绑定资源的工具类。
这些工具类都提供了静态方法,通过这些方法可以获取和当前线程绑定的资源,如DataSourceUtils.getConnection(DataSource dataSource)方法可以从指定的数据源中获取和当前线程绑定的Connection,而Hibernate的SessionFactoryUtils.getSession(SessionFactory sessionFactory,boolean allowCreate)方法则可以从指定的SessionFactory中获取和当前线程绑定的Session。
当需要脱离模板类,手工操作底层持久化技术的原生API时,就需要通过这些工具类获取线程绑定的资源,而不应该直接从DataSource或SessionFactory中获取。因为后者不能获得与本线程相关的资源,因此无法让数据操作参与到与本线程相关的事务环境中。
Spring为不同的持久化技术提供模板类,模板类在内部通过资源获取工具类间接访问TransactionSynchronizationManager中的线程绑定资源,所以,如果DAO使用模板类进行持久化操作,这些DAO就可以配置成singleton,如果不使用模板类,也可以直接通过资源获取工具类访问线程相关资源。
TransactionSynchronizationManager将DAO、Service类中影响线程安全的所有状态统一抽取到类中,并用ThreadLocal进行替换,从此DAO(必须是基于模板类或资源获取工具类创建的DAO)和Service(必须采用Spring事务管理机制)摘掉了非线程安全的帽子。完成了身份转变。
事务传播行为
当我们调用一个基于Spring的Service接口方法(如UserService#addUser())时,它将运行于Spring管理的事务环境中,Service方法可能会在内部调用其它的Service接口方法以共同完成一个完整的业务操作。因此就会产生服务接口方法嵌套调用的情况,Spring通过事务传播行为控制当前的事务如何传播到被嵌套调用的目标服务方法中。
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为:
编程式的事务管理
在实际应用中很少使用编程来进行事务管理,即便如此,Spring还是为编程式事务管理提供了模板类org.springframework.transaction.support.TransactionTemplate,以满足一些特殊场合的需要。
TransactionTemplate和那些持久化模板类一样是线程安全的,因此可以在多个业务中共享TransactionTemplate实例进行事务管理。TransactionTemplate拥有多个设置事务属性的方法如setReadOnly(boolean readOnly)等。
TransactionTemplate有两个主要的方法:
void setTransactionManager(PlatformTransactionManager transactionManager)设置事务管理器。
Object execute(TransactionCallback action):在TransactionCallback 回调接口中定义需要以事务方式组织的数据访问逻辑。
TransactionCallback 接口只有一个方法:
Object doInTransaction(TransactionStatus status).如果操作不会返回结果,则可以使用TransactionCallback的子接口TransactionCallbackWithoutResult。
由于Spring事务管理基于TransactionSynchronizationManager进行工作,所以如果在回调接口方法中,需要显式访问底层数据连接,则必须通过资源获取工具类得到线程绑定的数据连接。这是Spring事务管理的底层协议,不容违反。如果ForumDao是基于Spring提供的模板类构建的,由于模板类已经在内部使用了资源获取工具类获取数据库连接,所以用户就不必关心底层数据连接的连接问题。
使用XML配置声明式事务
大多数用户选择声明式事务管理的功能,这种方式对代码的侵入性最小,可以让事务管理代码,完全从业务代码中移除,非常符合非侵入式轻量级容器的概念。
Spring的声明式事务管理是通过SpringAOP实现的,通过事务的声明性信息,Spring试图将事务管理增强逻辑动态织入业务方法的响应连接点中,这些逻辑包括,获取线程绑定资源,、开始事务、提交/回滚事务、进行异常转换和处理等工作。
一个将被实施事务增强的服务接口
BbtForum拥有四个方法,我们希望addTopic()和updateForum()方法拥有写事务的能力,而其他两个方法只需要拥有读事务的能力。
BbtForum只是一个简单的POJO,这里只是简单的使用持久层DAO类,通过他们的协作实现业务能力,在这里我们看不到任何事务操作代码,所以直接使用BbtForum,那么这些方法都将以无事务的方式运行,现在我们的任务是通过Spring声明式事务配置让这些业务方法拥有适合的事务功能。
使用原始的TransactionProxyFactoryBean