9.1数据库事物基础知识
9.1.1 何为数据库事务
- 数据库事物有严格的定义,它必须满足4个特性:原子性、一致性、隔离性和持久性:
- 原子性:表示组成一个事务的多个数据库操作是一个不可分割的原子单位,只有所有的操作执行成功,整个事物才提交,事物中任何一个数据库操作失败,已经执行的任何操作都必须撤销,让数据库返回到初始状态。
- 一致性:事务操作成功后,数据库所处的状态和它的业务规则是一致的,即数据不会被破坏。如从A账户转账100元到B账户,不管操作成功与否,A和B的存款总额是不变的。
- 隔离性:在并发数据操作时,不同的事物拥有各自的数据空间,他们的操作不会对对方产生干扰。准确地说,并非要求做到完全无干扰,数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性越好,但并发越弱。
- 持久性:一旦事务提交后,事务中所有的数据操作都必须被持久化到数据库中,即使数据库提交事务后,数据库马上奔溃,在数据库重启时,也必须保证能够通过某种机制恢复数据。
- 在这些事务特性中,数据“一致性”是最终目标,其他的特性都是为达到这个目标的措施、要求或手段。
- 数据库管理系统一般采用重执行日志保证原子性、一致性和持久性,日志记录了数据库变化的每一个动作,数据库在一个事务中执行一部分操作后发生错误退出,数据库即可根据重执行日志撤销已经执行的操作。此外,对于已经提交的事务,即使数据库奔溃,在重启数据库时也能够根据日志对尚未持久化的数据进行相应的重执行操作。
- 和Java程序采用对象锁机制进行线程同步类似,数据库管理系统采用数据库锁机制保证事务的隔离性。当多个事务视图对相同的数据进行操作时,只有持有锁定事务才能操作数据,直到前一个事务完成后,后面的事务才有机会对数据进行操作。Oracle数据库还使用了数据版本机制,在回滚段为数据的每一个变化都保存一个版本,使数据的变更不影响数据的读取。
9.1.2 数据并发的问题
- 一个数据库可能拥有多个访问客户端,这些客户端都可用并发的方式访问数据库。数据库中相同数据可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性。包括3类读问题(脏读、不可重复读、幻读);2类数据更新问题(第一类丢失更新和第二类丢失更新)
脏读
- A 事务读取 B事务尚未提交的更改数据,并在这个数据的基础上操作。如果恰巧 B事务回滚,那么 A事务读到的数据根本是不被承认的。
时间 | 转账事务 A | 取款事务 B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为 1000元 | |
T4 | 取出 500 元把余额改为 500元 | |
T5 | 查询账户余额为 500元(脏读) | |
T6 | 撤销事务余额恢复为 1000 元 | |
T7 | 汇入 100元把余额改为 600元 | |
T8 | 提交事务 |
- 在次场景,B希望取款 500元而后又撤销了动作,而 A往相同的账户中转账 100元,就因为 A事务读取了 B事务尚未提交的数据,因而造成账户白白丢失了500元。在Oracle数据库中,不会发生脏读的情况。
不可重复度
- 不可重复读是指 A事务读取了 B事务已提交的更改数据。假设 A在取款事务过程中,B往该账户转账 100元,A两次读取的账户余额发生不一致。
时间 | 转账事务 A | 取款事务 B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为 1000元 | |
T4 | 查询账户余额为 1000元 | |
T5 | 取出 100元把余额改为 900元 | |
T6 | 提交事务 | |
T7 | 查询账户余额为 900元(和T4读取的不一致) |
- 同一事务中,T4时间点和 T7时间点读取账户存款余额不一样。
幻象读
- A事务读取 B事务提交的新增数据,这时 A事务将出现幻象读的问题。幻象读一般发生在计算统计数据事务中。假设银行系统在同一事务中,两次统计存款账户的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时,两次统计的总金额将不一致;
时间 | 转账事务 A | 取款事务 B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计总存款数为 1000元 | |
T4 | 新增一个存款账户,存款为 100元 | |
T5 | 提交事务 | |
T6 | 在次统计总存款数为 10100元(幻想读) |
- 如果新增数据刚好满足事务查询条件,这个新数据就进入了事务的视野,因而产生了两个统计不一致的情况。
- 幻象读和不可重复读是两个容易混淆的概念,前者是指读到了其他已经提交事务的新增数据。而后者是读到了已经提交事务的更改数据(更改或删除),为了避免在这两种情况,采取的对策是不同的,防止读取到更改数据,只需要对操作的数据添加行级锁,阻止操作中的数据发生变化,而防止读取到新增数据,则往往需要添加表级锁–将整个表锁定,防止新增数据。
第一类丢失更新
- A 事务撤销时,把已经提交的 B事务的更新数据覆盖了。这种错误可能造成很严重的问题,通过下面的账户取款转账就可以看出来
时间 | 转账事务 A | 取款事务 B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为 1000元 | |
T4 | 查询账户余额为 1000元 | |
T5 | 汇入 100元把余额改为1100元 | |
T6 | 提交事务 | |
T7 | 取出 100元把余额改为 900元 | |
T8 | 撤销事务 | |
T9 | 余额恢复为 1000元(丢失更新) |
- A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。
第二类丢失更新
- A事务覆盖 B事务已经提交的数据,造成 B事务所做操作丢失;
时间 | 转账事务 A | 取款事务 B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为 1000元 | |
T4 | 查询账户余额为 1000元 | |
T5 | 取出 100元把余额改为 900元 | |
T6 | 提交事务 | |
T7 | 汇入100元 | |
T8 | 提交事务 | |
T9 | 打余额改为 1100元(丢失更新) |
- 上面的例子里由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,相反如果转账事务先提交,那么用户账户将损失 100元。
9.1.3 数据库锁机制
- 按锁定的对象的不同,一般可以分为表锁定和行锁定,前者对整个表进行锁定,而后者对表中特定行进行锁定。从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。共享锁定会防止独占锁定,但允许其他的共享锁定。而独占锁定即防止其他的独占锁定,也防止其他的共享锁定。为了更改数据,数据库必须进行更改的行上施加独占行锁定,INSERT、UPDATE、DELETE和SELECT FOR UPDATE 语句都会隐式采用必要的行锁定。
- Oracle数据库常用的5种锁定
- 行共享锁定:一般通过 SELECT FOR UPDATE 语句隐式获得行共享锁定,在Oracle中用户也可以通过LOCK TABLE IN ROW SHARE MODE语句显示获得行共享锁定。行共享锁定并不防止对数据进行更改的操作,但是可以防止其他会话获取独占数据表锁定。 允许进行多个并发的行共享和独占行锁定,还允许进行数据表的共享或者采用共享行独占锁定 。
- 行独占锁定:通过一条 INSERT、UPDATE或DELETE语句隐式获取,或者通过一条LOCK TABLE IN ROW EXCLUSIVE MODE语句显示获取。这个锁定可以防止其他会话获取一个共享锁定、共享行独占锁定或独占锁定。
- 表共享锁定:通过 LOCK TABLE IN SHARE MODE语句显示获得。这种锁定可以防止其他会话获取行独占锁定(INSERT、UPDATE或DELETE),或者防止其他表共享行独占锁定或表独占锁定,它允许在表中拥有多个行共享和表共享锁定。该锁定可以让会话具有对表事务级一致性访问,因为其他会话在用户提交或者回滚该事务并释放对该表的锁定之前不能更改这个被锁定的表。
- 表共享行独占:通过 LOCK TABLE IN SHARE ROW EXCLUSIVE MODE语句显示获得。这种锁定可以防止其他会话获取一个表共享、行独占或者表独占锁定,它允许其他行共享锁定。这个种锁定类似于表共享锁定,只是一次只能对一个表放置一个表共享行独占锁定。如果A会话拥有该锁定,则B会话可以执行 SELECT FOR UPDATE操作,但如果B会话试图更新选择的行,则需要等待。
- 表独占: 通过LOCK TABLE IN EXCLUSIVE MODE显示获得。这个锁定防止其他会话对该表的任何其他锁定。
9.1.4 事务隔离级别
- ANSI/ISO SQL标准定义了4个等级的事务隔离级别,在相同数据环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可以导致不同的结果。不同事务隔离级别能够解决的数据并发问题的能力是不同的;
隔离级别 | 脏读 | 不可重复读 | 幻象读 | 第一类丢失更新 | 第二类丢失更新 |
---|---|---|---|---|---|
READ UNCOMMITED | 允许 | 允许 | 允许 | 不允许 | 允许 |
READ COMMITTED | 不允许 | 允许 | 允许 | 不允许 | 允许 |
REPEATABLE READ | 不允许 | 不允许 | 允许 | 不允许 | 不允许 |
SERIALIZABLE | 不允许 | 不允许 | 不允许 | 不允许 | 不允许 |
- 事务的隔离级别和数据并发是对立的,两者此增彼长。一般来说,使用READ UNCOMMITED隔离级别的数据库拥有最高的并发性和吞吐量,而使用 SERIALIZABLE 隔离级别的数据库并发性最低。
- SQL 92定义 READ UNCOMMITED 主要是为了提供非阻塞读的能力,Oracle虽然支持 READ UNCOMMITED,但它不支持脏读,因为Oracle使用多版本机制彻底解决了在非阻塞读时读到脏数据的问题并保证读的一致性,所以,Oracle 的 READ COMMITED隔离级别就已经满足了SQL 92标准的REPEATABLE READ隔离级别。
- SQL 92推荐使用REPEATABLE READ以保证数据的读一致性,不过用户可以根据应用的需要选择合适的隔离等级。
9.1.5 JDBC 对事务支持
- 并不是所有的数据库都支持事务,即使支持事务的数据库也并非支持所有的事务隔离级别,用户可以通过Connection#getMetaData()方法获取DatabaseMetaData对象,并通过该对象的 supportsTransactions()、supportsTransactionIsolationLevel(int level)方法查看底层数据库的事务支持情况。
- Connection 默认情况下是自动提交的,也即每条执行的SQL都对应一个事务, 为了能够将多条SQL当成一个事务执行,必须先通过 Connection#setAutoCommit(false)阻止Connection 自动提交,并可通过Connection#setTransactionIsolation() 设置事务的隔离级别,Connection中定义了对应SQL 92标准4个事务隔离级别的常量。通过Connection#commit()提交事务,通过Connection#rollback()回滚事务。
Connection conn;
try{
conn = DriverManager.getConnection();//@1 获取数据连接
conn.setAutoCommit(false);//@2 关闭自动提交的机制
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);//@3 设置事务隔离级别
Statement stmt = conn.createStatement();
int rows = stmt.executeUpdate("insert into t_topic values(1,"tom")");
rows = stmt.executeUpdate("update t_user set topic_nums = topic_nums+1 where user_id = 1");
conn.commit();// @4 提交事务
}catch(Exception e){
...
conn.rollback();//@5 回滚事务
}finally{
...
}
- 在 JDBC 2.0中,事务最终只能有两个操作:提交和回滚。但是,有些应用可能需要对事物进行更多的控制,而不是简单的提交或回滚。JDBC 3.0引入了一个全新的保存点特性,Savepoint 接口允许用户将事务分割为多个阶段,用户可以指定回滚到事务的特定保存点,而非回滚到开始事务的点。
- 使用保存点的功能,在发生特定问题时,回滚到指定的保存点,而非回到滚整个事务:
...
Statement stmt = conn.createStatement();
int rows = stmt.executeUpdate("insert into t_topic values(1,"tom")");
Savepoint svpt = conn.setSacepoint("savePoint1");//@1 设置一个保存点
rows = stmt.executeUpdate(update t_user set topic_nums = topic_nums+1 where user_id = 1");
...
conn.rollback(svpt);//@2 回滚到@1处的savePoint1,@1之前的SQL操作,在整个事务提交后依然提交,但@1到@2之间的SQL操作被撤销了
...
conn.commit();//@3 提交事务
- 并非所有的数据库都支持保存点功能,用户可以通过 DatabaseMetaData#supportsSavepoints()方法查看是否支持。
9.2 ThreadLocal 基础知识
- ThreadLocal 在 Spring 中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。
9.2.1 ThreadLocal 是什么
- ThreadLocal,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每个线程都可以独立的改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。
9.2.2 ThreadLocal 的接口方法
- ThreadLocal 类接口很简单,只有4个方法,
- void set(Object value) 设置当前线程的线程局部变量的值;
- public Object get() 该方法返回当前线程所对应的线程局部变量;
- public void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显示调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度;
- protected Object initialValue() 返回该线程局部变量的初始值,该方法是一个protected方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第一次调用get() 或 set(Object)时才执行,并且仅执行一次。Thread Local中的默认实现直接返回一个null。
- 值得一提的是,ThreadLocal已经支持泛型,该类的类名已经变成Thread Local。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。
- ThreadLocal 是如何做到为每一个线程维护一份独立的变量副本呐?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。实例:
public class SimpleThreadLocal{
private Map valueMap = Collections.synchronizedMap(new HashMap());
public void set(Object newValue){
valueMap.put(Thread.currentThread(), newValue);//@1 键为线程对象,值为本线程的变量副本
}
public Object get(){
Thread currentThread = Thread.currentThread();
Object o = valueMap.get(currentThread);//@2 返回本线程对应的变量
if(o == null && !valueMap.containsKey(currentThread)){//@3 如果在Map中不存在,放到Map中保存起来
o = initialValue();
valueMap.put(currentThread, o);
return o;
}
public void remove(){
valueMap.remove(Thread.currentThread());
}
public Object initialVlaue(){
return null;
}
}
}
9.2.3 一个 ThreadLocal 实例
package com.baobaotao.basic;
public class SequenceNumber {
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {//@1 通过匿名内部类覆盖 Thread Local的initialValue()方法,指定初始值
public Integer initialValue() {
return 0;
}
};
public int getNextNum() {//@2 获取下一个序列
seqNum.set(seqNum.get()+1);
return seqNum.get();
}
public static void main(String[] args) {
SequenceNumber sn = new SequenceNumber();
TestClient t1 =new TestClient(sn);//@3 3个线程共享sn,各自产生序列号
TestClient t2 =new TestClient(sn);
TestClient t3 =new TestClient(sn);
t1.start();
t2.start();
t3.start();
}
private static class TestClient extends Thread {
private SequenceNumber sn;
public TestClient(SequenceNumber sn) {
this.sn = sn;
}
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("thread[" + Thread.currentThread().getName() + "]sn[" + sn.getNextNum() +"]");
}
}
}
}
- 通常我们通过匿名内部类的方式定义 ThreadLocal 的子类,提供初始的变量值,如@1处所示,TestClient 线程产生一组序列号,在@3处,我们生成3个TestClient,他们共享同一个SequenceNumber实例。
thread[Thread-0]sn[1]
thread[Thread-0]sn[2]
thread[Thread-0]sn[3]
thread[Thread-1]sn[1]
thread[Thread-1]sn[2]
thread[Thread-1]sn[3]
thread[Thread-2]sn[1]
thread[Thread-2]sn[2]
thread[Thread-2]sn[3]
- 每个线程所产生的序号虽然都共享同一个 SequenceNumber实例,但他们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过 ThreadLocal 为每一个线程提供了单独的副本。
9.2.4 与Thread同步机制的比较
- ThreadLocal 和线程同步机制都是为了解决多线程中相同变量的冲突问题,那么ThreadLocal和线程同步机制相比有什么优势呐?
- 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时刻变量是多个线程共享的,使用同步机制要求程序缜密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
- 而 ThreadLocal 则从另一个角度来解决多线程的并发访问。ThreadLocal为每一个线程提供一个独立的变量副本,从而隔离了多个线程对访问数据的冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的对象封装,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
- 由于 ThreadLocal 中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK5.0通过泛型很好的解决了这个问题,在一定程度上简化ThreadLocal的使用。
- 概况来说:对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化,对象共享化。而ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。前者仅提供一份变量,让不同的线程排队访问;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
9.2.5 Spring 使用 ThreadLocal 解决线程安全问题
package com.baobaotao.scheme;
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
//@1 一个非线程安全的变量
private Connection conn;
public void addTopic() {
//@2 引用非线程安全变量
Statement stat = conn.createStatement();
}
}
- 由于@1处的conn是非线程安全的成员变量,因此addTopic()方法是非线程安全的,必须在使用时间创建一个新TopicDao实例(非singleton)。下面使用 ThreadLocal 对 conn 这个非线程安全的“状态”进行改造:
package com.baobaotao.scheme;
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
//@1 使用ThreadLocal保持Connection变量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
private static Connection getConnection() {
//@2 如果connThread Local没有本线程对应的Connection创建一个新的Connection,
//并将其保存到线程本地变量中
if(connThreadLocal.get() == null) {
Connection conn = ConnectionManager.getConnection();
connThreadLocal.set(conn);
return conn;
}else {
//@3 直接返回线程本地变量
return connThreadLocal.get();
}
}
public void addTopic() {
//@4 从ThreadLocal中获取线程对应的
Statement stat = getConnection().createStatement();
}
}
- 不同的线程在使用TopicDao时,先判断 connThreadLocal.get()是否为null,如果为null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用自己独立的Connection,而不会使用其他线程的Connection。因此,这个TopicDao就可以做到singleton共享了。
- 将Connection的ThreadLocal直接放在Dao只能做到本Dao的多个方法共享Connection时不发生线程安全问题,但无法和其他Dao共用同一个Connection,要做到同一事务多Dao共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。
9.3 Spring 对事务管理的支持
- 像Spring Dao 为不同的持久化实现提供了模板类一样;Spring 事务管理继承了这一风格,提供了事务模板类 TransactionTemplate。通过 TransactionTemplate 并配合使用事务回调 TransactionCallback 指定具体的持久化操作就可以通过编程方式实现事务管理,而无须关注资源获取、复用、释放、事务同步和异常处理的操作。
- Spring 事务管理的亮点在于声明式事务管理。Spring 允许通过声明式,在IoC 配置中指定事务的边界和事务属性,Spring自动在指定的事务边界上应用事务属性。声明式事务是EJB煊赫一时的技术,Spring 让这种技术平民化,甚至可以说,Spring的声明事务比EJB的更为强大。
- EJB 事务建立在JTA的基础上,而JTA又必须通过JNDI获取,这意味着,不管用户的应用是跨数据源的应用,还是单数据源的应用,EJB都要求使用全局事务的方式加以处理,这意味着基于EJB的应用无法脱离应用服务器所提供的容器环境。这种不加区分一概而论的作法无异于杀鸡宰牛都用同一把宰牛刀。
- Spring 深刻的认识到:大部分应用都是基于单数据源,只有位数不多的应用需要使用到多数据源的JTA事务。因此,再单数据源的情况下,Spring 直接使用底层的数据源管理事务。再面对多数据源的应用时,Spring才寻求Java EE 应用服务器的支持,通过引用应用服务器中的JNDI资源完成JTA事务。Spring 让人印象深刻的地方在于不管用户使用何种持久化实现技术,也不管是否使用了JTA事务,都可以采用相同事务管理模型,这种同一的处理方式所带来的好处是不可估量的:用户完全可以抛开事务管理的问题编写程序,并再Spring中通过配置完成事务的管理工作。
9.3.1 事务管理关键抽象
- 在Spring事务管理SPI(Service Provider Interface)的抽象层主要包括3个接口,分别是PlatformTransactionManager、TransactionDefintion和TransactionStatus,它们位于org.springframework.transaction 包中。
- TransactionDefintion 用于表述事务的隔离级别、超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性,这些事务属性可以通过XML配置或注解描述提供,也可以通过手工编程的方式设置。
- PlatformTransactionManager根据TransactionDefinition提供的事务属性配置信息,创建事务,并用TransactionStatus 描述这个激活事务的状态。
TransactionDefinition
TransactionDefinition 定义了 Spring 兼容的事务属性,这些属性对事务管理控制的若干方面进行配置。
-
事务隔离:当前事务和其他事务的隔离的程度。在 TransactionDefinition 接口中,定义了和 java.sql.Connection 接口中同名的4个隔离级别:ISOLATION_READ_UNCOMMITTED、ISOLATION_READ_COMMITTED、ISOLATION_REPEATABLE_READ、ISOLATION_SERIALIZABLE(实际上它直接使用 Connection 的同名常量进行赋值),这些常量分别对应于 9.1.4节所描述隔离级别。此外,TransactionDefinition还定义了一个默认的隔离级别:ISOLATION_DEFAULT,它表示使用底层数据库的默认隔离级别;
-
事务传播:通常在一个事务中执行的所有代码都会在运行于同一事务上下文中。但是Spring也提供了几个可选的事务传播类型:例如,简单地参与到现有的事务中,或者挂起当前的事务,创建一个新的事务。在Sprig事务管理中,传播行为是一个重要的概念,Spring提供了EJB CMT所支持的事务传播类型。
-
事务超时:事务在超时前能运行多久,超过时间后,事务被回滚。有些事务管理器不支持事务过期的功能,这时,如果设置TIMEOUT_DEFAULT之外的其他值时,将抛出异常。
-
只读状态:只读事务不修改任何数据,资源事务管理者可以针对可读事务应用一些优化措施,提高运行性能。只读事务在某些情况下(例如使用Hibernate时),是一种非常有用的优化,试图在只读事务中更改数据将引发异常。
TransactionStatus
TransactionStatus 代表一个事务的具体运行状态。事务管理器通过该接口获取事务的运行期的状态信息,也可以通过该接口间接的回滚事务,它相比于在抛出异常时回滚事务的方式更具有可控性。该接口继承于 SavepointManager接口,SavepointManager接口基于JDBC3.0保存点的分段事务控制能力提供了嵌套事务的机制。
- Object createSavepoint() 创建一个保存点对象,以便在后面可以利用 rollbackToSavepoint(Object savepoint) 方法使事务回滚到特定的保存点上,也可以通过 releaseSavepoint() 释放一个已经不用的保存点;
- void rollbackToSavepoint(Object savepoint) 将事务回滚到特定点上,被回滚的保存点将自动释放;
- void releaseSavepoint(Object savepoint) 释放一个保存点。如果事务提交,所有保存点会被自定释放,无须手工清除。
- 这3个方法在底层的资源不支持保存点时,都将抛出NestedTransactionNotSupportedException异常。
- TransactionStatus扩展了 SavepointManager并提供了一些方法:
- boolean hasSavepoint() 当前的事务是否在内部创建了一个保存点,保存点是为了支持Spring的嵌套事务而创建的;
- boolean isNewTransaction() 判断当前的事务是否是一个新的事务,如果返回false,表示当前事务是一个已经存在的事务,或者当前操作未运行在事务环境中;
- boolean isCompleted() 当前事务是否已经结束:已经提交或回滚;
- boolean isRollbackOnly() 当前事务是否已经被标识为rollback-only;
- void setRollbackOnly() 将当前的事务设置为 rollback-only。通过该标识通知事务管理器只能将事务回滚,事务管理器将通过显示调用回滚命令或抛出异常的方式回滚事务。
PlatformTransactionManager
- 通过JDBC的事务管理我们知道事务只能被提交或回滚(或回滚到某个保存点后提交),Spring高层事务抽象接口 org.springframework.transaction.PlatformTransactionManager 很好的描述了事务管理的这个概念;
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status)throws TransactionException;
void rollback(TransactionStatus status)throws TransactionException;
}
- TransactionStatus getTransaction(TransactionDefinition definition) 该方法根据事务定义信息从事务环境中返回一个已存在的事务,或者创建一个新的事务,并用TransactionStatus描述这个事务的状态;
- commit(TransactionStatus status) 根据事务的状态提交事务,如果事务状态已经被标识为 rollback-only,该方法将执行一个回滚事务的操作;
- rollback(TransactionStatus status) 将事务回滚。当 commit() 方法抛出异常时,rollback() 会被隐式调用。
9.3.2 Spring 的事务管理器实现类
- Spring 将事务管理委托给底层具体的持久化实现框架完成。因此,Spring为不同的持久化框架提供了 PlatformTransactionManager 接口的实现类。
事务 | 说明 |
---|---|
org.springframework.orm.jpa.JpaTransactionManager | 使用JPA进行持久化时,使用该事务管理器 |
org.springframework.orm.hibernate3.HibernateTransactionManager | 使用 Hibernate 3.0 版本进行持久化,使用该事务管理器 |
org.springframework.jdbc.datasource.DataSourceTransactionManager | 使用 Spring JDBC 或 iBatis 等基于DataSource数据源的持久化技术时,使用该事务管理器 |
org.springframework.orm.jdo.JdoTransactionManager | 使用JDO进行持久化时,使用该事务管理器 |
org.springframework.transaction.jta.JtaTransactionManager | 具有多个数据源的全局事务使用该事务管理器(不管采用何种持久化技术) |
- 这些事务管理器都是对特定事务实现框架的代理,这样,我们就可以通过Spring所提交的高级抽象对不同种类的事务实现相同的方式进行管理,而不用关心具体的实现。
- 要实现事务管理,首先要在Spring 中配置好相应的事务管理器,为事务管理器指定数据资源以及一些其他事务管理控制属性。
Spring JDBC 和 iBatis
- 如果使用 Spring JDBC 或 iBatis,它们都是基于数据源的Connection访问数据库,所以可以 使用DataSourceTransactionManager。只需在Spring中进行以下配置就可以了;
<bean id="dataSource"@1 配置一个数据源
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="${jdbc.driverClassName}"
p:url="${jdbc.url}"
p:username="${jdbc.username}"
p:password="${jdbc.password}"/>
<bean id="transactionManager"@2 基于数据源的事务管理器
class="org.springfromwork.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"@3 引用数据源
/>
- 在幕后,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。
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
p:dataSource-ref="dataSource"@1 指定一个数据源
/>
<bean id="transactionManager"
class="org.springfromwork.orm.jpa.JpaTransactionManager"
p:entityManagerFactory-ref="entityManagerFactory"@2 指定实体管理器
/>
Hibernate
- Hibernate 使用 org.hibernate.Session 封装 Connection,所以需要一个能够创建Session 的 SessionFactory。
<bean id="sessionFactory"
class="org.springfromwork.orm.hibernate3.LocalSessionFactoryBean"
p:dataSource-ref="dataSource"@1 指定一个数据源
p:mappingResources="classpath:bbtForum.hbm.xml">@2 指定 Hibernate 配置文件
<property name="hibernateProperties">@3 Hibernate 其他配置属性
<props>
<prop key="hibernate.dialect">${hibernate.dialect}</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.generate_statistics">true</prop>
</props>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory"@4 注入会话工厂
/>
JTA
- 如果希望在JavaEE 容器里使用JTA,我们将通过JNDI和Spring的JtaTransactionManager获取一个容器管理的 DataSource。JtaTransactionManager不需要知道DataSource和其他特定的资源,因为它引用容器提供的全局事务管理:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">
<jee:jndi-lookup id="accountDs" jndi-name="java:comp/env/jdbc/account"/>
<jee:jndi-lookup id="orderDs" jndi-name="java:comp/env/jdbc/order"/>
<bean id="transactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
</bean>
</beans>
9.3.3 事务同步管理器
- Spring 将 JDBC 的 Connection、Hibernate 的 Session 等访问数据库的连接或会话对象统称为资源。这些资源在同一时刻是不能多线程共享的,为了让Dao、Service类可能做到singleton,Spring 的事务同步管理器 org.springframework.transaction.support.TransactionSynchronizationManager 使用 ThreadLocal为不同事务线程提供了独立的资源副本,同时维护事务配置的属性和运行状态信息。事务同步管理器是 Spring 事务管理的基石部分,不管用户使用编程式事务管理,还是声明式事务管理,都离不开事务同步管理器。
- Spring 框架为不同的持久化技术提供了一套从 TransactionSynchronizationManager 中获取对应线程绑定资源的工具类,
持久化技术 | 线程绑定资源获取工具 |
---|---|
Spring JDBC 或 iBatis | org.springframework.jdbc.datasource.DataSourceUtils |
Hibernate 3.0 | org.springframework.orm.hibernate3.SessionFactoryUtils |
JPA | org.springframework.orm.jpa.EntityManagerFactoryUtils |
JDO | org.springframework.orm.jdo.PersistenceManagerFactoryUtils |
- 这些工具类都提供了静态的方法,通过这些静态方法可以获取和当前线程绑定的资源,如DataSourceUtils.getConnection(DataSource dataSource)可以从指定的数据源中获取和当前线程绑定的Connection,而 Hibernate 的 SessionFactoryUtils.getSession(SessionFactory sessionFactory, boolean allowCreate) 则从指定的 SessionFactory中获取和当前线程绑定的Session。
- 当需要脱离模板类,手工操作底层持久技术的原生API时,就需要通过这些工具类获取线程绑定的资源,而不应该直接从 DataSource 或 SessionFactory中获取。因为后者不能获取和本线程相关的资源,因此无法让数据操作参与到本线程相关的事务环境中。
- Spring 为不同的持久化技术提供了模板类,模板类在内部通过资源获取工具类间接访问TransactionSynchronizationManager 中的线程绑定资源。所以,如果Dao使用模板类进行持久化操作,这些Dao就可以配置成singleton。如果不适用模板类,也可以直接通过资源获取工具类访问线程相关的资源。
public abstract class TransactionSynchronizationManager {
//@1 用于保存每个事务线程对应的Connection 或 Session 等类型的资源
private static final ThreadLocal resources = new ThreadLocal();
//@2 用于保存每个事务线程对应事务的名称
private static final ThreadLocal currentTransactionName = new ThreadLocal();
//@3 用于保存每个事务线程对应事务的read-only状态
private staitc final ThreadLocal currentTransactionReadOnly = new ThreadLocal();
//@4 用于保存每个事务线程对应事务的隔离级别
private static final ThreadLocal currentTransactionIsolationLevel = new ThreadLocal();
//@5 用于保存每个事务线程对应事务的激活太态
private static final ThreadLocal actualTransactionActive = new ThreadLocal();
}
- TransactionSynchronizationManager 将Dao、Service类中影响线程安全的所有“状态”统一抽取到该类中,并用ThreadLocal进行替换,从此Dao(必须基于模板类或资源获取工具类创建的Dao)和 Service(必须采用Spring事务管理机制)摘掉了非线程安全的帽子,完成了脱胎换骨式的身份转变。
9.3.4 事务传播行为
- 当我们调用一个基于Spring的Service接口方法(如UserService#addUser())时,它将运行于Spring管理的事务环境中,Service接口方法可能会在内部调用其他的Service接口方法以共同完成一个完整的业务操作,因此就会产生服务接口方法嵌套调用的情况,Spring 通过事务传播行为控制当前的事务如何传播到被嵌套调用的目标服务接口方法中。事务传播是Spring进行事务管理的重要概念,其重要性咋么强调都不为过。但是事务传播行为也时被误解最多的地方。
- Spring 在 TransactionDefinition接口中规定了7种类型的事务传播行为,它们规定了事务方法和事务方法发生嵌套调用时事务如何进行传播。
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起 |
PROPAGATION_NEVER | 以非事务方法执行,如果当前存在事务,则抛出异常 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行于PROPAGATION_REQUIRED类似的操作 |
9.4 编程式的事务管理
-
在实际应用中,很少需要通过编程来进行事务管理,即便如此,Spring 还是为编程式事务管理提供了模板类 org.springframework.transaction.supper.TransactionTemplate,以满足一些特殊场合的需要;
-
TransactionTemplate 和那些持久化模板类一样,是线程安全的,因此,我们可以在多个业务中共享TransactionTemplate实例进行事务管理。TransactionTemplate拥有多个设置事务属性的方法,如setReadOnly(boolean readOnly)、setIsolationLevel(int isolationLevel)等。
- void setTransactionManager(PlatformTransactionManager transactionManager):设置事务管理器;
- Object execute(TransactionCallback action):在TransactionCallback回调接口中定义需要以事务方式组织的数据访问逻辑。
-
TransactionCallback 接口只有一个方法:Object doInTransaction(TransactionStatus status)。如果操作不会返回结果,可以使用TransactionCallback 的子接口 TransactionCallbackWithoutResult。
public class ForumService{
private ForumDao forumDao;
TransactionTemplate template;//@1 通过IoC注入
public void addForum(final Forum forum){
template.execute(new TransactionCallbackWithoutResult(){
protected void doInTransactionWithoutResult(TransactionStatus status){
forumDao.addForum(forum);//@2 需要在事务环境中执行的代码
}
});
}
}
- 由于 Spring 事务管理基于 TransactionSynchronizationManger 进行工作,所以如果在回调接口方法中需要显示方法底层数据连接,必须通过资源获取工具类得到线程绑定的数据连接,这是Spring事务管理的底层协议,不容违反。如果 ForumDao 是基于 Spring 提供的模板类构建的,由于模板类已经在内部使用了资源获取工具类获取数据连接,所以用户就不必关心底层数据连接的获取问题了。
9.5 使用 XML 配置声明式事务
- 大多数 Spring 用户选择声明式事务管理的功能,这种方法对代码的侵入性最小,可以让事务管理代码完全从业务中移除,非常符合非侵入式轻量容器的理念。
- Spring 的声明式事务管理是通过 Spring AOP实现的,通过事务的声明性信息,Spring 负责将事务管理增强逻辑动态织入到业务方法相应连接点中。这些逻辑包括获取线程绑定资源、开始事务、提交/回滚事务、进行异常转换和处理等工作。
- Spring 提供了和EJB CMT 相似的声明式事务管理,这不但体现在两者的最终执行效果上,还体现在两种事务声明的语法上。
比较项 | EJB CMT | Spring |
---|---|---|
是否绑定JTA | 绑定在JTA上,即使是单数据源也如此。所以EJB不能脱离容器运行 | 可以在任何环境下使用,包括直接在Spring中声明的数据源或应用服务器JNDI中的JTA数据源 |
持久化技术支持 | 采用非开放的EJB自制持久化技术 | 通过少量配置即可和JDBC、JDO、Hibernate等持久化技术一起工作 |
目标类要求 | 必须是实现特定接口的特殊类 | 可以是任何POJO,不过在内部必须使用资源获取工具类操作数据连接或会话,如果DAO使用模板类进行构建,这种要求将自动得到满足 |
回滚原则 | 没有提供。 | 提供声明式的回滚规则 |
开放性控制 | 使用 EJB CMT,除了使用 setRollbackOnly(),没有办法影响容器的事务管理 | Spring允许用户通过AOP定制事务行为。例如,如果需要,用户可以在事务回滚中插入定制的行为,也可以增加任意的增强,就和任何AOP的增强一样 |
分布式事务 | 支持分布式事务。一般应用并不需要使用这样的功能 | Spring 不直接支持高端应用服务器所提供的跨越远程调用的事务上下文传播。此时,可以通过Spring的 Java EE服务集成来提供。此外,如果在Spring 中集成JOTM后,Spring也可以提供JTA事务的功能 |
- 回滚的概念比较重要:它使我们能够指定什么样的异常导致自动回滚,什么样的异常不影响事务提交,这些规则可以在配置文件中通过声明的方式地指定,同时,我们仍旧可以通过调用TransactionStatus#setRollbackOnly()方法编程式地回滚当前事务。通常,我们定义一条规则,如声明MyApplicationException必须总是导致事务回滚。这种方式带来了显著的好处,它使用户的业务对象不必依赖于事务设施。典型的例子式用户不必在代码中导入Spring API、事务代码等。
- 对EJB来说,默认的行为是EJB容器在遇到系统异常(通常指运行时异常)时自动回滚当前事务。EJB遇到应用异常(通常指检测型异常)时并不会自动回滚。默认情况下,Spring 的声明式事务管理和 EJB 的相同(即只在遇到运行期异常时自动回滚)。
特别篇
- Java把所有的非正常情况分为两种:异常(Exception)和错误(Error),它们都继承Throwable父类。
- Java的异常(Exception和Error)分为检查异常和非检查的异常。
- 其中根据Exception异常进行分类,可分为运行时异常和非运行时异常。
检查异常:
-
就是编译器要求你必须处理的异常。比如我们在编程某个文件的读于写时,编译器要求你必须要对某段代码try…catch… 或者 throws exception,这就是检查异常,简单的来说,你代码还没有运行,编码器就会检查你的代码,对可能出现的异常必须做出相对的处理。(比如当文件不存在时…)
-
如何处理检查异常:
-
1、继续往上抛出,(这是一个消极的方法),一直可以抛到java虚拟机来处理,通过throw exception抛出。
-
2、用try…catch捕获 (提示) 对于检查时的异常必须处理,或者必须捕获或者必须抛出
-
-
检查异常大概有哪些?
- 除了RuntimeException与其派生类(子类),以及错误(Error)。其他的差不多都是检查异常。
非检查异常:
-
编译器不要求强制处置的异常,虽然有可能出现错误,但是我不会在编译的时候检查。
-
如何处理非检查异常:
- 1、try…catch捕获
- 2、继续抛出
- 3、不处理
-
这类异常一般我们是不处理的,因为会很判断会出现什么问题,而且有些异常你也无法运行时处理,比如空指针。
-
非检查异常有哪些:
- RuntimeException与其子类,以及错误(Error)
Exception异常进行划分,它可分为运行时异常和非运行时异常。
-
运行时异常:
- 都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是非检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
- 运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过
-
非运行时异常:
- 是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不要自定义检查异常。
9.5.1 一个将被实施事务增强的服务接口
- BbtForun 是业务层接口,我们希望通过 Spring 的声明式事务让这个接口的方法拥有合适的事务功能。
package com.baobaotao.bbt;
import com.baobaotao.bbt.Forum;
import com.baobaotao.bbt.Topic;
public interface BbtForum {
void addTopic(Topic topic);
void updateForum(Forum forum);
Forum getForum(int ForumId);
int getForumNum();
}
- BbtForum 拥有4个方法,我们希望 addTopic() 和 updateForum() 方法拥有写事务的能力,而其他两个方法只需要拥有读事务的能力就可以了。
package com.baobaotao.bbt;
import com.baobaotao.bbt.Forum;
import com.baobaotao.bbt.Topic;
public class BbtForumImpl implements BbtForum{
private ForumDao forumDao;
private TopicDao topicDao;
private PostDao postDao;
@Override
public void addTopic(Topic topic) {
topicDao.addTopic(topic);
postDao.addPost(topic.getPost());
}
@Override
public void updateForum(Forum forum) {
forumDao.updateForum(forum);
}
@Override
public Forum getForum(int forumId) {
return forumDao.getForum(forumId);
}
@Override
public int getForumNum() {
return forumDao.getForumNum();
}
}
- BbtForumImpl 是一个POJO,只是简单使用持久层多个 DAO 类,通过它们的协作实现 BbtForum 接口的功能。在这里,我们看不到任何事务操作的代码,所以如果之间使用BbtForumImpl,这些方法都将以无事务的方式运行。现在,我们的任务是通过 Spring 声明事务配置让这些业务方法拥有合适的事务功能。
9.5.2 使用原始的 TransactionProxyFactoryBean
声明式事务配置
- 使用 TransactionProxyFactoryBean 配置
<!-- @1 引入Dao和DaoSource的配置文件 -->
<import resource="classpath:applicationContext-dao.xml"/>
<!-- @2 声明事务管理器 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- @3 需要实施事务增强的目标业务Bean -->
<bean id="bbtForumTarget"
class="com.baobaotao.bbt.BbtForumImpl"
p:forumDao-ref="forumDao"
p:topicDao-ref="topicDao"
p:postDao-ref="postDao"/>
<bean id="bbtForum"@4 使用事务代理工厂类为目标业务Bean提供事务增强
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
p:transactionManager-ref="txManager"@4-1 指定事务管理器
p:target-ref="bbtForumTarget">@4-2 指定目标业务Bean
<property name="transactionAttributes">@4-3 事务属性配置
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>@4-4 只读事务
<prop key="*">PROPAGATION_REQUIRED</prop>@4-5 可写事务
</props>
</property>
</bean>
- 当然,我们必须首先配置好 DAO Bean、资源池等基础设施,这些信息统一在 applicationContext-dao.xml 配置文件中定义。在这个实例中,我们使用 Spring JDBC 的持久化技术,所以它的资源池是JDBC数据源,而事务管理器是 DataSourceTransactionManager。如果使用其他持久化技术,需要对基础设施和事务管理器进行相应的调整。
- 按照约定的习惯,需要事务增强的业务类一般将id取名为 xxxTarget,如@3所示,这可以在字面上表示该 Bean 是要被代理的目标 Bean。
- 通过 TransactionProxyFactoryBean 对业务类进行代理,织入事务增强功能,如@4所示。首先,需要为该代理类指定事务管理器,这些事务管理器实现了 PlatformTransactionManager 接口;其次,通过 target 属性指定需要代理的目标 Bean;最后,为业务 Bean 的不同方法配置事务属性。Spring 允许我们通过键值配置业务方法的事务属性信息,键可以使用通配符,如get* 代表目标类中所有以get为前缀的方法,它匹配BbtForum 的 getForum(int forumId) 和 getForumNum() 方法。而 key="*" 匹配 BbtForum 接口所有的方法。
异常回滚/提交规则
- < prop > 内的值为事务属性信息,其格式如:
PROPAGATION(传播行为),ISOLATION(隔离级别-可选),readOnly(可选),-Exception(发生这些异常时回滚事务-可选),+Exception(发生这些异常时照样提交事务-可选)
- 传播行为时唯一必须提供的配置项,< prop >的值为空时,也不会发生配置异常,但对应的匹配方法不会应用事务,相当于没有配置 。
- 隔离级别配置项是可选的,默认为 ISOLATION_DEFAULT,表示使用数据库默认的隔离级别。隔离级别其他可选的设置值为:
- ISOLATION_READ_UNCOMMITTED
- ISOLATION_READ_COMMITTED
- ISOLATION_REPEATABLE_READ
- ISOLATION_SERIALZABLE
- 如果希望将配置方法设置为只读事务,可添加 readOnly 配置项;
- 当事务运行过程中发生异常时,事务可以被声明为归滚或继续提交。默认情况下,当发生运行期异常时,事务将被回滚,发生检查型异常时,既不回滚也不提交,控制权交给外层调用。这种默认的回滚规则在大多数情况下是适用的,不过用户也可以通过配置显式指定回滚规则:通过指定带正号(+) 或负号(-) 的异常类名(或异常名匹配片段)。当抛出负号型异常时,将触发事务回滚,当抛出正号异常时,即使这个异常是检查型异常,事务也会提交。抛出的异常或该异常的祖先类的类名匹配规则中指定的异常类名(或异常名片段),规则就生效,如:
<prop key="add">PROPAGATION_REQUIRED,-Exception</prop>
- 只要业务方法运行时抛出的异常或其父类异常的类名包括“Exception”,事务都回滚,以下异常都符合这条规则:SQLException、ParseException。正因为 Spring 采用名称字符串包含的比较方式,所以用户甚至可以将回滚的规则设置为“-tion”。
- 因为 Spring 默认的事务回滚规则为:运行期异常回滚,检查型异常不会滚,所以带负号的异常设置仅对检查型异常有意义。
- 此外,用户可以指定多个带正号或负号的事务回滚/提交规则,如:
<prop key="add">
PROPAGATION_REQUIRED,-XxxException,-YyyException
</prop>
异常提交的实例
- 假如我们希望 BbtForum#addTopic(Topic topic) 在抛出 PessimisticLockingFailureException 异常时,依旧提交事务,对应的配置如下所示:
<prop key="addTopic">
PROPAGATION_REQUIRED,+PessimisticLockingFailureException
</prop>
- 调整 BbtForumImpl#addTopic(Topic topic) 实现方法,模拟运行时异常抛出 PessimisticLockingFailureException
public class BbtForumImpl implements BbtForum{
...
@Override
public void addTopic(Topic topic) throws Exception {
topicDao.addTopic(topic);//@1 依旧会提交此处的数据持久化操作
if(true) throws new PessimisticLockingFailureException("fail");
postDao.addPost(topic.getPost());//@2 此处的数据持久化操作无法达到
}
...
}
- 当运行这个方法时,虽然在@1和@2之间抛出了异常,但Spring最终将使@1处的数据持久化操作生效,而@2处的数据持久化操作将因为前面抛出了异常而无法执行。
9.5.3 基于 tx/aop 命名空间的配置
- 使用 TransactionProxyFactoryBean 代理工厂类为业务类添加事务性支持,又有以下明显的缺点
- 需要对每个需要事务支持的业务类进行单独的配置;
- 在指定事务方法时,只能通过方法名进行定义,无法利用方法签名的其他信息进行定位(如方法入参、访问域修饰符等);
- 事务属性的配置串的规则比较麻烦,规则串虽然包括多项信息,但统一由逗号分割的字符串来描述,不能利用IDE中的诱导输入功能,容易出错;
- 在为业务类 Bean 添加事务支持时,在容器中既需要定义业务类 Bean(通常命名为xxxTarget),又需要通过 TransactionProxyFactoryBean 对其进行代理以生成支持事务的代理 Bean。 实际上,我们只会从容器中返回代理的Bean,而业务类Bean仅时为了能被代理才定义的,这样就造成相似的东西有两份配置,增加了配置信息量。
- Spring 在基于 Schema 的配置中,添加了一个 tx 命名空间,在配置文件中以明确结构化的方式定义事务属性,大大提高了配置事务属性的便利性。配合 aop 命名空间所提供切面定义这把利剑,业务类方法事务配置得到了大大的简化,而在描述能力上却得到了很大的提升。
- 通过 tx 和 aop 命名空间对上节中基于 FactoryBean 的事务配置方式进行替换。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<import resource="classpath:applicationContext-dao.xml"/>
<!-- @1 不再需要为了事务AOP增强的实施而改名换姓 -->
<bean id="bbtForum"
class="com.baobaotao.bbt.BbtForumImpl"
p:forumDao-ref="forumDao"
p:topicDao-ref="topicDao"
p:postDao-ref="postDao"/>
<!-- @2 事务管理器 -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- @3 使用强大的切点表达式语言轻松定义目标方法 -->
<aop:config>
<!-- @3-1 通过aop定义事务增强切面 -->
<aop:pointcut id="serviceMethod"
expression="execution(* com.baobaotao.service.*Forum.*(..))"/>
<!-- @3-2 引用事务增强 -->
<aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice"/>
</aop:config>
<!-- @4 事务增强 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- @4-1 事务属性定义 -->
<tx:attributes>
<tx:method name="get" read-only="false"/>
<tx:method name="add*" rollback-for="Exception"/>
<tx:method name="update*"/>
</tx:attributes>
</tx:advice>
</beans>
- 首先,需要在配置文件中引入 tx 命名空间的声明,如 < beans > 元素粗体部分所示。采用 aop/tx 定义事务方法时,它站在“局外人”的角度对 IoC 容器中的 Bean 进行事务管理配置定义,再由 Spring 将这些配置织入到对应的 Bean 中。
- 在这一过程中我们看到了三种角色:通过 aop/tx 定义的声明式事务配置信息、业务Bean、Spring 容器。SPring 容器自动将第一者应用于第二者,从容器中返回的业务 Bean 已经是被织入事务增强的代理 Bean,即第一者和第二者在配置时不直接发生关系。
- 而在使用 TransactionProxyFactoryBean 进行事务配置时,TransactionProxyFactoryBean 需要直接通过 target 属性引用目标业务 Bean,结果造成目标业务 Bean 往往需要使用 target进行命名(如userServiceTarget),以避免和最终代理 Bean 名称(如 userService)冲突。使用 aop/tx 方式后,业务 Bean 的名称不需要做任何“配合性”的调整,aop直接通过切点表达式语言就可对业务 Bean 进行定位。从这个意义上来说,aop/tx 的配置方式对业务 Bean 是“无侵入”的,而 TransactionProxyFactoryBean 的配置显然是“侵入式”的。
- 在 aop 命名空间中,通过切点表达式语言,我们将 com.baobaotao.service 包下所有以 Forum 为后缀的类纳入了需要进行事务增强的范围。配合 < tx:advice > 的 < aop:advisor >完成了事务切面的定义,如@3所示。
- < aop:advisor > 引用的 txAdvice 增强式在 tx 命名空间上定义的,如@4所示。首先,事务增强一定需要一个事务管理器的支持,< tx:advice >通过 transaction-manager 属性应用了@2处定义的事务管理器(它默认查找名为 transactionManager 的事务管理器,所以如果事务管理器命名为 transactionManager,可以不指定 transaction-manager 属性)。在@4-1中,我们看到原来参杂在一起,以逗号分隔字符串定义的事务属性,现在变成了一个结构清晰的XML片段。< tx:method >元素拥有以下的属性:
属性 | 是否必须 | 默认值 | 描述 |
---|---|---|---|
name | 是 | 与事务属性关联的方法名。可使用通配符(*) | 如"get*"、“handle*”、"on*Event"等 |
propagation | 否 | REQUIRED | 事务传播行为,可选值为:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED |
isolation | 否 | DEFAULT | 事务隔离级别,可选值为:DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE |
timeout | 否 | -1 | 事务超时的时间(以秒为单位),如果设置为-1,事务超时的时间由底层的事务系统决定 |
read-only | 否 | false | 事务是否只读 |
rollback-for | 否 | 所有运行期异常回滚 | 触发事务回滚的 Exception,用异常名称的片段进行匹配。可以设置多个,以逗号分开。如:“Exception1,Exception2” |
no-rollback-for | 否 | 所有检查型异常不回滚 | 不触发事务回滚的 Exception,用异常名称的片段进行匹配。可以设置多个,以逗号分开。如:“Exception1,Exception2” |
9.6 使用注解配置声明式事务
9.6.1 使用 @Transactional 注解
package com.baobaotao.bbt;
import org.springframework.transaction.annotation.Transactional;
import com.baobaotao.bbt.Forum;
import com.baobaotao.bbt.Topic;
//@1 对业务类进行事务增强的标注
@Transactional
public class BbtForumImpl implements BbtForum{
...
@Override
public void addTopic(Topic topic) throws Exception {
topicDao.addTopic(topic);
postDao.addPost(topic.getPost());
}
@Override
public Forum getForum(int forumId) {
return forumDao.getForum(forumId);
}
...
}
- 因为注解本身具有一组普适性的默认事务属性,所以往往只需要事务管理的业务类中添加一个@Transactional注解就完成了业务类事务属性的配置。
- 当然,注解只是提供元数据,它本身并不能完成事务切面织入的功能。因此,我们还需要再Spring配置文件通过一行小小的配置“通知”Spring容器对标注@Transactional的Bean进行加工处理:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aopi
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<import resource="classpath:applicationContext-dao.xml"/>
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
<!-- @1 对标注@Transactional注解的Bean进行加工处理,以织入事务管理切面 -->
<tx:annotation-driven transaction-manager="txManager"/>
<!-- @2 由于该Bean实现类标注了@Transactional,所以将会被@1处的注解驱动自动织入事务 -->
<bean id="bbtForum"
class="com.baobaotao.bbt.BbtForumImpl"
p:forumDao-ref="forumDao"
p:topicDao-ref="topicDao"
p:postDao-ref="postDao"/>
</beans>
- 默认情况下,< tx:annotation-driven >会自动使用名称为“transactionManager”的事务管理器,所以如果用户的事务管理器id为“transactionManager”,可以进一步将@1处的配置简化为:< tx:annotation-driven >。
- < tx:annotation-driven > 还有另外两个属性。
- proxy-target-class:如果为 true,Spring 将通过创建子类来代理业务类,如果为false,则使用基于接口的代理。如果使用子类代理,需要再类路径中添加CGLib.jar类库。
- order:如果业务类除事务切面外,还需要织入其他的的切面,通过该属性可以控制事务切面再目标连接点的织入顺序。
关于@Transactional的属性
- 基于@Transactional注解的配置和基于XML的配置方式一样,它拥有一组普适性很强的默认事务属性,我们往往可以直接使用这些默认的属性就可以了:
- 事务传播行为:PROPAGATION_REQUIRED;
- 事务隔离级别:ISOLATION_DEFAULT;
- 读写事务属性:读/写事务;
- 超时时间:依赖于底层的事务系统的默认值;
- 回滚设置:任何运行期异常引发回滚,任何检查型异常不会引发回滚。
- 因为这些默认设置再大多数情况下都适用的,一般不需要手工设置事务注解的属性,但,Spring 允许我们通过手工设定属性值覆盖默认值。
属性名 | 说明 |
---|---|
propagation | 事务传播行为,通过以下枚举提供合法值:org.springframework.transaction.annotation.Propagation 例如:@Transactional(propagation=Propagation.REQUIRES_NEW) |
isolation | 事务隔离级别,通过以下枚举类提供合法值:org.springframework.transaction.annotation.Isolation 例如:@Transactional(isolation=Isolation.READ_COMMITTED) |
readOnly | 事务读写性,boolean 型:例如 @Transactional(readOnly=true) |
timeout | 超时时间,int 型,以秒为单位,例如:@Transaction(timeout=10) |
rollbackFor | 一组异常类,遇到时进行回滚,类型为:Class<? extends Throwable>[],默认为{}。例如:@Transactional(rollbackFor={SQLException.class}),多个异常之间可用逗号分割。 |
rollbackForClassName | 一组异常类名,遇到时进行回滚,类型为 String[] ,默认值为{}。例如:@Transactional(rollbackForClassName={“Exception”}) |
noRollbackFor | 一组异常类,遇到时不回滚,类型为:Class<? exteds Throwable>[],默认为{} |
noRollbackForClassName | 一组异常类名,遇到时不回滚,类型为 String[],默认值为{} |
在何处标注@Transactional注解
- @Transactional注解可用被应用于接口定义和接口方法、类定义和类的 public 方法上。
- 但 Spring 建议在业务实现类上使用@Transactional 注解,当然我们也可用在业务接口上使用@Transactional注解。但只要会留下一些容易被忽视的隐患。因为注解不能被继承,所以业务接口中标注的@Transactional 注解不会被业务实现类继承,如果通过以下的配置启用子类代理:
<tx:annotation-driven proxy-target-class="true"/>
- 业务类不会添加事务增强,照样工作在非事务的环境下。举一个具体的实例:如果使用子类代理,假设用户为 BbtForum 接口标注了 @Transactional 注解,其实现类 BbtForumImpl 依旧不会启用事务机制。
- 因此,Spring 建议在具体业务类上使用 @Transactional 注解。这样,不管 < tx:annotation-driven >将proxy-target-class属性配置为true或false,业务类都会启用事务机制。
在方法处使用注解
- 在方法处的注解会覆盖类定义出的注解,如果有些方法需要使用特殊的事务属性,则可以在类注解的基础上,提供方法注解。
package com.baobaotao.bbt;
import org.springframework.transaction.annotation.Transactional;
import com.baobaotao.bbt.Forum;
import com.baobaotao.bbt.Topic;
@Transactional//@1 类级的注解,适用于类中所有 public 的方法
public class BbtForumImpl implements BbtForum{
@Transactional(readOnly=true)//@2 提供额外的注解信息,它将覆盖@1处的类级注解
@Override
public Forum getForum(int forumId) {
return forumDao.getForum(forumId);
}
}
- @2 处的方法注解提供了 readOnly 事务属性设置,它将覆盖类级注解中默认的 readOnly=false 设置。
使用不同的事务管理器
- 在一般情况下,一个应用仅需使用到一个事务管理器就可以了。如果希望在不同的地方使用不同的事务管理器,则可以通过如下方式实现:
public class MultiForumService{
//@1 使用名为topic的事务管理器
@Transactional("topic")
public void addTopic(Topic topic) {
...
}
//@2 使用名为forum的事务管理器
@Transactional("forum")
public void updateForum(Forum forum) {
...
}
}
- 而 topic 和 forum 的事务管理器可以在 XML 中分别定义,如:
<bean id="forumTxManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="forumDataSource">@1 可以使用不同的数据源
<qualifier value="forum"/>@2 为事务管理器标识一个名字
</bean>
<bean id="topicTxManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="topicDataSource">
<qualifier value="topic"/>
</bean>
<tx:annotation-driven/>
- 在@1处,为事务管理器指定了一个数据源,每个事务管理器都可以绑定一个独立的数据源。在@2处,制定了一个可被@Transactional 注解引用的事务管理器标识。
- 在一两处使用带标识的@Transactional 也许时挺合适的,但时如果到处都使用,则显得比较哆嗦。可以自定义一个绑定到特定事务管理器的注解,然后直接使用这个自定义的注解进行标识:
package com.baobaotao.bbt;
...
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("forum")//@1 绑定到forum的事务管理器中
public @interface ForumTransactional {
}
- 按相似的方法,还可以定义一个绑定到 topic 事务管理器的 @TopicTransactional。完成定义后,就可以用以下方法对原来的代码进行调整了:
public class MultiForumService {
//@1使用名为topic的事务管理器
@TopicTransactional
public void addTopic(Topic topic) {
...
}
//@2 使用名为forum的事务管理器
@ForumTransactional
public void updateForum(Forum forum) {
...
}
}
9.6.2 通过 AspectJ LTW 引入事务切面
- Spring 还提供了 AspectJ 切面的支持,在 org.springframework.aspects-{version}.jar 中有一个用 AspectJ 语法编写的 AnnotationTransactionAspect 切面类,它用于为使用@Transactional 注解的业务类提供事务增强。
- 使用 -javaagent: 的JVM参数,在类路径 META-INF 目录下提供如下的 AspectJ 配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<aspectj>
<aspects>
<aspect name="org.springframework.transaction.aspectj.AnnotationTransactionAspect"/>
</aspects>
<weaver
options="-showWeaveInfo -XmessageHandlerClass:
org.springframework.transaction.aspectj.AspectJWeaverMessageHandler">
<include within="com.baobaotao.servie.impl.*"/>
</weaver>
</aspectj>
- 在类加载期,对标注@Transactional 注解的织入 AnnotationTransactionAspect 事务增强切面。
- AnnotationTransactionAspect 切面类需要使用事务管理器,所以必须利用 Spring IoC 为切面类提供事务管理器的注入:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
<bean id="bbtForum"
class="com.baobaotao.bbt.BbtForumImpl"
p:forumDao-ref="forumDao"
p:topicDao-ref="topicDao"
p:postDao-ref="postDao"/>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
<tx:annotation-driven/>
</beans>
- 如@1所示,通过 transactionManager 属性注入事务管理器 Bean,这样切面类就能为目标类提供事务管理的功能了。
9.7 集成特定的应用服务器
- 一般来说,Spring 的事务抽象于应用服务器是无关的。不过,如果用户希望事务管理器使用特定的 UserTransaction 和 TransactionManager 对象(可以被自动探测),以获取更多的事务控制功能(如事务挂起等),这是可以采用 Spring 为集成这些应用服务器所提供的适配器。
9.7.1 BEA WebLogic
- 在一个使用 WebLogic 9.0 或更高版本的环境中,用户一般会优先选用特定于 web Logic的 WebLogicJtaTransactionManager 类取代基础的 JtaTransactionManager 类,因为在 WebLogic 环境中,该类提供了对 Spring 事务定义的完全支持,超过了标准的 JTA 语义。
- 用户可以使用以下配置达到目的:
<bean id="txManager"
classs="org.springframework.transaction.jta.WebLogicJtaTransactionManager"/>
- 它的特性包括:支持事务名、支持为每个事务定义隔离级别,以及在任何环境下正确的回复事务的能力。
9.7.2 BEA WebLogic
- 在 WebSphere 6.1 及以上版本环境下,用户可以使用 Spring 的 WebSphereUowTransactionManager类。这个特殊的适配器支持 IBM 的 UOWManager 的 API,在给适配器中,Spring所支持的事务挂起特性得到了 IBM 官方的支持。
<bean id="txManager"
classs="org.springframework.transaction.jta.WebSphereUowTransactionManager"/>