事务的概念
首先要理解事务。举个例子,使用电影在线选座,主要包括三个操作:选座,支付,生成电影票信息。如果选座成功,但是支付失败,系统如果没有释放座位,就会导致座位资源的流失;如果支付成功,但是生成电影票信息失败,就会导致消费者的经济损失,所以这三个操作必须要么都成功,要么都失败,这就必须靠事务来保证。在软件开发领域,有一个专业的术语用来描述事务所需要具备的特性——ACID。ACID主要指的是:
原子性(Atomic):事务由一个或多个行为组成,原子性指这些行为全部发生或者全部不发生,每个行为的结果是一样的。
一致性(Consistent):事务执行结束后,系统的数据必须保持一致性,比如A表有指向B表的外键,就不能存在B表的记录删除了而A表的外键记录还在,这就破坏了数据的一致性。这实际上需要通过原子性来保证。
隔离性(Isolated):多个事务之间不会相互干扰,并且不会导致最终数据的不一致性,这通常需要使用互斥的手段实现,比如各种粒度的锁机制。
持久性(Durable):事务执行完成后,事务的结果必须持久化到原有的数据中,而不应该存储到诸如内存等临时存储中,因为这样可能导致事务结果的丢失。
Spring事务管理
事务属性
Spring并不直接管理事务,而是通过各种事务管理器来调用特定平台的事务实现,用户在使用事务时不需要关心底层的实现。针对各个持久化场景,Spring提供了各个事务管理器,他们都继承了AbstractPlatformTransactionManager,实现了ResourceTransactionManager接口。这里面比较常用的事务管理器包括:
1.DataSourceTransactionManager:用于JDBC的持久化支持,也可用于IBATIS
2.HibernateTransactionManager:用于Hibernate3的持久化支持
3.JpaTransactionManager:用于Java持久化API的持久化支持
4.JtaTransactionManager:主要用于分布式事务的支持
在Spring中,事务包含有五种属性:
传播行为(propagation behavior)
传播行为定义了事务创建、启动或挂起的时机,以及某方法是否需要在事务环境下运行等。Spring提供了七种传播行为:
1.PROPAGATION_MANDATORY:表示该方法必须在事务中运行,若当前事务不存在则抛异常。举例来说,一个service方法调用了两个dao方法,那么在执行其中的一个dao方法时,都要求已经启动了事务,否则需要抛异常。
2.PROPAGATION_NESTED:表示若当前已存在一个事务,那么该方法将在嵌套事务中运行。嵌套事务可以独立于当前事务进行提交或回滚,若当前事务不存在,则该传播行为与PROPAGATION_REQUIRED一样。举例来说,一个service方法中调用了某dao方法,并且根据该dao方法执行的成功或失败区分了两套处理逻辑,这时候若dao执行失败,它只要保证不对数据库数据有任何影响(通过rollback)就可以了,不需要回滚整个service方法,故这个dao方法需要在嵌套事务中运行。
3.PROPAGATION_NEVER:表示当前方法不应该运行在事务中,若当前已经有一个事务在运行,则抛异常。这种适用于在某些dao操作中,它要求之前的事务已经结束,而它本身的操作结果不会受到其他dao操作结果的影响(比如被其他操作所rollback)。
4.PROPAGATION_NOT_SUPPORTED:表示当前方法不应该运行在事务中,若当前已经有一个事务在运行,则将其挂起。
5.PROPAGATION_REQUIRED:表示当前方法必须运行在事务中,若当前存在事务,则在原有事务中运行,否则创建一个新的事务。
6.PROPAGATION_REQUIRES_NEW:表示当前方法必须运行在自己的事务中,所以若存在当前事务,那么在该方法运行期间该事务会被挂起,以避免被其他方法使用,若不存在当前事务,则创建一个新的事务
7.PROPAGATION_SUPPORTS:表示当前方法不需要事务上下文,但是若当前事务已经存在,那么该方法就在这个事务中运行
其实决定传播行为的关键点在于:1.该方法需不需要启动事务管理 2.事务边界和该方法自己的边界是否一样。
隔离级别(isolation level)
在多个事务并发的情况下,一个事务不可避免的会受到其他事务执行结果的影响,包括脏读(某事务读取了其他事务未提交的数据后,其他事务又将其数据回滚,导致该事务使用了无效的数据),不可重复读(某事务执行两次相同的查询操作,但是由于在这两次操作中间存在其他事务更新了数据,从而导致两次查询的结果不一致),幻读(某事务读取数据,在还未读取完的时候,其他事务又插入了一些新的数据,导致原事务数据多了一些原本不存在的数据,这在一定程度上引入了紊乱)。若通过相关互斥机制保证事务的绝对隔离,则会很大程度影响并发的性能,最差情况就相当于事务是串行地执行。为了尽可能避免这些问题、权衡性能以及提高事务隔离的灵活性,Spring定义了五种隔离级别,以允许应用程序自己决定所能接受的、被其他事务所影响的程度。
1.ISOLATION_DEFAULT: 使用底层数据库默认的隔离级别。
2.ISOLATION_READ_UNCOMMITTED: 允许读取未提交的数据,这可能导致脏读、不可重复读和幻读。
3.ISOLATION_READ_COMMITTED:允许读取已提交的数据,这可以避免脏读,但是还是可能导致不可重复读和幻读。
4.ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果是一致的,除非由本事务自己更新,这可以避免不可重复读和脏读,但是还是可能导致幻读。
5.ISOLATION_SERIALIZABLE:完全按照ACID所要求的,可以避免脏读、不可重复读和幻读。注意:这种事务隔离级别的效率最差,因为它经常需要将事务相关的表进行加锁,锁粒度大。
只读(read only)
若事务声明为只读,则数据库就能够利用只读特性进行相关优化,比如提高只读事务的并发数等。注意:由于数据库对只读的优化是在启动事务时做的,所以只有对那些具备了可以创建一个新事务的传播行为的方法设置只读,才能够进行优化,设置只读特性才有意义。
超时(time out)
为了应用程序和底层数据库的运行效率,事务不能执行太长时间。你可以声明事务的超时时间,在规定时间内若没有执行完毕,则会自动回滚,无需等待异常。注意:超时时钟的启动是在事务开始时进行的,所以若该方法在传播行为中指明了无需事务管理,则“超时”的设置将不起作用。
回滚(roll back)
规定了哪些错误(Error)或异常(Exception)会导致事务回滚。默认情况下,事务遇到运行时异常(RuntimeException)时回滚,遇到被检查的异常(浅谈Java异常处理机制)时不会回滚。当然你可以设置,使事务遇到特定异常时进行回滚或者不回滚。
【这里捎带提一下Spring的持久层操作异常。Spring的DAO框架没有抛出与特定技术相关的异常,例如SQLException或HibernateException,抛出的异常都是与特定技术无关org.springframework.dao.DataAccessException类的子类,从而避免系统与某种特殊的持久层实现耦合在一起。DataAccessException是RuntimeException,是一个无须检测的异常,不要求代码去捕获并处理这类异常,遵循了Spring的一般理念:异常检测会使代码到处是不相关的catch或throws语句,使代码杂乱无章;并且NestedRuntimeException的子类,是可以通过NestedRuntimeException的getCause()方法获得导致该异常的另一个异常。】
事务管理实现
用户使用Spring实现事务通常有两种方式:编码实现和声明实现。顾名思义,编码实现是指在代码中调用Spring提供的API实现事务,这种实现允许用户自己控制事务的边界,比较的灵活,但是缺点是代码里需要包含跟业务逻辑无关的事务控制代码,从而和Spring事务管理产生一定耦合;声明实现指的是通过Spring AOP,在XML文件中配置事务管理,但是这种事务管理的粒度只能是单个方法,没办法深入方法里面的逻辑。假设用户使用的持久层为JDBC,下面讲讲如何实现两种方式的事务管理。
编码式实现
通过编码实现事务,主要有两种方法,一种只使用TransactionManager,一种使用TransactionTemplate。
1.使用TransactionManager
首先需要在Spring配置文件中加入事务管理器的Bean声明:
在需要使用事务的地方加上以下代码:
这样子就可以了,当数据库操作执行失败抛了异常,这里能捕获并执行rollback操作,若数据库操作执行成功,则通过commit将本次事务的结果持久化到数据库(持久性)。
2.使用TransactionTemplate
在配置文件中,需要加入TransactionTemplate的定义:
在需要使用事务的地方,加上以下代码:
声明式实现
看完了编码实现,现在看看声明式事务实现。在Spring中,声明式事务是通过事务属性(transaction attitude,包含了上文提到的五点)来定义的,它描述了事务策略是如何应用到方法上的。
首先在配置文件中,需要配置AOP和事务属性(txAdvice):
上面通过AOP和tx:advice的声明,指明了在执行哪些方法(例子是service层下的所有类的所有方法)时,需要怎么样的事务管理。
tx命名空间(xmlns:tx="http://www.springframework.org/schema/tx")的属性描述如下:
<tx:advice>:id用于指定此通知的名字, transaction-manager用于指定事务管理器,默认的事务管理器名字为“transactionManager”。
<tx:method>:用于定义事务属性即相关联的方法名。
name:定义与事务属性相关联的方法名,将对匹配的方法应用定义的事务属性,可以使用“*”通配符来匹配一组或所有方法,如“save*”将匹配以save开头的方法,而“*”将匹配所有方法。
propagation:事务传播行为定义,默认为“REQUIRED”,表示Required,其值可以通过TransactionDefinition的静态传播行为变量的“PROPAGATION_”后边部分指定,如“TransactionDefinition.PROPAGATION_REQUIRED”可以使用“REQUIRED”指定。
isolation:事务隔离级别定义;默认为“DEFAULT”,其值可以通过TransactionDefinition的静态隔离级别变量的“ISOLATION_”后边部分指定,如“TransactionDefinition. ISOLATION_DEFAULT”可以使用“DEFAULT”指定。
timeout:事务超时时间设置,单位为秒,默认-1,表示事务超时将依赖于底层事务系统。
read-only:事务只读设置,默认为false,表示不是只读。
rollback-for:需要触发回滚的异常,以“,”分割。默认任何RuntimeException 将导致事务回滚,而任何被检查的异常将不导致事务回滚;异常名字定义和TransactionProxyFactoryBean中含义一样
no-rollback-for:不触发回滚的 异常;以“,”分割。异常名字定义和TransactionProxyFactoryBean中含义一样。
除了<tx:advice>元素,tx命名空间还提供了<tx:annotation-driven>元素,该元素允许用户通过“注解”启用Spring事务管理。
一般情况下,只需要在配置文件中加入:
<tx:annotation-driven>元素会让Spring检查上下文中所有的Bean,对@Transactional注解的Bean添加相应的事务管理策略。若@Transactional用在类上,则是对该类所有的方法启用事务管理,若用在方法上,则是针对该方法启用事务管理。该元素的属性描述如下:
transaction-manager,默认transactionManager,事务管理器的名字,仅当事务管理器不是transactionManager时,需要显示指定。
mode,默认proxy,proxy模式仅当外部调用产生时才产生代理。可替代的选项是aspectj,通过更改类的字节码来提供事务功能的织入。aspectJ织入需要提供spring-aspects.jar包支持。
proxy-target-class,默认为false。只适用于proxy方式,用于控制代理创建的方式。当值为true时,基于class类型的代理(cglib)将被创建;当值为false,基于标准的JDK接口的代理将被创建(可以参考 java动态代理(JDK和cglib))。
order,默认为Orderd.LOWEST_PRECEDENCE,定义多个advice执行的次序,默认情况下由系统决定。
在需要使用事务的地方,加上以下代码:
@Transactional注解的属性描述如下:
propagation:指定事务传播行为,默认为Required,使用Propagation.REQUIRED指定;
isolation:指定事务隔离级别,默认为“DEFAULT”,使用Isolation.DEFAULT指定;
readOnly:指定事务是否只读,默认false表示事务非只读;
timeout:指定事务超时时间,以秒为单位,默认-1表示事务超时将依赖于底层事务系统;
rollbackFor:指定一组异常类,遇到该类异常将回滚事务;
rollbackForClassname:指定一组异常类名字,其含义与<tx:method>中的rollback-for属性语义完全一样;
noRollbackFor:指定一组异常类,即使遇到该类异常也将提交事务,即不回滚事务;
noRollbackForClassname:指定一组异常类名字,其含义与<tx:method>中的no-rollback-for属性语义完全一样;
Spring提供的@Transaction注解事务管理内部同样利用环绕通知TransactionInterceptor实现事务的开启及关闭。使用该注解进行事务管理需要特别注意以下几点:
1.如果在接口、实现类或方法上都指定了@Transactional 注解,则优先级顺序为方法>实现类>接口;建议只在实现类或实现类的方法上使用@Transactional,而不要在接口上使用,这是因为如果使用JDK代理机制是没问题,因为其使用基于接口的代理;而使用CGLIB代理机制时就会出现问题,因为其使用基于类的代理而不是接口,接口上的@Transactional注解是“不能被继承的”。另外,在JDK代理机制下,“自我调用”同样不会应用相应的事务属性的。
2.在使用Spring代理时,默认只有在public可见度的方法的@Transactional 注解才是有效的,其它可见度(protected、private、包可见)的方法上即使有@Transactional 注解也不会应用这些事务属性的,Spring也不会报错,如果你非要使用非公共方法注解事务管理的话,可考虑使用AspectJ框架。