Sprin事务管理机制
什么是事务?
事务的好处?
如何使用事务?
1、认识事务的本身
对于一个软件系统来说,需要相应的数据资源(如数据库,文件系统等)来保存系统状态,在对系统资源进行操作的时候,必须要保证系统资源处于一种“正确”的状态,因此我们需要对这些操作进行一些必要的限定来保证提供的完整性,而事务就是以可控的方式对数据库资源进行访问的一组操作。为保证在事务的执行前后,数据库资源承载的系统状态始终处于“正确”状态,事务本身持有4个限定属性,即原子性、一致性、隔离性和持久性,即ACID属性。
原子性:原子性要求事务所包含的全部操作是一个不可分割的整体,这些操作要么全部提交成功,要么其中一个操作失败,就全部失败。
一致性:一致性要求事务所包含的操作不能违反数据资源的一致性检查,数据资源在事务执行前处于某一个数据一致性状态,那么,事务执行之后也依然需要保持数据间的一致性检查。
隔离性:事务的隔离性主要规定了各个事务之间相互影响的程度。隔离性概念主要面向对数据资源的并发访问,并兼顾影响事务的一致性。当两个事物或者更多事务同时访问同一个数据资源的时候,不同的隔离级别决定了各个事务对该数据资源访问的不同行为。事务有四种隔离级别,从弱到强分别是Read Uncommitted、 Read Committed 、 Repeatable Read 和Serializable:
(1):Read Uncommitted.它是最低隔离级别。最直接的效果就是,一个事务可以读取另一个事务没有提交的更新结果,从而导致无法避免脏读、不可重复读、幻读这三个问题。
脏读(脏读的重点是回滚):如果一个事务对数据进行了更新,但事务还没有提交,另一个事务就可以读取到他还没有提交的更新结果。如果此时第一个事务回滚,那么第二个事务在此之前所“看到”的就是一笔脏数据。
不可重复读(不可重复读的重点是修改):不可重复读是指同一个事务在整个过程中对同一笔数据进行读取,每次读取结果都不同。如果事务1在事务2的更新操作之前读取一次数据,在事务2更新之后再读取一次数据,这两次结果是不同的。
幻读(幻读的重点是插入或删除):事务1执行同样的条件对数据进行两次或多次查询,第一次和第二次查询期间事务2对数据进行了删除或者新增操作,导致事务1的查询结果不一样。
(2):Read Committed。它是大部分数据库采用的默认隔离级别。在改隔离级别下,一个事务的更新操作结果只有在该事务提交之后,另外一个事物才能读取同一笔数据更新后的结果。这样可以避免脏读,但是无法避免不可重复读和幻读。
(3)Repeatable Read。它的隔离级别可以保证在整个事务的过程中,对同一笔数据的读取结果是相同的,不管其他事务是否同时在对同一笔数据进行更新,也不管其他事务对数据的更新提交与否。因此避免了脏读和不可重复读。
(4)Serialzable。它是最严格的隔离级别,所有的事务操作必须依次顺序执行,这样可以避免以上所有情况,但是降低了系统的吞吐量和并发度。不推荐使用。
持久性:事务的持久性是指,一旦整个事务操作成功,对数据所做的变更将被记载并不可逆转。
2、事务的家族成员
在一个典型的事务处理场景中,有以下几个参与者:
(1)Resource Manager.简称RM,它负责存储并管理系统数据资源的状态,比如数据库服务器、JMS消息服务器等都是相应的Resource Manager。
(2)Transaction Pricessing Monitor。简称TPM或者TP Monitor,它的职责是在分布式事务场景中协调包含多个RM的事务处理。TP Monitor 通常对应特定的软件中间件。随着软件开发技术的进步,TP Monitor的实现也由原来基于过程式的设计与实现,转向面向对象的更趋模块化的设计和实现。通常J2EE规范中的引用服务器(Application Server)担当的就是TP Monitor的角色。
(3)Transaction Manager。简称为TM,它可以认为是TP Monitor中的核心模块,直接负责父RM之间事务处理的协调工作,并且提供事务界定、事务上下文传播等功能接口。
(4)Appliaction。以独立形式存在的或者运行于容器中的应用程序,可以认为是事务边界的触发点。
实际上,并非每个事务的场景中都会出现以上提到的所有参与者,如果根据整个事务中涉及的RM的多寡来区分事务的类型的话,可以将事务分为两个类,即全局事务和局部事务。
全局事务:如果整个事务处理过程中有多个RM参与,那么就需要引入TP Monitor来协调多个RM之间的事务处理。TP Monitor将采用两阶段提交协议来保证整个事务的ACID属性。这种场景下的事务称为全局事务或者分布式事务。
局部事务:如果当前事务只有一个RM参与其中,我们就可以称当前事务为局部事务。比如,在当前事务中只对一个数据库进行更新,或者只向一个消息队列中发送消息的情况,都属于局部事务。
基于java的事务管理,java平台的局部事务支持和全局事务支持。
Java平台的局部事务支持:在java的局部事务场景中,系统里事务管理的具体处理方式,会随着所使用的数据访问技术的不同而各异。我们不是使用专用的事务API来管理事务,而是通过当前使用的数据访问技术所提供的基于connection(对于jdbc是java.sql.connection,Hibernate是session)的API来管理事务。
数据库资源的局部访问事务管理。要在对数据库的访问过程中进行事务管理,每种数据访问技术都提供了特定于它自身的事务管理API,比如JDBC是java平台访问关系数据库最基础的API。如果直接使用JDBC对数据访问的话,我们可以将数据连接java.sql.Connection的自动提交AutoCommit功能设置为false,改为手动提交来控制整个事务的提交或者回滚。而如果我们使用Hibernate进行数据访问,那就得使用Hibernate的Session进行数据访问期间的事务管理。
Java平台的分布式事务支持:主要是通过JTA(java transaction API)提供支持的。
基于JTA的分布式事务管理:JTA是sun 公司提出的标准化分布式事务访问的java接口规范。不过,JTA规范定义的只是一套java接口定义,具体的实现留给了相应的供应商去实现,各个java ee应用服务器需要提供对JTA的支持使用JTA进行分布式事务管理有两种方式,一是直接使用JTA接口的编程事务管理,二是基于应用服务器的声明式事务管理。
JTA编程事务管理:使用JTA进行分布式事务的编程式事务管理,通常使用javax.transaction.UserTransaction接口进行,各应用服务器都提供了针对它的JNDI查找服务。
JTA声明式事务:如果使用EJB进行声明式的分布式事务管理的话(限于CMT(Container Managed Transaction)的情况),JTA的使用则只限于EJB容器内部,对于应用程序来说则完全就是透明的。现在唯一需要的工作实际上就是在相应的部署描述符中指定相应的事务属性即可。
3、Spring事务王国的架构
Spring的事务框架将开发过程中事务管理相关的关注点进行适当的分离,并对这些关注点进行合理的抽象,最终打造出一套方便的使用,功能强大的事务管理“利器”。通过Spring的事务框架,我们可以按照统一的编程模型来进行事务编程,而不用关心所使用的数据访问技术以及具体要访问什么类型的事务资源。并且Spring的事务框架与Spring提供的数据访问支持可以紧密结合,更是让我们在事务管理与数据访问之间游刃有余。而最主要的是,结合Spring的AOP框架,Spring的事务框架为我们带来了原来只有CMT才有的声明式事务管理,而无需绑定到任何应用服务器上。
Spring的事务管理框架设计理念的基本原则是:让事务管理的关注点和数据访问关注点进行分离。
当在业务层使用事务的抽象API进行事务界定的时候,不需要关心事务将要加诸于上的事务资源是什么,对不同的事务资源的管理将由相应的框架实现类来操心。
当在数据访问层对可能参与事务的数据资源进行访问的时候,只需要使用相应的数据访问API进行数据访问,不需要关心当前事务资源如何参与事务或者是否需要参与事务。这同样由事务框架来打理。
3.1、统一中原的过程
org.springframework.transaction.PlatformTransactionManager是Spring事务抽象架构的核心接口,它的主要作用是为引用程序提供事务界定的统一方式。
public interface PlatformTransactionManager{
TransactionStatus getTarnsaction(TransactionDefinition definition)throws Transaction Exception;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
};
PlatformTransactionManager是整个事务抽象策略的顶层接口,它就好像我们的战略蓝图,而战略的具体实施则将由相应的PlatformTransactionManager实现类来执行。Spring的事务框架针对不同的数据访问方式以及全局事务场景,提供了相应的PlatformTransactionManager实现类。这里我们先以JDBC数据访问方式的局部事务管理为例。对于层次划分清晰的应用来说,我们通常都是将事务管理放在service层,而将数据访问逻辑放在DAO层。这样做的目的在于可以不用因为将事务管理代码放在DAO层,而降低数据访问逻辑的重用性,也可以在service层根据相应的逻辑,来决定提交或者回滚事务。如:
Public void serviceMethod(){
Object txObject = transactionManager.beginTransaction();
...
dao1.doDataAccess();
dao2.doDataAccess();
...
transactionManager.commitTrasaction(txObject);
}
因为JDBC的局部事务控制是由同一个java.sql.Connection来完成的,所以要保证两个DAO的数据访问方法处于同一个事务中,我们就得保证它们使用的是同一个java.sql.Connection。要做到这一点,通常会采用称为connection-passing的方式,即为同一个事务中的各个dao的数据访问方法传递当前事务对应的同一个java.sql.Connection。如:
Public void serviceMethod(){
Object txObject = transactionManager.beginTransaction();
Connection connection = (Connection)txObjection;
...
dao1.doDataAccess(connection );
dao2.doDataAccess(connection );
...
transactionManager.commitTrasaction(txObject);
}
但是这种方式最致命的问题就是无法摆脱java.sql.Connection的纠缠,而且数据访问对象的定义要绑定到具体的数据访问技术上来,现在是用jdbc进行访问,就要声明java.sql.Connection的依赖,如果是hibernate的话就要声明对session的依赖。因此可以采用把整个事务对应的java.sql.Connection(session,以下省略)实例放在统一的一个地方(TransactionResourceManage)去,无论谁要取用改资源,都直接从这地方来获取,这样就解除了事务管理代码和数据访问之间通过java.sql.Connection的“直接”耦合。具体的说就是在事务开始之前就取得一个java.sql.Connection,然后将这个Connection绑定到当前的调用线程。之后,数据访问对象在使用Connection进行数据访问的时候,就可以从当前绑定线程上获取这个事务开始的时候绑定的Connection实例。当所有的数据访问对象全部使用这个绑定到当前线程的Connection完成了数据访问工作时,我们就使用这个Connection实例提交或者回滚事务,然后解除它到当前线程的绑定。
但是现在每个个数据访问对象都只能通过TransactionResourceManage来获取数据资源接口。另外如果当前数据访问对象对应的数据方法不想参与跨越多个数据操作事务的话,甚至不想使用类似的事务管理支持,是否就意味着无法获得connection进行数据访问了呢? DatasuorceUtils就是管理connection,DatasourceUtils会从类似TransactionResourceManager(Spring中对应org.springframework.transaction support.TransactionSynchronizationSynchronizationManager)哪里获取Connection资源。如果当前线程之前没有绑定任何connection,那么它 就通过数据访问对象的DataSource引用获取新的connection,否则就使用绑定的那个connection。因为它提供了Spring事务管理框架在数据访问层需要提供的基础设施中不可或缺的一部分,而JbdcTemplate等类内部已经使用DataSourceUtils来管理连接了,所以我们不用担心着细节。
从这里可以看出Spring的事务管理与它的数据访问框架是紧密结合的。
3.2、Spring事务抽象的3个主要接口
TransactionDefinition:定义事务的相关属性,包裹隔离级别、传播行为、超时时间、是否为只读事务等。
PlatformTransactionManager:根据TransactionDefinition来开启相关事务。
TransactionStatus:负责事务开启到结束期间的状态,对事物进行有限的控制。
3.2.1、TransactionDefinition
TransactionDefinition主要定义了那些事务属性可以指定:事务的隔离级别,事务的传播行为,事务的超时时间,是否为只读事务。
事务的隔离级别分五种:ISOLATION_DEFAULT,表示使用数据库默认的隔离级别,通常情况下是指Read Committed;ISOLATION_READ_UNCOMMITTER;对应Read Uncommitted隔离级别,无法避免脏读不可重复读和幻读;ISOLATION_READ_COMMITTED隔离级别,可以避免脏读,但是无法避免不可重复读和幻读;ISOLATION_REPEATABLE_READ隔离级别,可以避免脏读和不可重复读,但是无法避免幻读;ISOLATION_SERIALIZABLE隔离级别,可以避免所有的脏读、不可重复读、幻读,但并发效率低。
事务的传播行为:表示整个事务处理过程所跨的业务对象,将以什么样的行为参与事务(我们将在声明式事务中更多的依赖该属性)。事务的传播行为有如下几种:PROPAGATION_REQUIRED:表示当前事务存在的话,则加入当前事务,如果不存在则创建一个事务,总之,要至少保证存在一个事务;通常默认为事务的传播行为
PROPAGATION_SUPPORT表示如果当前存在一个事务,则加入当前事务,如果不存在事务,则直接执行;对于一些查询方法来说,通常是比较适合的传播行为选择。
PROPAGATION_MANDATORY:强制要求当前存在一个事务,如果不存在,则抛出异常
PROPAGATION_REQUIRES_NEW:不管当前事务是否存在,都会创建一个新的事务。如果当前存在事务,会将当前事务挂起。如果某个业务对象所做的事情不想影响到外层事务,则适合选择
PROPAGATION_NOT_SUPPORTED:不支持当前事务,而是在没有事务的情况下执行
PROPAGATION_NEVER:永远不需要当前存在事务
PROPAGATION_NESTED:如果存在当前事务,则再当前事务的一个嵌套事务中执行,否则与PROPAGATION_REQUIRED的行为类似,即创建新的事物,在新创建的事务中执行。 其应用场景在于,你可以将一个大的事务划分为多个小的事务来处理,并且外层事务可以根据各个内部嵌套事务的执行结果,来选择不同的执行流程。比如,某个业务对象的业务方法A.service(),可能调用其他业务方法B.service()向数据库中插入一批业务数据,但当插入数据的业务放大出现错误的时候(比如主键冲突),我们可以在当前事务中捕捉前一个方法抛出的异常,然后选择另一个更新数据的业务方法C.service()来执行。这时就可以吧B.service和C.service方法的传播行为指定为PROPAGATION_NESTED。
3.2.2、TransactionStatus
该接口定义整个事务处理过程中的事务状态,更多的时候,我们将在编程式事务中使用该接口。在事务处理的过程中,我们可以使用TransactionStatus进行如下工作:使用TransactionStatus提供的响应方法查询事务的状态;通过setRollbackOnly()方法标记当前事务以使其回滚;如果响应的PlatfromTransactionManager支持Savepoint,可以通过TransactionStatus在当前事务中创建内部事务。
3.2.3、PlatformTransactionManager
PlatformTransactionManager是Spring事务抽象框架的核心组件,我们之前已经提过了它的定义以及使用,PlatformTransactionManager的整个抽象体系基于Strategy(策略)模式,由PlatformTransactionManager对事务界定进行统一抽象,而具体的界定策略的实现则交由具体的实现类(eg:DataSourceTransactionManager、HibernateTransactionManager等)
3.3、使用Spring进行事务管理
3.3.1、编程式事务管理
通过Spring进行编程式事务管理有两种方式,要么直接使用PlatformTransactionManager,要么使用更方便的TransactionTemplate。二者各有优点,但更推荐PlatformTransactionManager进行编程式事务。
PlatformTransactionManager接口定义了事务界定的基本操作,我们可以直接使用PlatformTransactionManager进行编程式事务管理,只要为PlatformTransactionManager提供合适的实现类,然后结合TransactionDefinition开启事务,TransactionStatus来回滚或者提交事务,就可以完成针对当前对象的整个事务管理。但是,直接使用PlatformTransactionManager缺点会很明显,从抽象事务操作以屏蔽不同事务管理API差异的角度看,PlatformTransactionManager可能已经足够了,但是从应用程序开发的角度来看,却依然过于底层,只是期间的这些差异处理就够我们忙活,如果要在每个需要事务管理的地方,全部使用PlatformTransactionManager进行事务管理,那么代码的数量将会是惊人的。
鉴于PlatformTransactionManager进行事务管理的流程比较固定,各个事务管理期间只有部分逻辑存在差异,我们可以考虑像Spring的数据访问层那样,使用模板方法模式与Callback()相结合的方式,对直接使用PlatformTransactionManager进行事务管理的代码进行封装。这就有了更方便的编程式事务管理方式,即使用TransactionTemplate的编程式事务管理。
TransactionTemplate对PlatformTransactionManager相事务界定操作以及异常处理进行了模板化封装,开发人员更多地关注与通过相应的Callback接口提供具体的事务界定即可。Spring针对TransactionTemplate提供了两个Callback接口,TransactionCallback和TransactionCallbackWithoutResult,二者的唯一区别是是否返回执行结果。
3.3.2、声明式事务管理
直接使用编程式事务管理的不足就是,事务管理代码与业务逻辑代码相互混杂,而声明式事务则可以避免这种不同系统的关注点之间的纠缠,使得事务管理代码不用再去影响业务逻辑的实现。如果我们从原来硬编码事务管理的系统中,将这些事务管理相关的代码从业务对象总剥离出来,为每一个service提供一个transaction管理对象,将事务管理逻辑集中到具体的类中即可,而service实现类可以对事务管理一无所知,只要保证针对所有的service的调用必须走transaction对应的实体类即可。但是这种方法依旧要在每个service对象中实现一个相应的transaction对象。因此,Spring Aop(动态代理原理)对这个进行了很好的实现。事务管理本身就是一种横切关系,与其他的横切还珠店本质上没有任何区别,所以饿哦们完全可以为其提供相应的Advice实现,然后织入到系统中需要该横切逻辑的Joinpoint处即可。我们要做的就是提供一个拦截器,在业务方法开始执行之前开启一个事务,当方法执行完成或者异常退出的时候就提交事务或者回滚事务。
如何判断哪些方法需要事务支持,以及事务的TransactionDefinition相关信息又从哪里获取?
如何对方法抛出的异常处理以及哪些需要回滚哪些不需要?
我们需要提供某种方式来记载业务方法与对应的事务信息之间的映射关系,拦截器只要查询这种映射关系,就可以知道要不要为当前业务方法创建事务。如果要创建事务,则以业务方法作为标志,到映射关系中查找创建事务所需要的信息,然后创建事务。我们当然可以将这种信息写死到拦截器实现类中,不过,将他们放在外部配置文件总(xml)或者java源代码的注解中才是当下比较流行的方式。这种映射信息正规来讲,叫做驱动事务的元数据。有了类似于PrototypeTransactionInterceptor这样的Aop装备的支持,系统的事务管理就变成了只需要在元数据总声明相应的事务控制信息。
4、Spring中的本地线程变量ThreadLocal
在之前的事务管理实现原型中,我们展示了ThreadLocal在Spring的事务管理框架中所起的核心作用,即通过ThreadLocal的使用来避免connection-passing方式最初的尴尬局面。但ThreadLocal的基本原理又是怎样的呢?
ThreadLocal是java语言提供的用于支持线程局部变量的标准实现类。
简单的说,ThreadLocal就像是一个窗口,通过这个窗口,我们可以将特定于线程的数据资源绑定到当前线程,也可以通过这个窗口获取绑定的数据资源,当然,更可以解除之前绑定到当前线程的数据资源。在整个线程的生命周期汇总,我们都可以通过ThreadLocal这个窗口与当前线程打交到。
详细参见:http://www.iteye.com/topic/103804