Spring 事务管理与传播属性

在博文 《Spring学习之事务的使用姿势》 中,演示了基于注解和xml的事务使用姿势,以@Transactional注解为例,其中很多的参数都没有详细说明

本篇博文,则主要目的是弄懂这些参数有啥用,以及在实际项目中如何选择

I. 事务的传播属性

1. @Transactional注解

实际使用的case

1
2
3
4
5
6
7
8
9
10
11
12
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false)
public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) {
MoneyEntity entity = moneyDao.queryMoney(outUserId);
if (entity.getMoney() > payMoney) { // 可以转账
// 先减钱
moneyDao.incrementMoney(outUserId, -payMoney);

// 再加钱
moneyDao.incrementMoney(inUserId, payMoney);
System.out.println("转账完成! now: " + System.currentTimeMillis());
}
}

接下来看下注解的源码定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";

@AliasFor("value")
String transactionManager() 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 {};
}

对应的属性说明

属性 类型 描述
value String 可选的限定描述符,指定使用的事务管理器
propagation enum Propagation 可选的事务传播行为设置
isolation enum Isolation 可选的事务隔离级别设置
readOnly boolean 读写或只读事务,默认读写
timeout int (in seconds granularity) 事务超时时间设置
rollbackFor Class对象数组,必须继承自Throwable 导致事务回滚的异常类数组
rollbackForClassName 类名数组 导致事务回滚的异常类名字数组
noRollbackFor Class对象数组,必须继承自Throwable 不会导致事务回滚的异常类数组
noRollbackForClassName 类名数组 不会导致事务回滚的异常类名字数组

上面的几个参数中,除了propagatin和isolation两个参数外,其次就是对readOnly的使用场景不太清晰外,其他的相对而言就没什么二义性了

  • timeout: 避免事务一致挂住,导致持有的锁不会释放(极端情况下死锁时,如果没有超时,系统就会拖挂)
  • rollbackForXXX: 表示导致事务回滚的异常类
  • noRollbackForXXX: 某些情况下,我不希望某些异常引起事务回滚,就可以通过这个设置特例了

2. Propagation 事务传播行为

事务传播行为:指在开始当前事务之前,已经有一个事务上下文存在了,此时可以怎么确定这个事务性方法的执行行为

举个例子简单说明这个场景

  • ServiceA 有个事务方法 methodA
  • ServiceB 有个事务方法 methodB
  • ServiceA的methodA方法内部调用ServiceB的methodB方法

这种时候methodB方法的执行时,是在methodA方法的事务中还是新启一个自己的事务呢?

a. 行为定义

  • TransactionDefinition.PROPAGATION_REQUIRED:如果当前事务存在,则加入该事务;如果不存在,则新建一个
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,并挂起当前的事务(如果当前有一个事务的情况下)
  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前事务存在,则加入,如果不存在,则以非事务方式运行
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:非事务方式运行,如果当前事务存在,则挂起
  • TransactionDefinition.PROPAGATION_NEVER:非事务方式运行,如果当前事务存在,则抛异常
  • TransactionDefinition.PROPAGATION_MANDATORY:如果当前事务存在,则加入该事务,如果不存在,则抛异常
  • TransactionDefinition.PROPAGATION_NESTED:如果事务存在,则创建新的事务作为当前事务的嵌套,两者一起提交,外面的回滚,则内部的也回滚;如果没有事务,则等同于PROPAGATION_REQUIRED

b. 简单说明

默认的是 PROPAGATION_REQUIRED, 将当前执行加入到当前的事务中运行,这个主要和我们绝大部分的场景比较相似,同样为了代码逻辑的简单性而言,尽量避免多个事务方法交叉调用比较好


对于PROPAGATION_REQUIRES_NEW就比较有意思了,什么场景需要又开启一个事务来执行呢?

举一个不太恰当的例子:

在一个论坛的回帖,有这么个功能,注册和回帖一起提交,任务拆解为

  • 回帖 + 积分+1 (假设这两个操作是一个事务)
  • 注册 + 活跃度+1 (加色这两个操作是一个事务)

如果这个注册并回帖的实现逻辑如下:

  1. 进入回帖事务,准备插入回帖内容到DB中
  2. 首先判断用户是否存在,没有存在,则直接进入注册逻辑
  3. 注册逻辑开启新的事务,内部实现注册,实现活跃度+1两个功能,事务提交
  4. 回到回帖逻辑,执行回帖,执行积分+1,提交事务

上面这个过程,回帖开启了一个事务A;然后发现需要注册用户信息,然后进入注册逻辑,开启另一个事务B;此时我们希望的是注册独立于回帖,即便回帖失败(如回帖内容包含敏感词导致失败时)用户依然是注册成功;这种场景下PROPAGATION_REQUIRES_NEW 这个就有用了


另外一个就是TransactionDefinition.PROPAGATION_NESTED, 区别于PROPAGATION_REQUIRES_NEW, 这种属性下,内部的事务成功与否是依赖外部事务的,即外部的成功提交之后,内部才会提交

3. isolation 隔离级别

学习DB时,也会见识隔离级别,比如mysql的InnoDB引擎中常见的 RU, RC, RR, Serializable四种隔离级别

主要就是针对多个事务之间的相互影响来说明的,隔离级别逐渐递增,更多关于db的隔离相关推荐参考 :《mysql之锁与事务详解》

a. 事务隔离级别

  • TransactionDefinition.ISOLATION_DEFAULT:默认值,根据DB的事务隔离级别而言,mysql的InnoDB引擎是RR
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED:表示一个事务在执行过程中,可以读取到其他事务未提交的内容(如另一个事务回滚时,可能出现脏读)基本不用
  • TransactionDefinition.ISOLATION_READ_COMMITTED:一个事务只能读取其他事务提交后的数据 (不可重复读,如第一次查询返回一个数据; 另一个事务更新了这个数据并提交,然后再次查询,返回了修改后的数据,导致两次结果不同,这就是不可重复读)
  • TransactionDefinition.ISOLATION_REPEATABLE_READ:一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读
  • TransactionDefinition.ISOLATION_SERIALIZABLE:所有事务逐次运行,效率最低,也不实用

最常见的隔离级别就是RC和RR

4. readony 只读属性

读事务用于客户代码只读但不修改数据的情形,只读事务用于特定情景下的优化

只读事务”并不是一个强制选项,它只是一个“暗示”,提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力,毕竟事务也是要消耗数据库的资源的。

但是你非要在“只读事务”里面修改数据,也并非不可以,只不过对于数据一致性的保护不像“读写事务”那样保险而已。

因此,“只读事务”仅仅是一个性能优化的推荐配置而已,并非强制你要这样做不可

II. Spring事务配置

通过Spring中实际实用事务,以及对事务使用中的几个参数进行说明,对Spring中的事务使用有了那么点概念,接下在则根据自己的理解简单的串一下,整个过程是怎么玩的

img

  • DataSource:数据源,负责配置DB相关信息
  • TransactionManager: 事务管理器,用户事务配置
  • 代理机制:具体的事务实现方式

前面两个的变动较少,主要就是代理机制的选择上,通常使用xml配置 tx:advice + aop:config@Transactional + <tx:annotation-driven transaction-manager="transactionManager"/> 两种方式

III. 其他

1. 推荐博文

工程源码

2. 个人博客: 一灰灰Blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

3. 声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

4. 扫描关注

QrCode