一、Spring数据库事务管理器的设计
在Spring中数据库事务是通过PlatformTransactionManager进行管理的,而真正能够支持事务的是org.springframework.transaction.support.TransactionTemplate模版,其核心方法源码如下:
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
//使用自定义的事务管理器
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this, action);
} else {//系统默认管理器
//获取事务状态
TransactionStatus status = this.transactionManager.getTransaction(this);
Object result;
try {
//接口回调方法
result = action.doInTransaction(status);
} catch (RuntimeException var5) {
//回滚异常方法
this.rollbackOnException(status, var5);
//异常抛出
throw var5;
} catch (Error var6) {
//回滚异常方法
this.rollbackOnException(status, var6);
//抛出错误
throw var6;
} catch (Exception var7) {
//回滚异常方法
this.rollbackOnException(status, var7);
//抛出无法捕获异常
throw new UndeclaredThrowableException(var7, "TransactionCallback threw undeclared checked exception");
}
//提交事务
this.transactionManager.commit(status);
//返回结果
return result;
}
}
源码中的注释是笔者加入的,从源码我们可以看出:
事务的创建、提交和回滚都通过PlatformTransactionManager接口完成
无异常时会提交事务,当事务产生异常时会回滚事务,默认所有异常都回滚,我们可以通过配置去修改发生某些异常时回滚或不回滚
Spring事务管理器结构如下图:
我们常用的是DataSourceTransactionManager,配置代码如下:
<!--事务管理器配置数据源事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
二、编程式事务和声明式事务
编程式事务是以代码的方式管理事务,事务将由开发者通过代码来实现。这是最简单的使用方式,但是不推荐使用。
声明式事务是一种约定型事务,Spring给出一个约定,当你的业务方法不发生异常(或发生异常,但被配置信息设置允许提交)时,Spring就会让事务管理器提交事务,而发生异常(并且该异常不被配置信息所允许提交事务)时,则让事务管理器回滚事务。声明式事务允许自定义事务接口——TransactoinDefinition,它可以由xml或者注解@Transactional进行配置。
1、@Transactional 配置项
我们先看看Transactional源代码:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
String value() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
根据源码我们给出配置项及其含义:
- propagation 表示当前事务与父事务(同一个线程中之前申明事务)的关系。父子事务体现为,方法调用栈的调用先后顺序。说得更直白点,就是一个方法Method1调用另外一个申明Transaction的Method2,Method1可能申明Transaction,也可能没有,Method2如何处理Transaction的问题。
- isolation 刻画不同事务之间数据的可见性,数据的隔离级别。ISOLATION_READ_UNCOMMITTED,ISOLATION_READ_COMMITTED,ISOLATION_REPEATABLE_READ,ISOLATION_SERIALIZABLE隔离级别逐渐增强。基本与关系型数据库的隔离级别一致。 timeout 以秒为单位的事务超时时间。默认值-1,表示没有超时时间。
例子:@Transactional(timeout=30) - readOnly 提示事务管理器,方法内部只涉及到读操作,Spring默认为false。依赖于底层事务管理器的解释,并不一定有效果。
例子:@Transactional(readOnly=true) - rollbackFor 遇到什么样的异常事务会回滚。Spring默认值是RuntimeException,Checked
Exception不会回滚。 例子:@Transactional(rollbackFor=Exception.class)
其中isolation和propagation是最为重要的内容,这些属性被Spring放在事务定义器TransactionDefinition接口中。
2、声明式事务的约定流程
首先根据Spring通过事务管理器(PlatformTransactionManager)创建事务,同时会把事务中的隔离级别、超时时间等属性根据配置内容往事务上设置,通常采用注解方式(@Transactional),声明式事务流程如下图:
代码示例如下:
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED,timeout = 3)
public int insertRole(Role role) {
return roleMapper.insertRole(role);
}
三、数据库事务相关知识
1、数据库事务ACID特性
- 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
- 一致性(Consistent):事务结束后系统状态是一致的;
- 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
- 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。
2、事务的并发问题
- 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
- 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
- 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
3、数据库事务隔离级别
数据库的四种隔离级别:脏读、读/写提交、不可重复读、序列化,如下表:
隔离级别 | 隔离级别的值 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交(read-uncommitted) | 0 | √ | √ | √ |
不可重复读(read-committed) | 1 | × | √ | √ |
可重复读(repeatable-read) | 2 | × | × | √ |
串行化(serializable) | 3 | × | × | × |
四、选择隔离级别和传播行为
1、选择隔离级别
在互联网应用中,选择隔离级别要综合考虑数据库数据一致性和系统的性能。一般而言,从脏读到序列化,数据一致性明显提高,但系统性能直线下降。比如序列化,会严重压制并发,引发大量线程挂起,知道获得锁才能进一步操作,而且恢复时又需要大量等待时间。所以大部分场景下,企业选择读/写(Read-Committed)提交的方式设置事务。
Spring中的事务隔离级别如下表:
隔离级别 | 含义 |
---|---|
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别。 |
ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的更改。可能导致脏读、幻影读或不可重复读。 |
ISOLATION_READ_COMMITTED | 允许从已经提交的并发事务读取。可防止脏读,但幻影读和不可重复读仍可能会发生。 |
ISOLATION_REPEATABLE_READ | 对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻影读仍可能发生。 |
ISOLATION_SERIALIZABLE | 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。 |
2、传播行为
传播行为是指方法之间的调用事务策略的问题。有时通过方法一去调度方法二时产生一条新事务,如果方法二发生异常,则会回滚事务,此时方法一的主事务也被回滚,显然这是不合理的。我们通过对事务的特性进行传播配置,控制方法之间调度事务策略,称为传播行为。流程如下图:
Spring的七种传播行为如下图:
传播行为 | 意义 |
---|---|
PROPAGATION_MANDATORY | 表示该方法必须运行在一个事务中。如果当前没有事务正在发生,将抛出一个异常 |
PROPAGATION_NESTED | 表示如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于封装事务进行提交或回滚。如果封装事务不存在,行为就像PROPAGATION_REQUIRES一样。 |
PROPAGATION_NEVER | 表示当前的方法不应该在一个事务中运行。如果一个事务正在进行,则会抛出一个异常。 |
PROPAGATION_NOT_SUPPORTED | 表示该方法不应该在一个事务中运行。如果一个现有事务正在进行中,它将在该方法的运行期间被挂起。 |
PROPAGATION_SUPPORTS | 表示当前方法不需要事务性上下文,但是如果有一个事务已经在运行的话,它也可以在这个事务里运行。 |
PROPAGATION_REQUIRES_NEW | 表示当前方法必须在它自己的事务里运行。一个新的事务将被启动,而且如果有一个现有事务在运行的话,则将在这个方法运行期间被挂起。 |
PROPAGATION_REQUIRES | 表示当前方法必须在一个事务中运行。如果一个现有事务正在进行中,该方法将在那个事务中运行,否则就要开始一个新事务。 |
七种传播行为在TransactionDefinition中定义,其中最常用的是REQUIRED,也是默认的传播行为。企业比较关注的是REQUIRES_NEW和NESTED两种。
五、@Transactional的自调用失效问题
1、什么是自调用
有时候配置了注解@Transactional,但是它会失效。原因是注解@Transactional的底层实现是Spring AOP技术,而Spring AOP技术使用的是动态代理。所以对于静态方法和非public方法,注解@Transactional失效。还有自调用的时候也会失效。
自调用是指本类方法调用自身类的方法,同时被调用的方法注解了@Transactional,此时被调用方法的@Transactional失效。代码解释如下:
public class RoleServiceImpl implements RoleService{
@Autowired
private RoleMapper roleMapper = null;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED,timeout = 3)
public int insertRole(Role role) {
return roleMapper.insertRole(role);
}
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
public int insertRoleList(List<Role> roleList) {
int count = 0;
for (Role role:roleList){
try {
//调用自身类方法,产生自调用问题
insertRole(role);
}catch (Exception ex){
System.out.println(ex);
}
}
return count;
}
}
2、产生自调用注解失效的原因
出现自调用注解失效问题的根本原因在于AOP实现原理,以上代码中是自己调用自己方法的过程,不存在代理对象的调用,就不会产生AOP去为设置@Transactional配置的参数。
3、解决方案
解决方法有两个:
为两个方法使用不同的接口,Spring IoC容器会生成不同的代理对象
直接从容器中获取代理对象,代码如下:
public class RoleServiceImpl implements RoleService{
@Autowired
private RoleMapper roleMapper = null;
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED,timeout = 3)
public int insertRole(Role role) {
return roleMapper.insertRole(role);
}
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
public int insertRoleList(List<Role> roleList) {
int count = 0;
RoleService service = ctx.getBean(RoleService.class);
for (Role role:roleList){
try {
//用代理对象调用方法,克服自调用问题
service.insertRole(role);
}catch (Exception ex){
System.out.println(ex);
}
}
return count;
}
}