一、概述
在Spring中数据库事务是通过PlatformTransactionManager(事务平台管理)进行管理的。TransactionTemplate是Spring所提供的事务管理器的模板,先看看一段TransactionTemplate的源码
//事务管理器
private PlatformTransactionManager transactionManager;
//.....
@Override
public Object execute(TransactionCallback action) throws TransactionException {
//使用自定义的事务管理器
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager)
{
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {//系统默认管理器
//获取事务状态
TransactionStatus status = this.transactionManager.getTransaction(this);
Object result = null;
try {
//回调接口方法
result = action.doInTransaction(status);
}catch (RuntimeException ex) {
// 回滚异常方法
rollbackOnException(status, ex);
//抛出异常
throw ex;
}catch (Error err) {
// 回滚异常方法
rollbackOnException(status, err);
//抛出异常
throw err;
}catch(Throwable ex){
// 回滚异常方法
rollbackOnException(status, err);
//抛出无法捕获异常
throw new UndeclaredThrowableException(ex,
"抛出无法捕获异常");
}
//没有异常,提交事务
this.transactionManager.commit(status);
//返回结果
return result;
}
}
源码我们可以知道:
1)事务的创建、提交和回滚由PlatformTransactionManager接口来完成;
2)只要产生异常,就回滚。
3) 没有异常,提交事务,并返回结果。
其实PlatformTransactionManager接口源码很简单,就三个方法
public interface PlatformTransactionManager {
//根据事务定义TransactionDefinition,获取事务
TransactionStatus getTransaction(TransactionDefinition definition);
//提交事务
void commit(TransactionStatus status);
//回滚事务
void rollback(TransactionStatus status);
}
Spring的事务体系也是在PlatformTransactionManager事务管理器接口上开展开来的,事务的提交、回滚等操作全部交给它来实现。
二、配置事务管理器
MyBatis框架中用的最多的就是DataSourceTransactionManager。它与PlatformTransactionManager的关系如下:
- extends
- implemets
(1)使用XML进行配置事务管理器:不常用,通常用注解的方式。
<!-- 配置事务管理器 -->
<bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 装配数据源 -->
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>
<!--使用xml配置事务方法 -->
<aop:config>
<!-- 设置添加事务的方法,使用切入点表达式-->
<aop:pointcut expression="execution(* *.checkout(..))" id="mypoint"/>
<!-- 将事务方法和事务的相关配置关联起来 -->
<aop:advisor advice-ref="myAdvice" pointcut-ref="mypoint" />
</aop:config>
<!-- tx配置事务的属性 (使用tx名称空间)-->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 配置事务的属性,多个事务方法也可以在这个里面放,name设置事务方法名,propagation设置事务相关信息 -->
<tx:method name="checkout" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
(2)基于注解的事务配置
- 首先在需要添加事务的方法上加上@Transactional注解
- 还需要Spring的配置文件中配置事务管理器
<!-- 添加事务管理器组件DataSourceTransactionManager -->
<bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 使用set方法注入数据源 -->
property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启基于注解声明式事务 注意配置transaction-manager属性,它引用了我们事务管理组件对象,这里要和事务管理器组件id一致 默认是transactionManager -->
<tx:annotation-driven transaction-manager="transactionManager" />
三、声明式事务的约定流程
当Spring IoC容器初始化时,Spring 会读入@Transactional 或者 XML配置的事务信息,并保存到一个事务定义类(TransactionDefinition接口的子类),以备使用。当运行时会让Spring拦截注解标注的某一个方法或者类的使用所有方法。
Spring提供事务管理器创建事务,把事务中定义的隔离级别、超时时间等属性根据配置内容往事务上配置。根据传播行为采取特定的措施。
在整个开发过程中,我们只需要编写业务代码和对事务属性进行配置就可以。
四、选择传播行为和隔离特性
上面说到,我们只需要编写业务代码和对事务属性进行配置就可以,那毫无疑问,怎么配置才是重点!首先先了解下数据库事务的特性。
1、数据库事务正确执行的4个基础要素:
- 1)原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 2)一致性:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。例如,完整性约束了a+b=10,一个事务改变了a,那么b也应该随之改变。
- 3)隔离性:两个事务的执行是互不干扰的,一个事务不可能看到其他事务运行时,中间某一时刻的数据。
- 4)持久性:在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并且不会被回滚。
2、其他三个都比较好理解,重点说隔离性,隔离性为了解决的问题主要有三个:通过设置隔离级别的方式
(1)、 脏读(Drity Read):事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的就是脏数据。同时也有脏读也是最低级别的隔离级别。
为了克服脏读,SQL标注提出了第二个隔离级别:读/写提交:就是说一个事务只能读取另一个事务已经提交的事务。
但是也会引发其他问题:
额是一个变化的值。我们称之为不可重复读。
(2)、不可重复读(Non-repeatable read) : 在同一个事务中,对于同一份数据读取到的结果不一致。比如,事务B在事务A提交前读到的结果,和提交后读到的结果可能不同。不可重复读出现的原因就是事务并发修改记录,要避免这种情况,最简单的方法就是对要修改的记录加锁,这导致锁竞争加剧,影响性能。
为了克服不可重读带来的错误,SQL标准又提出了一个隔离级别:可重复读:针对数据库的同一条记录的读/写按照一个序列化操作,不会产生交叉情况,保证数据一致性。可以认为是锁住这个记录。
但是也会产生一些问题 : 产生幻读
(3)、幻读(Phantom Read) : 在同一个事务中,同一个查询多次返回的结果不一致。事务A新增了一条记录,事务B在事务A提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读仅指由于并发事务增加记录导致的问题,这个不能像不可重复读通过记录加锁解决,因为对于新增的记录根本无法加锁。需要将事务串行化,才能避免幻读。
(4)、最后,为了克服幻读,SQL推出更高的隔离级别----序列化。
总结这三种情况对应的四种隔离级别:
3、怎么选择隔离级别
(1)随着隔离级别的提高,系统性能逐渐下降(“高风险,高回报”)。
(2)序列化会严重抑制并发,从而引发大量的线程挂起,知道获取锁才能进行下一步操作,恢复时又需要等待大量的时间,但是数据绝对安全。可以在并发量不大但又保证数据安全性的情境下使用。
(3)企业一般选择读/写提交模式。
(4)MySql支持4中隔离级别,默认为可重复读,Oracle支持读/写提交和可重复读,默认为读/写提交。
总之:隔离级别需要根据并发量的大小和性能来决定。
4、传播行为
事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。
Spring的7种传播行为:
其中REQUIRED:这是最常见的,也是Spring 默认的传播行为。它比较简单,支持当前事务,如果没有事务,就建一个新的事务。
一般而言,企业比较关注的是下面这两种:
1、REQUIRES_NEW:当一个REQUIRED方法A调用REQUIRES_NEW方法B时,B方法会新new一个事务,并且这个事务和A事务没有关系,也就是说B方法出现异常,不会导致A的回滚,同理当B已提交,A再出现异常,B也不会回滚。
2、NESTED:这个和REQUIRES_NEW的区别是B方法的事务和A方法的事务是相关的。只有在A事务提交的时候,B事务都会提交。也就是说当A发生异常时,A、B事务都回滚,而当B出现异常时,B回滚,而A回滚到savepoint。