最近工作中遇到的需求,需要用到嵌套事务,然而在涉及到不同事务方法之间互相调用时的传播行为时却不是很确定,之前好像只是停留在定义的层面,对于具体各种情况事务的回滚情况并不是很确定。所以决定对各种情形进行实际的代码demo,验证一下结果。
以及在开发中遇到了Transaction rolled back because it has been marked as rollback-only异常分析下出现的问题。
1.事务是什么
事务是一系列的动作,它们综合在一起才是一个完整的工作单元。 主要具有ACID四个特性:
- A:原子性 一系列的动作,结果要么全部成功,要么全部不成功。
- C:一致性 一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
- I:隔离性 可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏
- D:持久性 一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
2.数据库事务隔离级别
数据库的事务隔离级别分为四个级别:
-
脏读问题: 一个事务可以读取到了另一个事务为提交的数据
事务A 更新数据 data = 500,并未提交,事务B 读取到 data = 500, 如果事务A发生回滚,事务B读取到的data就是临时的、错误的值。 -
不可重复读问题: 前后多次读取,数据内容不一致 针对的是update或delete
事务A 两次对data 进行查询 ,第一次查询结束后,事务B对data值进行更新,事务A需要等B提交后才能读,此时两次读到的结果就不一致。 -
幻读问题: 前后多次读取,数据总量不一致 针对的insert
事务A读取 两次读取 table的总行数,假设第一次读取数据为10条,事务B进行插入操作,并提交事务,事务A第二次读取总条数变为11条。
- Read uncommitted 读未提交 一个事务可以读取到另一个事务未提交的数据 引发脏读问题
- Read committed 读提交 事务提交之后才能进行读操作 存在不可重复读问题
- Repeatable read 可重复读 事务开启之后,不允许再修改操作 存在幻读问题
- Serializable 事务串行化顺序执行,可以避免脏读、不可重复读与幻读
大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。
Mysql的默认隔离级别是Repeatable read。
Mysql InnoDB引擎支持事务 MyISAM不支持事务
数据库空中的开启事务、提交与回滚
START TRANSACTION;
INSERT INTO sys_user value (1,20,'男','ShinyRou');
commit ;--提交
START TRANSACTION;
INSERT INTO sys_user value (2,20,'男','ShinyRou1');
ROLLBACK ;--回滚
3.Spring事务管理
事务是数据库提供的特性,SPring中的事务只是进行了封装来方便操作事务的提交与回滚。
同一个事务,使用的是同一个数据库连接,同一个事务ID,在spring中的多个DB操作执行后不会立即执行到数据库,直到全部完成事务提交后才会执行到DB。
3.1Spring事务接口
-
PlatformTransactionManager接口 平台事务管理器 不同的ORM框架 提供了不同的实现
-
TransactionDefinition接口 定义事务 定义事务的隔离级别、传播特性、超时机制属性
-
TransactionStatus接口 事务的状态 获取事务的状态 是否有保存点、是否已经完成等等
3.2spring中事务的使用
3.2.1.编程式的事务 通过DataSourceTransactionMannager、TranssactionDefinition等API来编程式的控制事务的提交与回滚
3.2.2.声明式事务 使用@Transactional主键来实现 原理是通过AOP 基于代理模式实现
- 当使用了@Transactional注解的业务方法中 抛出了运行时异常 RuntimeException时,事务就会进行回滚
- @Transactional注解中rollbackFor 属性可以执行抛出什么异常时进行回滚
- 当@Transactional 注解使用在类上时,类中所有的方法都会开启事务
@Transactional注解的定义
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
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 {};
}
4.事务的传播行为
事务的传播行为主要是用于在多个事务方法互相调用时事务应该如何传播
Propagation枚举的定义
public enum Propagation {
REQUIRED(0), //支持当前事务,如果不存在事务,就新建一个事务
SUPPORTS(1), //支持当前事务,如果不存在事务,就不使用事务
MANDATORY(2), //支持当前事务,如果不存在事务,就抛出异常
//0,1,2 可以归为一类,如果当前有事务 就使用当前事务,只是在当前不存在事务时,处理方法不一致
REQUIRES_NEW(3), //如果当前事务存在,挂起当前事务,创建一个新事务 (不在同一个事务中)
NOT_SUPPORTED(4), //以非事务方式运行,如果当前事务存在,挂起当前事务
NEVER(5), //以非事务方式运行如果当前事务存在,就抛出异常
//3,4,5 可以归为一类,都不支持当前事务(不在同一个事务中)
NESTED(6); //如果不存在事务,则新建事务与REQUIRED相同,如果存在事务就创建子事务,执行嵌套事务
//嵌套事务:外围事务回滚,所有子事务也回滚,子事务回滚不会影响其他子事务与外围食物
//最为常用的是 REQUIRED REQUIRES_NEW NESTED
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
传播行为结果验证可以参考
传播行为代码验证 注意:原文在验证REQUIRES_NEW时地方,注解里面的传播方式写错REQUIRED
5.Transaction rolled back because it has been marked as rollback-only异常的原因分析
//用户表事务操作
@Transactional
@Override
public int addUserREQUIRED(SysUser user) {
int userResult = sysUserDao.insert(user);
return userResult;
}
//日志表事务 正确执行
@Transactional
@Override
public int addLogREQUIRED(Log log) {
return logDao.insert(log);
}
//日志表事务 抛出异常
@Transactional
@Override
public int addLogREQUIREDWithException(Log log) {
int result = logDao.insert(log);
throw new RuntimeException("DB ERROR");
}
@Transactional
@Override
public void fooFunction(SysUser user) {
sysUserService.addUserREQUIRED(user);
Log log = new Log();
log.setUserId(user.getNo());
log.setDescription("add user"+user.toString());
//logService.addLogREQUIRED(log);//正确执行
try{
logService.addLogREQUIREDWithException(log);
}catch(Exception e){
e.getStackTrace();
}
}
假设fooFunction 中对addLogREQUIREDWithException 抛出的异常进行捕获就会出现上述异常
原因是: 默认传播行为 REQUIRED 此时整体使用的是同一个事务
addLogREQUIREDWithException中抛出异常 对log表的操作需要回滚,整个事务也标记为需要回滚,但是在fooFunction方法中异常被捕获不满足回滚条件,在事务整体commit时就会抛出上述异常
解决方法:
- 1.不要在fooFunction中使用try catch
- 2.声明addLogREQUIREDWithException 传播行为为ESTED 子事务的回滚不影响外围事务
- 3.fooFunction上不开启事务
实际开发中需要根据业务逻辑对数据的一致性要求来进行解决异常。