Transactional失效的场景
一:spring的事务管理
讲解Transactional之前先来聊聊spring的事务。
1:什么是事务?
答:事务是一组操作,这组操作要么全部完成,要么全部失败。
2:事务的特性?
答:ACID四种
原子性 (Atomicity) : 事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
一致性 (Consistency) : 一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
隔离性 (Isolation) : 可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
持久性 (Durability) : 一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
3)如果不考虑隔离性可能会引发的问题?
答:并发事务引起的问题:典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务。并发虽然是必须的,但可能会导致以下的问题。
脏读(Dirty reads)——脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。
不可重复读(Nonrepeatable read)——不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。
幻读(Phantom read)——幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。
4)怎么解决第三个问题呢?
设置事务的隔离级别。
- 未提交读:脏读,不可重复读,幻读都有可能发生
- 已提交读:避免脏读。但是不可重复读和幻读都有可能发生
- 可重复读:避免脏读和不可重复读。但是幻读有可能发生
- 串行化的:避免以上所有读问题
mysql数据库的默认级别是可重复读,oracle是读已提交。
5)spring有哪几种事务管理?
第一种:编程式事务。 是指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强,如下示例:
try {
//TODO something
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw new InvoiceApplyException("异常失败");
}
第二种:声明式事务。基于AOP面向切面的,它将具体的业务与事务处理部分解耦,代码侵入性很低,在开发中一般使用声明式事务比较多。声明式事务也有两种方式,一种是基于Aop的xml的配置文件方式,一种是基于@Transactional的注解方式。当然开发中也是基于注解的方式居多。
@Transactional
@GetMapping("/test")
public String test() {
int insert = cityInfoDictMapper.insert(cityInfoDict);
}
6)什么是Transactional以及其有什么作用,有哪些特性?
答:刚才第五点已经说了,Transactional主要是一个注解,标注spring的声明式事务的处理。
这个注解可以作用在哪些地方上面呢?
@Transactional可以作用在接口,类,类方法。
作用于类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息。
作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效。
spring的事务传播机制,有哪7种?
属性 | 说明 |
---|---|
REQUIRED | spring默认的事务传播行为,如果当前没有事务,则会开启一个事务,如果当前有一个事务,则加入当前的事务中。 |
SUPPORTS | 如果当前有事务,则使用事务,如果没有,则不使用。 |
MANDATORY | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。不会主动开启一个事务。 |
REQUIRES_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动,如果存在当前事务,在该方法执行期间,当前事务会被挂起(如果一个事务已经存在,则先将这个存在的事务挂起) |
NOT_SUPPORTED | 表示该方法不应该运行在事务中,如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。 |
NEVER | 表示当前方法不应该运行在事务上下文中,如果当前正有一个事务在运行,则会抛出异常。 |
NESTED | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与REQUIRED一样。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。 |
综上所述,NESTED和REQUIRES_NEW非常相似,都是开启一个属于它自己的新事务。使用REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。两个事务互不影响,两个事务不是一个真正的嵌套事务,同时它还需要JTA事务管理器的支持。
使用NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。嵌套事务开始执行时, 它将取得一个 savepoint,如果这个嵌套事务失败, 将回滚到此savepoint。潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。
7)transactional注解什么情况下会失效呢?
重点看一下:https://www.cnblogs.com/qizhelongdeyang/p/12418386.html
答:1:数据库引擎不支持事务。比如mysql的myisam引擎就不支持事务,那么从根本上就不可能生效,但是现在基本上mysql都是Innodb是支持事务的
2:@Transactional应用在非public方法上面或者带有final方法方面
因为@Transactional是基于动态代理的,private和final修饰的方法,不会被代理。spring中动态代理分为jdk动态代理和cglib代理,jdk动态代理要求接口必须实现接口(所以方法必须是public),但是cglib代理底层则是通过字节码生成被代理类的子类实现的,这里要求被代理类必须能被继承(所以方法也不能被final修饰)。
注意:protected、private 修饰的方法上使用 @Transactional注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。
3:@Transactional属性注解的配置引起。
TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
4:异常被try...catch捕获,导致其失效。
@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方法的异常,那这个事务还能正常回滚吗?
答案:不能!
会抛出异常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
因为当ServiceB中抛出了一个异常以后,ServiceB标识当前事务需要rollback。但是ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。
spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出RuntimeException。如果抛出RuntimeException 并在你的业务方法中没有catch到的话,事务会回滚。
在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit造成数据不一致,所以有些时候try catch反倒会画蛇添足。
5:@Transactional注解属性rollbackfor设置错误。
rollbackfor可以指定能够触发事务回滚的异常类型。spring默认抛出了未检查Unchecked异常(继承自RuntimeException的异常)或者Error才会回滚事务。其他的异常不会回滚事务。如果在事务中抛出了其他的异常,但是却希望spring能够回滚事务,就需要指定rollbackfor的属性。 若在目标方法中抛出的异常是 rollbackFor 指定的异常的子类,事务同样会回滚。
6:同一个类中调用方法,导致@Transactional失效
可以参考一下链接:
https://www.cnblogs.com/ynyhl/p/12066530.html
https://blog.csdn.net/ligeforrent/article/details/79223673
https://www.jianshu.com/p/2e4e1007edf2
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
//@Transactional
@GetMapping("/test")
private Integer A() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
/**
* B 插入字段为 3的数据
*/
this.insertB();
/**
* A 插入字段为 2的数据
*/
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert;
}
@Transactional()
public Integer insertB() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("3");
cityInfoDict.setParentCityId(3);
return cityInfoDictMapper.insert(cityInfoDict);
}
参考原文链接:
https://blog.csdn.net/hon_vin/java/article/details/105134342
https://www.cnblogs.com/ynyhl/p/12066530.html
https://blog.csdn.net/ligeforrent/article/details/79223673
https://www.jianshu.com/p/2e4e1007edf2
https://www.cnblogs.com/iullor/p/12434379.html
https://www.cnblogs.com/qizhelongdeyang/p/12418386.html
https://note.youdao.com/ynoteshare1/index.html?id=c4ea3f7bdfd1947f1c1e3a0c93350007&type=note&tdsourcetag=s_pctim_aiomsg