@Transactiona
SpringBoot使用@Transactional
注解配置事务
1.详细介绍
Spring 事务管理分为编程式和声明式的两种方式。
- 编程式事务指的是通过编码方式实现事务;
- 声明式事务基于 AOP,将具体业务逻辑与事务处理解耦。
声明式事务有两种方式:
- 在配置文件(xml)中做相关的事务规则声明。
- 基于
@Transactional
注解的方式。
@Transactional
可以作用于接口、接口方法、类以及类方法上。Spring
不建议在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。- 作用于类上时,该类的所有
public
方法将都具有该类型的事务属性。同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。 - 只有来自外部的方法调用才会被
AOP
代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional
注解进行修饰。
2.使用方法
注: SpringBoot项目会自动配置一个 DataSourceTransactionManager
,所以只需在方法(或者类)加上 @Transactional
注解,就自动纳入 Spring
的事务管理了。
如下在方法加上 @Transactional
注解:
1 |
|
3.Spring事务的默认回滚机制
Spring的事务管理默认是针对 unchecked exception 回滚,也就是默认对Error
异常和RuntimeException
异常以及其子类进行事务回滚,且必须抛出异常,若使用try-catch对其异常捕获则不会进行回滚!(Error
异常和RuntimeException
异常抛出时不需要方法调用throws或try-catch
语句);
而 checked exception 则必须用try
语句块进行处理或者把异常交给上级方法处理总之就是必须写代码处理它,所以必须在service
捕获异常,然后再次抛出,这样事务方才起效。
4.@Transactional 注解属性介绍
源码如下:
1 | ({ElementType.TYPE, ElementType.METHOD}) |
4.1、value 和 transactionManager 属性:
这两个是一样的意思。当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器。
4.2、propagation 属性:(默认值为 Propagation.REQUIRED)
propagation事务的传播行为,可选的值有:
4.2.1、Propagation.REQUIRED:
如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。如a方法和b方法都添加了注解,使用默认传播模式,则a方法内部调用b方法,会把两个方法的事务合并为一个事务。
这里又会存在问题,如果b方法内部抛了异常,而a方法catch
了b方法的异常,那这个事务还能正常运行吗?
答案是不行!会抛出异常:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
,因为当ServiceB中抛出了一个异常以后,ServiceB会把当前的transaction
标记为需要rollback
。但是ServiceA中捕获了这个异常,并进行了处理,认为当前transaction
应该正常commit
。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException
。
4.2.2、Propagation.SUPPORTS:
如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
4.2.3、Propagation.MANDATORY:
如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
4.2.4、Propagation.NEVER:
以非事务的方式运行,如果当前存在事务,则抛出异常。( 与Propagation.MANDATORY
相反 )
4.2.5、Propagation.REQUIRES_NEW:
重新创建一个新的事务,如果当前存在事务,暂停当前的事务。这个属性可以实现:
类A中的a方法加上默认注解@Transactional(propagation = Propagation.REQUIRED)
,类B中的b方法加上注解@Transactional(propagation = Propagation.REQUIRES_NEW)
,然后在a方法中调用b方法操作数据库,再在a方法最后抛出异常,会发现a方法中的b方法对数据库的操作没有回滚,因为Propagation.REQUIRES_NEW
会暂停a方法的事务。
4.2.6、Propagation.NOT_SUPPORTED:
不为这个方法开启事务 。
4.2.7、Propagation.NESTED:
和 Propagation.REQUIRED
效果一样。
4.3、isolation 属性:默认值为 Isolation.DEFAULT
isolation 事务的隔离级别,可选的值有:
- Isolation.DEFAULT:使用底层数据库默认的隔离级别。 (MySQL的默认事务隔离级别是可重复读)
- Isolation.READ_UNCOMMITTED: 未提交读
- Isolation.READ_COMMITTED: 已提交读
- Isolation.REPEATABLE_READ: 可重复读
- Isolation.SERIALIZABLE: 串行化
幻读和不可重复读相似容易混淆,幻读指的是第一个事务相同查询条件的查询行数,另一个事务增加或删除了某行(inserts a row
),导致第一个事务两次查询的结果不同。不可重复读指的是另一个事务修改( alters the row
)了某行的数据。
隔离和锁是不同的东西,隔离不是靠锁实现,是根据对数据的监控实现的,相比锁会回滚事务。
4.4、timeout 属性
事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
4.5、readOnly 属性
指定事务是否为只读事务,默认值为 false
,即非只读事务;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only
为 true
。( 注意: 在只读事务中修改数据库是会报错的! )
4.6、rollbackFor 属性
指定能够触发事务回滚的异常类型,可以指定多个异常类型。
4.7、noRollbackFor 属性
指定哪些异常类型不回滚事务。
5.@Transactional事务几点注意
A. 一个功能是否要事务,必须纳入设计、编码考虑。不能仅仅完成了基本功能就ok。
B. 如果加了事务,必须做好开发环境测试(测试环境也尽量触发异常、测试回滚),确保事务生效。
C. 以下列了事务使用过程的注意事项:
- 不要在接口上声明
@Transactional
,而要在具体类的方法上使用@Transactional
注解,否则注解可能无效。 - 不要图省事,将
@Transactional
放置在类级的声明中。放在类声明,会使得所有方法都有事务。故@Transactional
应该放在方法级别,不需要使用事务的方法,就不要放置事务,如查询方法。否则对性能是有影响的。 - 使用了
@Transactional
的方法,对同一个类里面的方法调用,@Transactional
无效。比如有一个类Test
,它的一个方法A,A再调用Test
本类的方法B(不管B是否public
还是private
),但A没有声明注解事务,而B有。则外部调用A之后,B的事务是不会起作用的。(经常在这里出错) - 使用了
@Transactional
的方法,只能是public
,@Transactional
注解的方法都是被外部其他类调用才有效,故只能是public
。故在protected
、private
或者package-visible
的方法上使用@Transactional
注解,它也不会报错,但事务无效。 spring
的事务在抛异常的时候会回滚,如果是catch
捕获了,事务无效。可以在catch
里面加上throw new RuntimeException()
;- 和锁同时使用需要注意:
由于Spring
事务是通过AOP
实现的,所以在方法执行之前会有开启事务,之后会有提交事务逻辑。而synchronized
代码块执行是在事务之内执行的,可以推断在synchronized
代码块执行完时,事务还未提交,其他线程进入synchronized
代码块后,读取的数据不是最新的。
所以必须使synchronized
锁的范围大于事务控制的范围,把synchronized
加到Controller
层或者大于事务边界的调用层!
6.总结事务失效的场景
- 应用在非public方法上。
- 同一个类中方法的调用。
- 数据库引擎不支持事务,如innodb支持事务,而myisam就不支持事务。
- 没有指定rollbackFor哪些异常回滚时,spring默认的是运行时异常和error,如果不属于这些异常也就不会触发事务回滚。或者异常被catch掉了也不会触发事务回滚。
- 传播机制设置为非事务方式。