Spring 数据库事务
数据库是项目开发过程中必不可少的一个组件,而数据库事务则是核心流程中经常使用的一种技术。我们来聊一聊 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 数据库事务做进一步讨论前,我们先通过 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定义了如下属性:
- Isolation:事务隔离级别
- Propagation:事务传播类型
- Timeout:事务执行的超时时间
- Read-Only:事务是否为只读
对于这些属性含义下文会做进一步讨论,现在只需要知道一个整体概念。
目前项目的数据库 datasource 有不同的实现,譬如 JDBC、Hibernate、JTA 等等,因此 PlatformTransactionManager 也有不同的实现。
以下为定义一个 JDBC datasource 并且使用相应的事务管理器:
<bean 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 class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
Spring 数据库事务使用
了解了一个整体抽象后,现在我们来研究怎么启用 Spring 数据库事务。
我们可以通过代码编程方式和声明方式来使用 Spring 数据库事务,但在大多数情况下都使用声明方式来使用事务,因此这里只讨论声明方式的具体细节。
基于 xml 的事务配置
在实际配置前,我们先了解下 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 还是基于注解,事务属性除了是否只读还有其他一些属性。上面配置中我们没有具体指明这些属性值,其实是使用了这些属性的默认值:
- Propagation:默认为 REQUIRED
- Isolation:默认为 DEFAULT
- Read-Only:默认为 false
- Timeout:默认为底层数据库超时时间
- Rollback-for:RuntimeException
我们可以改变这些属性默认值。这些属性是依赖来设定的,总结如下:
属性 | 是否必要 | 默认 | 描述 |
---|---|---|---|
name | 是 | 事务属性关联的方法名 | |
propagation | 否 | REQUIRED | 事务传播行为 |
isolation | 否 | DEFAULT | 事务隔离级别 |
timeout | 否 | -1 | 事务超时时间(单位秒) |
read-only | 否 | false | 事务是否只读 |
rollback-for | 否 | 导致事务回滚的异常;以逗号分割。 | |
no-rollback-for | 否 | 不导致事务回滚的异常;以逗号分割。 |
其中,Rollback(rollback-for、no-rollback-for) 和 Propagation 有些细节需要额外注意,下面做些探讨。
事务回滚(Rollback)
Spring 建议的做法是,我们通过抛出异常方式来回滚事务。当抛出异常时,Spring 事务框架会捕获异常,决定是否回滚事务,然后再重新抛出该异常。
在默认情况下,Spring 事务框架只会对于RuntimeException和Error回滚事务,其他异常则不会回滚事务。
我们可以指定异常类型回滚或不回滚事务,如下所示:
<tx:advice transaction-manager="txManager"> <tx:attributes> <tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/> <tx:method name="*"/> </tx:attributes> </tx:advice>
<tx:advice> <tx:attributes> <tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/> <tx:method name="*"/> </tx:attributes> </tx:advice>
事务传播(Propagation)
事务传播指的是,多个使用事务的方法存在相互调用时,各自的事务是怎么相互影响的。
下面是事务传播的行为:
行为 | 描述 |
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 代理而是直接调用,因此也不会生成所希望的事务。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论