[TOC]
参考链接:
MySql的四种事务隔离级别 - 超级小小黑 - 博客园
一口气说出 6种 @Transactional 注解失效场景
一、事务
事务的特性:
A(atomicity):原子性,一个事务时一个不可分割的工作单位,要么都提交,要不都不提交。
C(consistency):一致性,事务前后,数据状态一致。^ 一致性例子。
I(isolation):隔离性,事务互不干扰。
D(durability):持久性,一旦提交,数据永久性更改。
编程式事务
在代码中手动提交事务、回滚等操作,代码侵入性比较强
try {
//TODO something
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw new InvoiceApplyException("异常失败");
}
声明式事务
基于AOP面向切面,将业务与事务处理解耦,代码侵入性很低,所以是最常用的事务管理方式。
声明式事务实现方式有两种,一种基于TX和AOP的xml配置文件方式,第二种就是基于@Transactional注解。
@Transactional
@GetMapping("/test")
public String test() {
int insert = cityInfoDictMapper.insert(cityInfoDict);
}
二、@Transactional介绍
1. @Transactional注解作用于哪些地方。
@Transactional作用于接口
,类
,方法
。
- 作用于类:
表示该类的public方法都配置相同的事务属性信息。 - 作用于方法:
类配置了@Transactional,该类下方法也配置了@Transactional,方法的事务会覆盖类的事务。 - 作用于接口:
不推荐这种方法,因为一旦标注在Interface上并且配置了Spring AOP使用CGlib动态代理,将会导致@Transactional注解会失效。
@Transactional
@RestController
@RequestMapping
public class MybatisPlusController {
@Autowired
private CityInfoDictMapper cityInfoDictMapper;
@Transactional(rollbackFor = Exception.class)
@GetMapping("/test")
public String test() throws Exception {
return "success";
}
}
2.@Transactional注解属性
-
propagation
propagation
代表事务的传播属性,Propagation.REQUIRED:(默认)如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。(也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务)
Propagation.SUPPORT:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
Propagation.MADATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。(当类A中的a方法用默认
Propagation.REQUIRED
模式,类B中的b方法上采用Propagation.REQUIRES_NEW
模式,然后在a方法中调用b方法操作数据库,然而a方法抛出异常,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW
会暂停a方法的事务)Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
Propagetion.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。
Propagetion.NESTED:和
Propagetion.REQUIRED
效果一样。 -
isolation
isalation
事务的隔离级别Isolation.DEFAULT:(默认)使用底层数据库默认的隔离级别。大多数数据库默认的事务隔离级别是Read committed,比如Sql Server,Oracle。Mysql的默认隔离级别是Repeatable read。
Isolation.READ_UNCOMMITTED
允许脏读,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过”排他写锁“实现。Isolation.READ_COMMITTED
允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。Isolation.REPEATABLE_READ
禁止不可重复读取和脏读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。Isolation.SERIALIZABLE
它要求事务序列化执行,事务只能一个接着一个的执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。读数据一致性 脏读 不可重复读 幻读 未提交读 最低级别只能保证不读取物理上损坏的数据 :heavy_check_mark: :heavy_check_mark: :heavy_check_mark: 已提交读 语句级 :x: :heavy_check_mark: :heavy_check_mark: 可重复读 事务级 :x: :x: :heavy_check_mark: 序列化 最高级别,事务级 :x: :x: :x: 脏读
又称无效数据读出。一个事务读取另外一个事务还没有提交的数据叫脏读。
例如:事务T1修改了一行数据,但是还没有提交,这时候事务T2读取了被事务T1修改后的数据,之后事务T1因为某种原因Rollback了,那么事务T2读取的就是脏数据。不可重复读
同一个事务中,多次读出的同一数据是不一致的。
例如:事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。-
幻读
不好表述直接上例子吧:在仓库管理中,管理员要给刚到的一批商品进行入库管理,当然入库之前肯定是要查一下之前有没有入库记录,确保正确性。管理员A确保库中不存在该商品之后给该商品进行入库操作,假如这时管理员B因为手快将该商品进行了入库操作。这时管理员A发现该商品已经在库中。就像刚刚发生了幻读一样,本来不存在的东西,突然之间他就有了。
注:三种问题看似不太好理解,脏读侧重的是数据的正确性。不可重复读侧重的是对数据的修改,幻读侧重于数据的新增和删除。
-
timeout
timeout
事务的超时时间。
默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。 -
readOnly
readOnly
:指定事务是否为只读事务。
默认值为false;为了忽略那些不需要事务的方法,比如读取数据,可以设置成read-only为true -
rellbackFor
rollbackFor
用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。 -
noRollbackFor
noRollbackFor
:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。
三、@Transactional失效场景
1.@Transactional应用在非public方法上
编译不会报错,但是自己要注意
2.@Transactional属性propagetion设置错误
若配置以下三种propagation,事务有可能不回滚。
Propagation.SUPPORT^ Propagation.SUPPORT
Propagation.NOT_SUPPORTED[^ Propagation.NOT_SUPPORTED]
Propagetion.NEVER[1]
3.@Transactional注解rollbackbakcFor设置错误
4.同一个类中方法调用,导致@Transactional失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务还是不起作用的。这也是经常犯错误的一个地方。
public class Test{
@GetMapping("/A")
public String A(){
String result = this.B();
return result;
}
@GetMapping("/B")
@Transactional(rollbackFor=Exception.class)
public String B(){
String result = "1";
result+="2";
throw new Exception("模拟报错..");
result+="3";
return result;
}
}
/*访问/A,调用的B的事务不起作用,访问/B,B的事务起作用*/
为啥会出现这种情况?
其实着还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成代理对象来管理。
5.异常被catch,没有抛出给Spring
这种情况最常见!
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);
/**
* A 插入字段为 2的数据
*/
insert = cityInfoDictMapper.insert(cityInfoDict);
/**
* B 插入字段为 3的数据
*/
b.insertB();
} catch (Exception e) {
e.printStackTrace();
}
}
如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?
答案:不能!
因为当ServiceB
中抛出了一个异常以后,ServiceB
标识当前事务需要rollback
。但是ServiceA
中由于你手动的捕获这个异常并进行处理,ServiceA
认为当前事务应该正常commit
。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException
异常。
spring
的事务实在调用业务方法之前开始的,业务方法执行完毕之后才执行commit
orrollback
,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception
并在你的业务方法中没有catch到的话,事务会回滚。
在业务方法中一般不需要catch异常,如果非要catch,一定要抛出throw new RuntimeException()
,或者注解中指定抛异常类型@Transactional(rollbackFor=Exception)
,否则事务失效。
6.数据库引擎不支持事务
mysql默认使用innodb
引擎,所以基本不会出现这个问题,关于数据库引擎,都使用innodb
即可。
-
以非事务的方式运行,如果当前存在事务,则抛出异常。
[^ Propagation.NOT_SUPPORTED]: 以非事务的方式运行,如果当前存在事务,暂停当前的事务 ↩