1、使用位置
接口实现类或接口实现方法上,而不是接口类中。
访问权限:public 的方法才起作用。@Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。
系统设计:将标签放置在需要进行事务管理的方法上,而不是放在所有接口实现类上:只读的接口就不需要事务管理,由于配置了@Transactional就需要AOP拦截及事务的处理,可能影响系统性能
2、注意点
1) 不要在类上标注Transactional注解,要在需要的方法上标注。即使类的每个方法都需要事务也不要在类上标注,因为有可能你或别人新添加的方法根本不需要事务。
2) 标注了Transactional注解的方法体中不要涉及耗时很久的操作,如IO操作、网络通信等。
3) 根据业务需要设置合适的事务参数,如是否需要新事务(事务的传播)、超时时间等。
3、使用姿势
3.1 单个方法
一般不需要在业务方法中catch异常,如果非要catch,在做完你想做的工作后(比如关闭文件等)一定要抛出runtime exception,否则spring会将你的操作commit,这样就会产生脏数据.所以你的catch代码是画蛇添足。
@Override
@Transactional(rollbackFor = Exception.class)
public Json addOrder(TOrderAddReq tOrderAddReq) {
try{
//do 增删改方法
} catch (Exception e) {
//有异常直接抛出
throw e;
}
return json;
}
3.2 try catch 影响
结论一:对于@Transactional可以保证RuntimeException错误的回滚,如果想保证非RuntimeException错误的回滚,需要加上rollbackFor = Exception.class 参数。
结论二:try catch只是对异常是否可以被@Transactional 感知 到有影响。如果错误抛到切面可以感知到的地步,那就可以起作用。
3.3 事务嵌套
嵌套事务,实际是指的spring事务的传播机制
@GetMapping("/xxx")
@Transactional
public void doXxx() throws Exception{
innerService.inner();
int age = random.nextInt(100);
User user = new User().setAge(age).setName("name:" + age);
userService.save(user);
throw new Exception();
}
@Transactional( rollbackFor = Exception.class)
public void inner() throws Exception{
//do 增删改操作
}
Transactional 注解中的事务传播默认设置如下:
//如果发现已经有事务存在了,就加入这个事务,而不是新建一个事务
Propagation propagation() default Propagation.REQUIRED;
使用小结:
结论一:
对于@Transactional可以保证RuntimeException错误的回滚,如果想保证非RuntimeException错误的回滚,需要加上rollbackFor = Exception.class 参数。
结论二:
try catch只是对异常是否可以被@Transactional 感知 到有影响。如果错误抛到切面可以感知到的地步,那就可以起作用。
结论三:
由于REQUIRED属性,“两个事务”其实是一个事务,处理能力看报错时刻,是否添加了处理非RuntimeException的能力。
附录:
几种事务失效的场景
上面说到的两个问题,其实就是@Transactional注解使用不当,导致失效的两种情形;除此之外,以下几种情况也会导致事务失效:
业务代码中存在异常时,使用try…catch…语句块捕获,而catch语句块没有throw new RuntimeExecption异常;(最难被排查到问题且容易忽略)
注解@Transactional中Propagation属性值设置错误即Propagation.NOT_SUPPORTED(一般不会设置此种传播机制)
mysql关系型数据库,且存储引擎是MyISAM而非InnoDB,则事务会不起作用(比较少见);
业务代码抛出异常类型非RuntimeException,事务失效;Spring默认抛出未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性
事务的传播行为
事务的传播行为也会影响到事务与事务之间的关系,一定要搞清楚,否则经常会出现很奇怪的问题。
具体来讲有以下几种属性:
propagation 代表事务的传播行为,默认值为 Propagation.REQUIRED,其他的属性信息如下:
Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 )
Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默Propagation.REQUIRED模式,类B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停 a方法的事务 )
Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务。
Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常。
Propagation.NESTED :和 Propagation.REQUIRED 效果一样。
事务的隔离级别
SQL标准定义了4种事务隔离级别来避免3种数据不一致的问题。事务等级从高到低,分别为:
1.Serializable(序列化)
系统中所有的事务以串行地方式逐个执行,所以能避免所有数据不一致情况。
但是这种以排他方式来控制并发事务,串行化执行方式会导致事务排队,系统的并发量大幅下降,使用的时候要绝对慎重。
2.Repeatable read(可重复读)
一个事务一旦开始,事务过程中所读取的所有数据不允许被其他事务修改。
一个隔离级别没有办法解决“幻影读”的问题。
因为它只“保护”了它读取的数据不被修改,但是其他数据会被修改。如果其他数据被修改后恰好满足了当前事务的过滤条件(where语句),那么就会发生“幻影读”的情况。
其他两种事务隔离等级为:
3.Read Committed(已提交读)
一个事务能读取到其他事务提交过(Committed)的数据。
一个事务在处理过程中如果重复读取某一个数据,而且这个数据恰好被其他事务修改并提交了,那么当前重复读取数据的事务就会出现同一个数据前后不同的情况。
在这个隔离级别会发生“不可重复读”的场景。
4.Read Uncommitted(未提交读)
一个事务能读取到其他事务修改过,但是还没有提交的(Uncommitted)的数据。
数据被其他事务修改过,但还没有提交,就存在着回滚的可能性,这时候读取这些“未提交”数据的情况就是“脏读”。
在这个隔离级别会发生“脏读”场景。
参考:
spring事务注解@Transactional与trycatch
https://www.jb51.net/article/215488.htm
spring @Transactional踩坑记
https://juejin.cn/post/7203989318271090744