数据库是项目开发过程中必不可少的一个组件,而数据库事务则是核心流程中经常使用的一种技术。我们来聊一聊Spring中与数据库事务相关的一些技术细节。
首先我们来看下数据库事务的基本性质,概括起来即ACID性质:
原子性(Atomicity):要么全做,要么全部不做。也就是说,如果事务成功提交则它的操作全部完成,相反如果事务失败回滚则它的操作全部撤销。
一致性(Consistency):在事务前后数据库始终处于一致性状态。这意味着在事务执行过程中,数据库完整性都不会被破坏。
隔离性(Isolation):保证事务不受其他并发事务影响。隔离程度从弱到强可以分为四个级别,Read Uncommitted、Read Committed、Read Repeatable和Serializable。下文做进一步讨论。
持久性(Durability):事务完成后,改变是永久的。也就是说,事务成功提交后,它的操作无论如何都不会被 撤销 。
我们先了解下几个读现象:脏读、不可重复读、幻读。
为了方便说明,先定义一张数据库表users,它有如下两行记录:
id | name | age |
---|---|---|
1 | Joe | 20 |
2 | Jill | 25 |
脏读指一个事务能够读到其他事务还没提交的操作。
假如有两个事务并发执行,如下所示:
事务2改变了数据但还 没有 提交,这时候事务1读到了事务2还没有提交的数据。假如事务2回滚了,那么事务1看到的数据视图是错误的。
不可重复读指在一个事务执行过程中,一行记录被读取两次但在这两次读取中这行记录的数据不相同。
假如有两个事务并发执行,如下所示:
例子中事务2成功提交,意味着它对id为1的记录的改动生效。而对于事务1来说,它在两次读取中看到了该记录不同的age值。
幻读指在一个事务执行过程中,有两次相同的查询,但第二次看到数据集合比第一次多,看到了幽灵般出现的新数据。
假如有两个事务并发执行,如下所示:
例子中事务1执行了两次相同的查询,在第二次查询看到了事务2新插入并提交的数据。
这里我们对 不可重复读 和 幻读 加以区分: 不可重复读 指事务原先所读到的数据被修改或删除了,不可重复读取;而 幻读 则指事务在执行相同查询时读到了新增加的数据,读到幻象般出现的数据。
隔离级别从弱到强可以分为四个等级,Read Uncommitted、Read Committed、Read Repeatable和Serializable。
隔离级别与读现象联系如下:
从上图可以看出,Read Uncommitted隔离程度最弱,三种读现象都可能发生;而Serializable隔离程度最强,这三种读现象都不会发生。
在对Spring数据库事务做进一步讨论前,我们先通过Spring的一个事务管理接口了解事务整体抽象:
public interface PlatformTransactionManager { TransactionStatus getTransaction( TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }
PlatformTransactionManager有三个方法,分别用于获取事务、提交事务和回滚事务。
对于获取事务的 getTransaction(..) 接口,其参数为 TransactionDefinition ,用于表示我们希望获取什么样的事务;返回值为 TransactionStatus ,代表一个事务,我们可以通过它来控制事务执行以及获取事务状态。
TransactionDefinition定义了如下属性:
对于这些属性含义下文会做进一步讨论,现在只需要知道一个整体概念。
目前项目的数据库datasource有不同的实现,譬如JDBC、Hibernate、JTA等等,因此PlatformTransactionManager也有不同的实现。
以下为定义一个JDBC datasource并且使用相应的事务管理器:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
了解了一个整体抽象后,现在我们来研究怎么启用Spring数据库事务。
我们可以通过 代码编程方式 和 声明方式 来使用Spring数据库事务,但在大多数情况下都使用声明方式来使用事务,因此这里只讨论声明方式的具体细节。
在实际配置前,我们先了解下Spring实现声明式事务的整体框架。
Spring在声明式事务上使用了 AOP代理 来实现,如下所示:
如上图,Target Method为我们写的一个类方法,并使用了声明式事务。当我们在 IOC容器 中通过依赖注入获取该类的实例时,我们获取到的其实是一个AOP代理。当我们调用该Target Method时,其实是先调用了一个AOP代理的方法,AOP代理通过使用 Transaction advisor 和 PlatformTransactionManager 来实现事务,最后才调用了Target Method。
现在来看个具体例子。
我们有一个FooService接口和其实现:
package x.y.service; public interface FooService { Foo getFoo(String fooName); Foo getFoo(String fooName, String barName); void insertFoo(Foo foo); void updateFoo(Foo foo); }
package x.y.service; public class DefaultFooService implements FooService { public Foo getFoo(String fooName) { //...getFoo... } public void insertFoo(Foo foo) { //...insertFoo... } public void updateFoo(Foo foo) { //...updateFoo... } }
而事务配置如下:
<!-- from the file 'context.xml' --> <?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: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.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- this is the service object that we want to make transactional --> <bean id="fooService" class="x.y.service.DefaultFooService"/> <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) --> <tx:advice id="txAdvice" transaction-manager="txManager"> <!-- the transactional semantics... --> <tx:attributes> <!-- all methods starting with 'get' are read-only --> <tx:method name="get*" read-only="true"/> <!-- other methods use the default transaction settings (see below) --> <tx:method name="*"/> </tx:attributes> </tx:advice> <!-- ensure that the above transactional advice runs for any execution of an operation defined by the FooService interface --> <aop:config> <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/> </aop:config> <!-- don't forget the DataSource --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/> <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/> <property name="username" value="scott"/> <property name="password" value="tiger"/> </bean> <!-- similarly, don't forget the PlatformTransactionManager --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- other <bean/> definitions here --> </beans>
配置貌似有点复杂,不要慌,我们来一行行解析。
这里声明了一个fooService对象,希望它使用事务。
我们定义了一个事务语义 <tx:advice/> ,它可以理解成这样:所有以‘get’开头的方法都执行在一个 只读 的事务中,其他的方法则执行在默认配置的事务中。 <tx:advice/> 的transaction-manager属性指明了用来管理事务的PlatformTransactionManager。
<aop:config/>则定义事务语义txAdvice在程序中什么地方执行。在 <aop:config/> 中我们先定义了一个匹配FooService接口任何操作的 pointcut ,然后通过 advisor 将该 pointcut 关联到txAdvice。
综合起来就是,在执行FooService接口任何操作时,使用由txAdvice定义的事务。
advice、advisor、pointcut这些概念理解起来有点晕?
其实,poincut描述在什么地方,advice描述做什么事情,advisor则将pointcut和advice结合起来,描述了在什么地方执行什么事情。这样理解是不是好多了?:)
在具体实现上,通过以上声明配置Spring其实对 FooService 包装了一个AOP代理,该AOP代理配置使用了相应事务advice。当我们调用 FooService 的方法时,其实调用了该AOP代理,代理创建使用事务,并标识成事务只读,最终调用 FooService 的方法。
上面介绍了使用xml声明方式来使用事务,我们也可以使用基于注解的方式。
使用事务注解的类定义:
@Transactional public class DefaultFooService implements FooService { Foo getFoo(String fooName); Foo getFoo(String fooName, String barName); void insertFoo(Foo foo); void updateFoo(Foo foo); }
在xml中启用注解:
<!-- from the file 'context.xml' --> <?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: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.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- this is the service object that we want to make transactional --> <bean id="fooService" class="x.y.service.DefaultFooService"/> <!-- enable the configuration of transactional behavior based on annotations --> <tx:annotation-driven transaction-manager="txManager"/><!-- a PlatformTransactionManager is still required --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- (this dependency is defined somewhere else) --> <property name="dataSource" ref="dataSource"/> </bean> <!-- other <bean/> definitions here --> </beans>
可以看到,这种基于注解的事务方式比上面基于xml的事务方式少了AOP配置,我们只需要另外增加一行 <tx:annotation-driven …/> 就可以了。
无论基于xml还是基于注解,事务属性除了是否只读还有其他一些属性。上面配置中我们没有具体指明这些属性值,其实是使用了这些属性的默认值:
我们可以改变这些属性默认值。这些属性是依赖
属性 | 是否必要 | 默认 | 描述 |
---|---|---|---|
name | 是 | 事务属性关联的方法名 | |
propagation | 否 | REQUIRED | 事务传播行为 |
isolation | 否 | DEFAULT | 事务隔离级别 |
timeout | 否 | -1 | 事务超时时间(单位秒) |
read-only | 否 | false | 事务是否只读 |
rollback-for | 否 | 导致事务回滚的异常;以逗号分割。 | |
no-rollback-for | 否 | 不导致事务回滚的异常;以逗号分割。 |
其中,Rollback(rollback-for、no-rollback-for)和Propagation有些细节需要额外注意,下面做些探讨。
Spring建议的做法是,我们通过抛出异常方式来回滚事务。当抛出异常时,Spring事务框架会捕获异常,决定是否回滚事务,然后再重新抛出该异常。
在默认情况下,Spring事务框架只会对于 RuntimeException 和 Error 回滚事务,其他异常则不会回滚事务。
我们可以指定异常类型 回滚 或 不回滚 事务,如下所示:
<tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/> <tx:method name="*"/> </tx:attributes> </tx:advice> <tx:advice id="txAdvice"> <tx:attributes> <tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/> <tx:method name="*"/> </tx:attributes> </tx:advice>
事务传播指的是,多个使用事务的方法存在相互调用时,各自的事务是怎么相互影响的。
下面是事务传播的行为:
行为 | 描述 |
MANDATORY | 使用当前已存在的事务,如果当前没有处于事务中则抛出异常 |
NEVER | 以非事务方式执行,如果当前已经处于一个事务中则抛出异常 |
NOT_SUPPORTED | 以非事务方式执行,如果当前已经处于一个事务中则挂起该事务 |
REQUIRED | 使用当前已存在的事务,如果没有则创建一个新事务 |
REQUIRES_NEW | 创建一个新事务,如果当前已处于一个事务中则挂起该事务 |
SUPPORTS | 使用当前已存在的事务,没有则以非事务方式执行 |
NESTED | 如果当前已经处于一个事务中则在一个嵌套事务中执行,如果没有则创建一个新事务 |
举个例子,假如存在两个使用 Propagation.REQUIRED 事务的方法,它们调用关系如下:
当我们调用方法1时,会创建一个新事务;方法1调用方法2时,使用当前事务。当方法1最终返回时,整个事务才会提交或者回滚。
这意味着,在方法2抛出异常或者设置回滚状态会影响方法1的事务提交,因为它们本质上属于同一个事务。
再举个例子。假如存在两个使用Propagation.REQUIRES_NEW事务的方法,它们调用关系如下:
但我们调用方法1时,创建一个新事务;当方法1调用方法2时,会挂起方法1中的事务,创建一个新事务并执行。当方法2返回时,方法2的事务提交或回滚;当方法1返回时,方法1的事务提交或回滚。方法1和方法2的事务相互独立不受相互影响。
本文从数据库事务性质到Spring数据库事务支持进行了一些技术探讨,如有纰漏恳请指出。
下面是一个实际项目当中经常会遇到的一个坑。
假如有一个FooService服务,它有两个方法func1和func2,func2使用事务,func1调用func2。如下所示:
那么当我们调用FooService.func1时,func2的事务配置会生效么?
答案是不会。如前文所述,Spring数据库事务是AOP代理实现的,当我们调用func1时,其实是调用AOP代理,由于func1没有使用事务,因此这时候AOP代理不会创建使用事务;而func1调用func2时,这时候其实并 没有 经过AOP代理而是直接调用,因此也不会生成所希望的事务。:)