为什么要说事务?
一方面业务开发中经常要考虑事务,比如调用第三方接口失败时处理本地数据状态时要考虑事务,结算时A账户增加失败时B账户也不能扣款成功。。。总之,在遇到原子性的操作,要成功都成功,要失败都失败的场景时,事务肯定是要考虑的!
另一方面,“你有没有遇到事务失效的情况?” 或者 “哪些情况下事务会失效?”已经是面试时的高频问题,都爱问这个,我就被问过!这里学习整理下还是必要的,谁不想以后吊打面试官呢!
本文所有实例代码下载地址: https://github.com/ImOk520/myspringcloud
一、事务失效的情况
这里所有的例子都用下面这个更新操作演示:
<update id="update">
update dept
set dname = "更新1"
where deptno = #{deptno}
</update>
数据库初始值为:
代码里我们让异常发生在数据更新操作之后,即若事务有效,那就会异常后回滚,若事务失效,那即使异常发生了,数据库也依然完成了更新操作。
1.1 没加@Transactional注解
"/transaction")(
public class TransactionController {
private DeptService deptService;
("/test1")
// @Transactional
public void test1(Long deptno) {
deptService.update(deptno);
int i = 10/0;
}
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
public void update(Long deptno) {
deptDao.update(deptno);
}
}
可以看到上面的代码没有加上@Transactional注解,执行时提示JDBC不会被Spring所管理。那可以猜到,上面报异常后,数据不会回滚依然被修改:
可以看到数据名称已修改,这就是事务没生效或者说没有考虑事务出现的情况。
其他的不变(当然数据库把名字改下,后面就不说了,每次测试前改下),加上@Transactional注解:
"/transaction")(
public class TransactionController {
private DeptService deptService;
("/test1")
public void test1(Long deptno) {
deptService.update(deptno);
int i = 10/0;
}
}
这时再测试:
此时已经事务已经被Spring管理,出现异常数据库会回滚:
数据库的值没变,说明事务已生效。
1.2 类内部访问(自身调用)
这个咋说呢,简单的说就是:不带事务的方法调用该类中带事务的方法,不会回滚。
比如:A要调@Transactional注解标记的B方法,不直接去调,而是让和B同一个类中的普通方法C去调用B,然后A再调C。调用链是A-->C-->B,实例如下:
("/transaction")
public class TransactionController {
private DeptService deptService;
/**
* @Transactional 自身调用不会触发事务
*/
("/test3")
public void test3(Long deptno) {
deptService.testTransaction(deptno);
}
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
public void testTransaction(Long deptno){
update(deptno);
}
public void update(Long deptno) {
deptDao.update(deptno);
int i = 10/0;
}
}
此时事务不起作用,数据依然更新:
这种怎么解决呢?有两种方式:
1.2.1 在方法C上开启事务,方法B不用事务或默认事务
("/transaction")
public class TransactionController {
private DeptService deptService;
/**
* @Transactional 自身调用不会触发事务
*/
("/test3")
public void test3(Long deptno) {
deptService.testTransaction(deptno);
}
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
public void testTransaction(Long deptno){
update(deptno);
}
public void update(Long deptno) {
deptDao.update(deptno);
int i = 10/0;
}
}
可以看到数据库的更新操作没有完成,说明事务已回滚。
是不是有个疑问,这就是把@Transactional()注解提前到了C上,为什么就能使事务起作用呢?
因为@Transactional()注解在没有指定rollbackFor时,默认回滚异常为RuntimeException,在update()方法中一定会报jArithmeticException,而ArithmeticException extends RuntimeException,所以在B方法异常时,C方法上面的事务就会捕捉到并回滚。
同理,假如把@Transactional()注解提前到A上也会激活事务,即在从controller层加上注解:
("/transaction")
public class TransactionController {
private DeptService deptService;
("/test3")
public void test3(Long deptno) {
deptService.testTransaction(deptno);
}
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
public void testTransaction(Long deptno){
update(deptno);
}
public void update(Long deptno) {
deptDao.update(deptno);
int i = 10/0;
}
}
这样事务也会回滚。
1.2.2 还是只在B上加注解,但是C通过代理调用B
这个要回头考虑一下,为什么一开始的内部调用或者不带事务的方法调用该类中带事务的方法会出现事务失效?
因为spring的回滚基于代理模式,若一个不带事务的方法调用该类的带事务的方法,即通过this.xxx()
调用,不会生成代理事务,所以事务不起作用。
那就好解决了,那我在不带事务的方法中通过该类的代理去调用该类的带事务的方法就行了:
("/transaction")
public class TransactionController {
private DeptService deptService;
/**
* @Transactional 自身调用不会触发事务
*/
("/test3")
public void test3(Long deptno) {
deptService.testTransaction(deptno);
}
}
(proxyTargetClass = true, exposeProxy = true)
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
public void testTransaction(Long deptno){
DeptServiceIml proxy = (DeptServiceIml) AopContext.currentProxy();
proxy.update(deptno);
}
public void update(Long deptno) {
deptDao.update(deptno);
int i = 10/0;
}
}
可以看到事务回滚了。
要注意的是添加@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)注解去开启这个类的代理。
1.3 被异常处理了(指try catch,不是throws Exception)
这种事务失效的情况就很好理解了,异常被try catch了都没抛出来当然不会触发事务。
("/transaction")
public class TransactionController {
private DeptService deptService;
/**
* @Transactional 即使加入spring管理,若被try catch处理了,也不会生效,数据仍被改变
* Feng, Ge
*/
("/test2")
public void test2(Long deptno) {
deptService.update(deptno);
try {
int i = 10/0;
} catch (ArithmeticException e) {
log.info("ok_ok_ok");
e.printStackTrace();
}
}
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
public void update(Long deptno) {
deptDao.update(deptno);
}
}
可以看到在遇到异常时事务未回滚,原因是这个ArithmeticException: / by zero异常被try catch了,并没有被@Transactional()注解默认的RuntimeException回滚异常捕捉到,因此事务不会生效。实际上try catch这种异常处理会失效,若这里throws ArithmeticException事务会回滚:
"/test1")(
public void test1(Long deptno) throws ArithmeticException{
deptService.update(deptno);
int i = 10/0;
}
这里涉及一些异常的概念,参见: 异常看这里 。异常除了RuntimeException运行时异常(非检测异常),还有非运行时异常(或者叫检测异常、编译时异常)。非运行时异常比如IOException,FileNotFoundException等,有没有想过若写成@Transactional(rollbackFor = IOException.class),那方法中若抛出运行时异常比如ArithmeticException,如下,此时事务是否回滚?
"/test1")(
(rollbackFor = IOException.class)
public void test1(Long deptno) {
deptService.update(deptno);
int i = 10/0;
}
可以看到依然回滚了。为啥?实际上rollbackFor是一个异常类集合,上面虽然只有IOException,但实际上对RuntimeException异常还是有效的,因此会回滚。
那方法体内要是抛出非运行时异常,@Transactional(rollbackFor = Exception.class)会使事务回滚吗?实际上,这种情况不用考虑,因为非运行时异常也是编译异常,在编译期间就会强制进行异常处理。若采用try catch处理则肯定事务会失效,若采用throws Exception则不会失效。
总结起来,就是在try catch时要注意事务是否会受影响,throws Exception不影响事务回滚。
1.4 异常类型不匹配
这个实际上也很好理解:若方法体内抛出的是非运行时异常,但是rollbackFor数组里只有RuntimeException时自然事务会失效,如下:
"/test7")(
public void test7(Long deptno) throws Exception {
deptService.update(deptno);
File file = new File("aa");
InputStream inputStream = new FileInputStream(file);
}
public void update(Long deptno) {
deptDao.update(deptno);
}
可以看到事务没有回滚。想让回滚也很简单,修改下rollbackFor = Exception.class即可:
"/test7")(
(rollbackFor = Exception.class)
public void test7(Long deptno) throws Exception {
deptService.update(deptno);
File file = new File("aa");
InputStream inputStream = new FileInputStream(file);
}
事务已回滚。
1.5 Transactional注解标注方法修饰符为非public时
实际上这个在代码里一般不会出现,在非public方法上加上@Transactional注解时,IDEA就会报错提示:
1.6 数据库引擎不支持事务
检查下自己的数据库是否支持事务,如mysql的mylsam就不支持事务。
1.7 propagation属性设置错误
默认的事务传播属性是Propagation.REQUIRED,但是一旦配置了错误的传播属性,也是会导致事务失效,如下三种配置将会导致事务失效:
- Propagation.SUPPORTS
- Propagation.NOT_SUPPORTED
- Propagation.NEVER
本文后面将详细介绍事务的各传播类型,再仔细说明这三种事务传播类型使得事务失效的情况。
二、事务传播类型实例
@Transactional传播类型有:
- REQUIRED(如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,这是最常见的选择,也是Spring默认的事务传播行为。)
- SUPPORTS(支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。)
- MANDATORY(支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。)
- REQUIRES_NEW(无论当前存不存在事务,都创建新事务。)
- NOT_SUPPORTED(以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。)
- NEVER(以非事务方式执行,如果当前存在事务,则抛出异常。)
- NESTED(如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。)
默认传播属性为REQUIRED。
2.1 REQUIRED
REQUIRED这种事务传播类型可以使得:
外部方法未开启事务时,内部方法(被调用方法)在自己的事务中独立运行,外部方法异常不影响内部方法。(或者也可以进一步说:如果有事务则加入事务,如果没有事务,则创建一个新的事务。)
先看一个例子:
"/test66")(
public void test66(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
public void update(String id) {
employeeDao.update(id);
}
}
可以看到两个表都更新成功了。这是为啥?
可以看到controller方法/test66 没有开启事务,两个内部的service则开启了事务,且传播类型是默认的REQUIRED类型,因此两个内部方法分别开启自己独立的事务,即使外部方法异常也不影响两个内部方法的事务执行。
事务是独立的很好理解,要是在其中的一个内部方法中也抛出异常,另一个方法也不会受影响:
"/test66")(
public void test66(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
public void update(String id) {
employeeDao.update(id);
int i = 10/0;
}
}
employee异常回滚,但是dept正常更新了,互不影响。
但是哈,如果外部方法开启了事务,那么在传播类型是REQUIRED时,两个内部方法的事务就不在独立而是会加入外部方法的事务,成为一个事务,无论哪里异常,所有都会回滚:
"/test66")(
public void test66(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
public void update(String id) {
employeeDao.update(id);
int i = 10/0;
}
}
上面employee异常导致一块回滚,主要就是外部方法的事务把下面的事务统一成了一个事务。
2.2 SUPPORTS
SUPPORTS类型:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
"/test67")(
public void test67(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.SUPPORTS)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.SUPPORTS)
public void update(String id) {
employeeDao.update(id);
}
}
外部方法未开启事务,更新dept表和employee表的方法以非事务的方式独立运行,外部方法异常不影响内部更新,所以两条记录都更新成功。这个地方也解释了上面1.7小节 propagation属性设置错误---SUPPORTS 导致事务失效的场景。
关于SUPPORTS类型我们再看下另一种场景,那就是假如外部方法开启了事务:
"/test67")(
public void test67(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.SUPPORTS)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.SUPPORTS)
public void update(String id) {
employeeDao.update(id);
}
}
外部方法开启事务,内部方法加入外部方法事务,外部方法回滚,内部方法也要回滚,所以两个记录都更新失败。
综上,在外部方法开启事务的情况下Propagation.SUPPORTS修饰的内部方法会加入到外部方法的事务中,所以Propagation.SUPPORTS修饰的内部方法和外部方法均属于同一事务,只要一个方法回滚,整个事务均回滚。
SUPPORTS和REQUIRED的区别在外部有事务时一样,但在外部方法没有事务时,REQUIRED是没有我就创建新的,SUPPORTS是没有拉倒,就以非事务运行。
2.3 MANDATORY
MANDATORY: 支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
先看外部方法没有事务的情况:
"/test68")(
public void test68(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.MANDATORY)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.MANDATORY)
public void update(String id) {
employeeDao.update(id);
}
}
两个表都没有更新数据,即“如果当前不存在事务,就抛出异常”,事务回滚。
外部方法开启事务的情况:
"/test68")(
public void test68(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.MANDATORY)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.MANDATORY)
public void update(String id) {
employeeDao.update(id);
}
}
这是虽然不再报异常,但是事务还是回滚了。
也就是MANDATORY事务传播类型不会造成事务失效这种情况。
2.4 REQUIRES_NEW
REQUIRES_NEW: 无论当前存不存在事务,都创建新事务。
"/test68")(
public void test68(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.REQUIRES_NEW)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.REQUIRES_NEW)
public void update(String id) {
employeeDao.update(id);
}
}
无论当前存不存在事务,都创建新事务,所以两个数据新增成功。
2.5 NOT_SUPPORTED
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
外部方法没事务时:
"/test68")(
public void test68(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.NOT_SUPPORTED)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.NOT_SUPPORTED)
public void update(String id) {
employeeDao.update(id);
}
}
以非事务方式执行,所以两个数据更新成功。
外部启用事务时:
"/test68")(
public void test68(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.NOT_SUPPORTED)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.NOT_SUPPORTED)
public void update(String id) {
employeeDao.update(id);
}
}
如果当前存在事务,就把当前事务挂起,相当于以非事务方式执行,所以两个数据更新成功。
综上,无论外部是否有事务,只要是NOT_SUPPORTED类型就意味着和其他事务“撇清关系”了,管你怎么弄,我反正就是以非事务执行。
2.6 NEVER
NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
外部方法没有事务时:
"/test68")(
public void test68(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.NEVER)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.NEVER)
public void update(String id) {
employeeDao.update(id);
}
}
外层没有事务会以非事务的方式运行,两个表更新成功。
外部方法启用事务时:
"/test68")(
public void test68(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.NEVER)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.NEVER)
public void update(String id) {
employeeDao.update(id);
}
}
有事务则抛出异常,两个表都都没有更新数据。
2.7 NESTED
NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
外部方法没有事务时:
"/test68")(
public void test68(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.NESTED)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.NESTED)
public void update(String id) {
employeeDao.update(id);
}
}
外层没有事务会以REQUIRED属性的方式运行(外部方法未开启事务时,内部方法(被调用方法)在自己的事务中独立运行,外部方法异常不影响内部方法。),两个表更新成功;
外部方法启用事务时:
"/test68")(
public void test68(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
int i = 10/0;
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.NESTED)
public void update(Long deptno) {
deptDao.update(deptno);
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.NESTED)
public void update(String id) {
employeeDao.update(id);
}
}
如果当前存在事务,则在嵌套事务内执行,这里外部的事务报异常,会影响所有内部方法的事务,所以外部方法异常内部方法的事务都回滚。
既然是嵌套,那再考虑一种场景——异常来自某个内部方法:
"/test68")(
public void test68(Long deptno, String id) {
deptService.update(deptno);
employeeService.update(id);
}
public class DeptServiceIml implements DeptService {
private DeptDao deptDao;
(propagation = Propagation.NESTED)
public void update(Long deptno) {
deptDao.update(deptno);
int i = 10/0;
}
}
public class EmployeeServiceIml implements EmployeeService {
private EmployeeDao employeeDao;
(propagation = Propagation.NESTED)
public void update(String id) {
employeeDao.update(id);
}
}
都会滚了。因为dept内部方法异常,会使dept表更新回滚,外部方法也会更正回滚,接着employee也回滚。
到此Spring下事务失效的场景及7种事务传播行为已经全部介绍完成了,有了这些面试官会给你点赞!
补充一、@Transactional注解的全部属性
属性 | 类型 | 描述 |
value | String | 可选的限定描述符,指定使用的事务管理器 |
propagation | enum: Propagation | 可选的事务传播行为设置 |
isolation | enum: Isolation | 可选的事务隔离级别设置 |
readOnly | boolean | 读写或只读事务,默认读写 |
timeout | int (in seconds granularity) | 事务超时时间设置 |
rollbackFor | Class对象数组,必须继承自Throwable | 导致事务回滚的异常类数组 |
rollbackForClassName | 类名数组,必须继承自Throwable | 导致事务回滚的异常类名字数组 |
noRollbackFor | Class对象数组,必须继承自Throwable | 不会导致事务回滚的异常类数组 |
noRollbackForClassName | 类名数组,必须继承自Throwable | 不会导致事务回滚的异常类名字数组 |