一、数据库事务基础知识
“一荣俱荣,一损俱损”这句话很能体现事务的思想,很多复杂的事务要分布进行,但他们组成一个整体,要么整体生效,要么整体失效。这种思想反映到数据库上,就是多个SQL语句,要么所有执行成功,要么所有执行失败。
数据库管理系统一般采用重执行日志保证原子性、一致性、和持久性,,重执行日志记录了数据库变化的每一个动作,数据库在一个事务中执行一部分操作后发生错误退出,数据库及可以根据重执行日志撤销已经执行的操作。此外,对于已经提交的事务,即使数据库崩溃,在重启数据库时也能够根据日志对尚未持久化的数据进行响应的重执行操作。
和Java程序采用对象锁机制进行线程同步类似,数据库管理系统采用数据库锁机制保证事务的隔离性。当多个事务试图对相同的数据进行操作时,只有持有锁的事务能够操作数据,知道前一个事务完成后,后面的事务才有机会对数据进行操作。Oracle数据库还使用了数据库版本的机制,在回滚段为数据的每个变化都保存一个版本,是数据的更改不影响数据的读取。
数据库锁机制:按锁定的对象不同,一般可以分为表锁定和行锁定,前者对整个表进行锁定,后者对表中特定行进行锁定;从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。共享锁定会防止独占锁定,但允许其他的共享锁定。而独占锁定既防止其他的独占锁定,也防止其他的共享锁定。为了更改数据,数据库必须在进行更改的行上施加行独占锁定,INSERT、UPDATE、DELETE和SELECT FOR UPDATE 语句都会隐式采用必要的行锁定。
尽管数据库为用户提供了锁的DML操作方式,但直接使用锁管理是非常麻烦的,因此数据库为用户提供了自动锁机制。只要用户指定会话的事务隔离级别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源添加上适合的锁。此外数据库还会维护这些所,当一个资源上的锁数目太多时,自动进行锁升级以提供系统的运行性能,而这一过程对用户来说完全是透明的。
JDBC对事务的支持见代码:
public void JdbcTest(){
try{
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/login", "root", "123456");
conn.setAutoCommit(false);//关闭自动提交的机制
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);//设置事务隔离级别为:不可以发生脏读、不可重复读和虚读的常量
stmt = conn.createStatement();
int rows = stmt.executeUpdate("INSERT INTO tab_user(user,password) VALUES('李四','123')");
Savepoint svpt = conn.setSavepoint("savePoint1");//设置一个保存点
rows = stmt.executeUpdate("UPDATE tab_user SET user = '王五' WHERE id = 2");
conn.rollback(svpt);//回滚到保存点1处
conn.commit();//最后提交
conn.close();
stmt.close();
}catch(SQLException e){
e.printStackTrace();
}
}
在JDBC 2.0中,事务最终只能有连个操作:提价和回滚。但是有些应有可能需要对事务进行更多的控制,而不是简单地提交和回滚。JDBC 3.0引入了一个全新的保存点特性,Savepoint接口允许用户将事务分割为多个阶段,用户可以指定回滚到事务的特定保存点。但是并非所有的数据库都支持保存点功能,用户可以通过DatabaseMetaData#supportsSavepoints()方法查看是否支持。
二、ThreadLocal基础知识
按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步。但模板类并未采用线程同步机制,因为线程同步会降低并发性,影响系统性能。此外,通过代码同步解决线程安全的挑战性很大,可能会增强好几倍的实现难度。那么在无需线程同步的情况下怎么化解的线程安全的难度,答案就是ThreadLocal!
ThreadLocal位于java.lang.ThreadLocal。顾名思义,它不是一个线程,而是线程的一个本地化对象。对工作于多线程中对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量,这也是类名中Local所要表达的意思。
ThreadLocal只有4个方法:
1) void set(Object value):设置当前线程的线程局部变量的值;
2) public Object get():返回线程所对象的线程局部变量;
3) public void remove():将当前线程局部变量的值删除,目的是为了减少内存的占用;
4) protected void Object initialValue():返回该线程局部变量的初始值,该方法是一个protected方法,为了让子类覆盖而设计。
在JDK 5.0中,ThreadLocal已经支持泛型,该类的类名已经变味ThreadLocal
(一)、ThreadLocal实例
public class SequenceNumber {
//通过匿名内部类覆盖ThreadLocal的ininialValue()方法,指定初始值
private static ThreadLocal seqNum = new ThreadLocal(){
public Integer initialValue(){
return 0;
}
};
//获取下一个序列值
public int getNextNum(){
seqNum.set(seqNum.get()+1);
return seqNum.get();
}
public static void main(String[]args){
SequenceNumber sn = new SequenceNumber();
//三个线程共享sn,各自产生序列号
TestClient t1 = new TestClient(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(){
//每个线程打出3个序列值
for(int i = 0;i<3;i++){
System.out.println("thread["+Thread.currentThread().getName()+"]sn["+sn.getNextNum()+"]");
}
}
}
}
结果发现,线程所产生的序列号虽然都是共享同一个SequenceNumber实例,但它们并没有发生互相干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每个线程提供了单独的副本。
(二)、与Thread同步机制的比较
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化,对象共享化。而ThreadLocal采用了“以空间换时间”的方式:访问并行话,对象独享化。前者仅提供一份变量,让不同的线程排队访问,而后者为每个线程都提供了一份变量,因此可以同时访问而互不影响。
三、Spring对事务管理的支持
Spring为事务管理提供了一致的编程模板,在高层次建立了一致的事务抽象。也就是说,不管选择Spring JDBC、Hibernate、JPA还是iBatis,Spring都让我们可以用统一的编程模式进行事务管理。
像Spring DAO为不同的持久化实现提供了模板类一样,Spring事务管理继承了这一风格,也提供了事务模板类TransactionTemplate。通过TransactionTemplate并配合使用事务回调TransactionCallback指定具体的持久化操作就可以通过编程方式实现事务管理,而无须关注资源获取、复用、释放、事务同步和异常处理的操作。
(一)、事务管理关键抽象
在Spring事务管理SPI高层抽象层主要包括3个接口,分别是PlatformTransactionManager、TransactionDefinition和TransactionStatus。
TransactionDefinition用于描述事务的隔离级别、超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性,这些事务属性可以XML配置或注解描述提供,也可以通过手工编程的方式设置。
PlatformTransactionManager根据TransactionDefinition提供的事务属性配置信息,创建事务,并用TransactionStatus描述这个激活事务的状态。
(1)、TransactionDefinition定义的事务属性
1)、事务隔离:当前事务和其他事务的隔离的程度;
2)、事务传播:通常在一个事务中执行的所有代码都会在运行于同一事务上下文中运行;
3)事务超时:事务在超时前能运行多久,超过时间后,事务被回滚。
4)只读状态:只读事务不修改任何数据,资源事务管理者可以针对可读事务应用一些优化措施,提供运行性能。
(2)、TransactionStatus代表一个事务的具体运行状态。通过该接口获取事务的运行期状态信息,也可以通过该接口间接地回滚事务,更具有灵活性。
该接口拥有以下的方法:
1) Object createSavepoint():创建一个保存点对象;
2) Void rollbackToSavepoint(Object savepoint):将事务回滚到特定的保存点上,被回滚的保存点将自动释放;
3) Void releaseSavepoint(Object savepoing):释放一个保存点。如果事务提交,所有保存点会被自动释放,无须手工清楚。
4) Boolean hasSavepoint():判断当前事务是否在内部创建了一个保存点,保存点是为了支持Spring的嵌套事务而创建的;
5) Boolean isNewTransaction():判断当前事务是否是一个新的事务,如果返回false,表示当前事务是一个已经存在的事务,或者当前操作未运行在事务环境中;
6) boolean isCompleted():判断当前事务是否已经结束:提交或回滚;
7) void setRollbackOnly:通过该标识通知事务管理器只能讲事务回滚,事务管理器将通过显示调用回滚命令或抛出异常的方式回滚事务。
8) Boolean isRollbackOnly:判断当前事务是否被标识为rollback-only。
(二)、Spring的事务管理器实现类
Spring将事务管理委托给底层具体的持久化实现框架完成。因此Spring为不同的持久化框架提供了PlatformTransactionManager接口的实现类,如图:
(三)、事务同步管理器
Spring将JDBC的Connection、Hibernate的Seeesion等访问数据库的链接或会话对象统称为资源。这些资源在同一时刻是不能多线程共享的,为了让Dao、Service类可能做到singleton,Spring的事务同步管理器类使用ThreadLocal管理为不同事务线程提供了独立的资源副本,同时维护事务配置的属性和运行状态信息。事务同步管理器是Spring事务管理的基石部分,不管用户使用编程式事务管理,还是声明式事务管理,都离不开事务同步管理器。
Spring框架为不同的持久化技术提供了一套从同步管理器类(TransactionSynchronizationManager)中获取对应线程绑定资源的工具类,如图:
这些工具类还有一个重要用途:将特定异常转为Spring的DAO异常。
(四)、事务传播行为
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了七种传播行为:
传播行为 | 含义 |
---|---|
PROPAGATION_REQUIRED | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务 |
PROPAGATION_SUPPORTS | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 |
PROPAGATION_MANDATORY | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
PROPAGATION_REQUIRED_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NOT_SUPPORTED | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NEVER | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
PROPAGATION_NESTED | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 |
四、使用XML配置声明式事务
Spring的声明式事务管理通过Spring AOP实现的,通过事务的声明性信息,Spring负责将事务管理增强逻辑动态织入到业务方法相应的连接点中。这些逻辑包括获取线程绑定的资源、开始事务、提交、回滚事务、进行异常转换和处理等工作。
回滚规则的概念比较重要:它使我们能够指定价格什么样的异常导致自动回滚,什么样的异常不影响事务提交,这些规则可以在配置文件中通过声明的方式指定,同时,我们仍旧可以通过调用TransactionStatus#setRollbackOnly()方法编程式地回滚当前事务。通常,我们定义一条规则,声明MyApplicationException必须总是导致事务回滚。这种方式带来了显著的好处,它使用户的业务对象不必依赖事务设施。典型的例子是用户不必在代码中导入Spring API、事务代码等。
(一)、一个被实施事务增强的服务接口
BbtForum是业务层的接口,我们希望通过Spring的声明事务让这个接口的方法拥有适合的事务功能:
import com.baobao.springtest9.pojo.Forum;
import com.baobao.springtest9.pojo.Topic;
public interface BbtForum {
void addTopic(Topic topic);
void updateForum(Forum forum);
Forum getForum(int forumId);
int getForumNum();
}
BbtForum拥有4个方法,我们希望addTopic()和updateForum()方法拥有写事务的能力,而其他两个方法只需要拥有读事务的能力就可以了。BbtForumImpl对该接口进行了实现:
import com.baobao.springtest9.dao.ForumDao;
import com.baobao.springtest9.dao.PostDao;
import com.baobao.springtest9.dao.TopicDao;
import com.baobao.springtest9.pojo.Forum;
import com.baobao.springtest9.pojo.Topic;
import com.baobao.springtest9.service.BbtForum;
public class BbtForumImpl implements BbtForum {
private ForumDao forumDao;
private TopicDao topicDao;
private PostDao postDao;
public ForumDao getForumDao() {
return forumDao;
}
public void setForumDao(ForumDao forumDao) {
this.forumDao = forumDao;
}
public TopicDao getTopicDao() {
return topicDao;
}
public void setTopicDao(TopicDao topicDao) {
this.topicDao = topicDao;
}
public PostDao getPostDao() {
return postDao;
}
public void setPostDao(PostDao postDao) {
this.postDao = postDao;
}
public void addTopic(Topic topic) {
topicDao.addTopic(topic);
postDao.addPost(topic.getPost());
}
public void updateForum(Forum forum) {
forumDao.updateForum(forum);
}
public Forum getForum(int forumId) {
return forumDao.getForum(forumId);
}
public int getForumNum() {
return forumDao.getForumNum();
}
}
BbtForumImpl是一个POJO,只是简单使用持久层多个DAO类,通过它们的写作实现BbtForum接口的功能。在这里,我们看不到任何事务操作的代码,所以如果直接使用BbtForumImpl,这些方法都将以无事务的方式运行。现在,我们的任务是通过Spring声明事务配置让这些业务方法拥有适合的适合功能。
(二)、使用原始的TransactionProxyFactoryBean
声明式事务配置
从循序渐进的学习角度来看,通过TransactionProxyFactoryBean有助于我们理解更直接地认识Spring实施声明性事务的内在工作原理。
PROPAGATION_REQUIRED,readOnly
PROPAGATION_REQUIRED
通过TransactionProxyFactoryBean对业务类进行代理,织入事务增强功能。首先,需要为该代理类指定事务管理器,这些事务管理器实现了PlatformTransactionManager接口;其次,通过target属性指定需要代理的目标Bean,最后为业务Bean的不同方法配置事务属性。Spring允许我们通过键值对配置业务方法的属性信息,键可以使用通配符,如get*代表目标类中所有以get为前缀的方法。而ket=”*”匹配目标业务类所有的方法。
(三)、基于tx/aop命名空间的配置
使用TransactionProxyFactoryBran代理工厂类为业务类添加事务性支持,但它拥有一些以下明显的缺点:
(1)、需要对每个需要事务支持的业务类进行单独的配置;
(2)、在指定事务方法时,只能通过方法名进行定义,无法利用方法签名的其他信息进行定位(如方法入参、访问与修饰符等);
(3)、食物属性的配置串的规则比较麻烦,规则串虽然包括多项信息,但统一由逗号分隔的字符串来描述,不能利用IDE中的诱导输入功能,容易出错;
(4)、为业务类Bean添加事务支持时,在容器中既需要定义业务类Bean(通常命名为xxxTarget),又需要通过TransactionProxyFactoryBean对其进行代理以生成支持事务的代理Bean。实际上,我们只会从容器中返回代理的Bean,而业务类Bean仅是为了能代理才定义的,这样就造成相似的东西有两份配置,增强了配置信息量。
我们通过tx和aop命名空间对基于FactoryBean的事务配置方式进行替换
五、使用注解配置声明式事务
除了基于XML的事务配置之外,Spring还提供了基于注解的事务配置,即通过@Transactional对需要事务增强的Bean接口,实现类或方法进行标注,在容器中配置基于注解的事务增强驱动,即可启动基于注解的声明式事务。
(一)、使用@Transactional注解
//对业务类进行事务增强的标注
@Transactional
public class BbtForumImpl implements BbtForum {
private ForumDao forumDao;
private TopicDao topicDao;
private PostDao postDao;
public Forum getForum(int forumId) {
return forumDao.getForum(forumId);
}
public ForumDao getForumDao() {
return forumDao;
}
}
因为注解本身具有一组普适性的默认事务属性,所以往往只要为需要事务管理的业务类中添加一个@Transactional注解就完成了业务类事务属性的配置。
但是注解只是提供元数据,它本身并不能完成事务切面织入的功能。因此,我们还需要在Spring配置文件通过一行小小的配置通过Spring容器对标注@Transactional的Bean进行加工处理使事务注解生效:
(二)、关于@Transactional的属性
基于@Transactional注解的配置和基于XML的配置方式一样,它拥有一组普适性很强的默认事务属性,我们往往可以直接使用这些默认的属性就可以了:
(1)、事务传播行为:PROPAGATION_REQUIRED;
(2)、事务隔离级别:ISOLATION_DEFAULT;
(3)、读写事务属性:读/写事务;
(4)、超时时间:依赖于底层的事务系统的默认值;
(5)、回滚设置:任何运行期异常引发回滚,任何检查型异常不会引发回滚。
(三)、在何处标注@Transactional注解
@Transactional注解可以被应用于接口定义和接口方法、类定义和类的public方法上。
但Spring建议在业务实现类上使用@Transactional注解,当然我们也可以在业务接口上使用@Transactional注解。但这样会留下一些容易被忽视的隐患。因为注解不能被继承,虽在业务接口中标注的@Transactional注解不会被实现的业务类继承,如果通过以下的配置启用子类代理:
业务类不会添加事务增强,照样工作在非事务的环境下。举一个具体的实例:如果使用子类代理,假设用户为BbtForum接口标注了@Transactional注解,其实现类BbtForumImpl依旧不会启动事务机制。因此Spring建议在具体业务类上使用@Transactional注解。
(四)、在方法处使用注解
方法处的注解会覆盖类定义处的注解,如果有些方法需要使用特殊的事务属性,则可以在类注解的基础上,提供方法注解。
@Transactional //①类级的注解,适用于类中所有public的方法
public class BbtForumImpl implements BbtForum {
private ForumDao forumDao;
private TopicDao topicDao;
private PostDao postDao;
@Transactional(readOnly=true) //②提供额外的注解信息,它将覆盖①处的类级注解
public Forum getForum(int forumId) {
return forumDao.getForum(forumId);
}
public ForumDao getForumDao() {
return forumDao;
}
}
②处的方法注解提供了readOnly事务属性的设置,它将覆盖类级注解中默认的readOnly=false设置。
(五)、使用不同的事务管理器
在一般情况下,一个应用仅需要使用到一个事务管理器就可以了。如果希望在不同的地方使用不同的事务管理器,则可以通过如下的方式实现:
public class MultiForumService {
@Transactional("topic") //使用名为topic的事务管理器
public void addTopic(Topic topic)throws Exception{}
@Transactional("forum") //使用名为forum的事务管理器
public void updateForum(Forum forum){}
}
而topic和forum的事务管理器可以在XML中分别定义:
在①处,为事务管理器指定了一个数据源,每个事务管理器都可以绑定一个独立的数据源。在②处,指定了一个可被@Transactional注解引用的事务管理器标识。
六、小结
Spring声明式事务管理是Spring中的亮点,也是被使用最多的功能之一。Spring使声明式事务平民化,现在我们可以在Spring轻量级容器中享受这项曾经只能在臃肿、厚重的EJB应用服务器才拥有的功能。
Spring事务管理是SpringAOP技术的精彩应用的案例,事务管理作为一个切面织入到目标业务方法的周围,业务方法完全从事务代码中解脱出来,代码的复杂度大大降低。被织入的事务代码基于Spring事务同步管理器进行工作,事务同步管理器维护着业务类对象线程相关的资源。DAO类需要利用资源获取工具访问底层数据连接,或者直接建立在相应持久化模板类的基础上。要了解他们内部的机制,就必须事先了解ThreadLocal的工作机制。
Spring的事务配置相对来说比较简单,这些配置主要提供两方面的信息:其一,切点信息,用于定位实施事务切面的业务类方法;其二,控制事务行为的事务属性,这些属性包括事务隔离级别、事务传播行为、超时时间、回滚规则等。理解事务属性具体配置值的实际意义是非常关键的,因此本章9.1节对此专门进行了讲解。
Spring 3.0通过新的aop/txSchema命名空间 @Transactional注解技术,大大简化了声明式事务配置的难度。但是,对了解基于TransactionProxyFactoryBean代理类定义声明式事务的工作机制有助于理解事务增强的内部过程。