1. 了解事务传播属性
事务传播属性描述的是Spring当中一个事务方法中调用了别的事务方法时,应该怎么处理别的事务方法。最常见的有三个REQUIRED
、REQUIRES_NEW
、NESTED
。
- 1.
REQUIRED
描述的是遇到这个事务方法,它将会共用调用方的事务。 - 2.
REQUIRES_NEW
描述的是遇到这个事务方法时,新创建一个事务去执行。 - 3.
NESTED
主要是涉及到Safepoint
的概念,方法执行前添加Safepoint
,方法执行后把Safepoint
删掉,发生异常就回滚到Safepoint
处。
具体的内容,在后续当中去进行体会,没有真实的案例比较难以说明。
我们有如下的两个Service
类,XXXService
和UserService
@Service
public class XXXService {
@Autowired
UserService userService;
@Transactional
public void transaction() {
userService.addUser();
userService.updateUser();
}
}
@Service
public class UserService {
@Transactional
public void addUser() {
// doSomething
}
@Transactional
public void updateUser() {
// doSomething
}
}
对于每个标注了@Transaction
的方法,都会被Spring
进行AOP
动态代理,其中拦截该方法的执行用到的spring-tx
包下的TransactionInterceptor
这个事务拦截器所拦截到,去执行代理的逻辑。
对于每个@Transaction
的方法,最终都会被类似如下的伪代码所调用(伪代码对于分析Spring
事务传播属性非常有作用,对于写业务代码的分析当中简直是神器!一定要牢记)
createTransactionIfNecessary // 如果必要的话,就创建一个新的事务
try {
// 执行目标方法.......
} catch(Throwable ex) {
// 回滚......
throw ex; // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务
套用这个模板,对transaction
方法的整个执行写出一个伪代码
createTransactionIfNecessary // 如果必要的话,就创建一个新的事务
try {
// 下面是addUser方法的被代理逻辑
createTransactionIfNecessary // 如果必要的话,就创建一个新的事务
try {
// 执行目标方法......
} catch(Throwable ex) {
// 回滚.......
throw ex; // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务
// 下面是updateUser方法的被代理逻辑
createTransactionIfNecessary // 如果必要的话,就创建一个新的事务
try {
// 执行目标方法......
} catch(Throwable ex) {
// 回滚.......
throw ex; // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务
} catch(Throwable ex) {
// 回滚.......
throw ex; // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务
需要注意的是:
- 1.并不是在每个代理方法执行完之后都有资格去提交/回滚事务的,只有
1'该代理方法是创建事务的代理方法
/2'该代理方法创建了Savepoint
这两种情况下,才有资格去提交/回滚事务。这个点是非常重要的! - 2.对于
REQUIRED
和REQUIRES_NEW
当中,都没有涉及到创建Savepoint
,对于创建Savepoint
的情况,在后面讲述NESTED
的情况下会进行进一步讲解,在REQUIRED
和REQUIRES_NEW
这两种情况下都是只有创建事务的目标代理方法才有资格去进行回滚/提交。
2. 对于REQUIRED
(默认)的情况进行分析
默认的事务传播属性为REQUIRED
,代表了addUser
和updateUser
这两个事务方法,将会和transaction
方法共用同一个事务,中途不会因为执行createTransactionIfNecessary
的执行而去创建事务。
也就是说对于上面的三个事务方法而言,只有transaction
方法是有资格去提交/回滚任务的,对于addUser
和updateUser
这两个方法都没有资格。
从上面的分析当中,我们可以知道在REQUIRED
的情况下,addUser
和updateUser
既不能新创建事务,也不能新提交/回滚事务,因此我们将模板的调用关系进行简化得到下面的伪代码。
createTransactionIfNecessary // 如果必要的话,就创建一个新的事务
try {
// 下面是addUser方法的被代理逻辑
try {
// 执行目标方法......
} catch(Throwable ex) {
throw ex; // 把捕获的异常往上抛
}
// 下面是updateUser方法的被代理逻辑
try {
// 执行目标方法......
} catch(Throwable ex) {
throw ex; // 把捕获的异常往上抛
}
} catch(Throwable ex) {
// 回滚.......
throw ex; // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务
对于全部采用默认(REQUIRED
)的情况,下面模拟几种情况:
- 1.如果三个方法都没抛出异常,那么事务执行到最后,因为
transaction
方法有资格提交事务,所以它将事务提交,然后调用结束。(结果:addUser
和updateUser
均提交成功) - 2.如果
transaction
方法执行过程中抛出了异常,那么直接在外层的catch
代码块当中去进行回滚并将异常抛给处理transaction
方法的上一级方法。(结果:addUser
、updateUser
要么没被执行,要么被回滚,最终一定都失败) - 3.如果
addUser
抛出了异常,那么直接就往上抛给transaction
代理方法了(updateUser
方法不会被执行到),被外层的transaction
代理方法捕获到,因为transaction
方法是有资格提交/回滚事务的,所以直接执行回滚,并且直接将异常抛给处理transaction
方法的上一级方法。(结果:addUser
被回滚,updateUser
没机会被执行) - 4.如果
updateUser
抛出了异常,那么addUser
方法是执行完了的,但是updateUser
方法出了异常,还是抛给transaction
代理方法了,因此也直接进行回滚,并且直接将异常抛给处理transaction
方法的上一级方法。(结果:addUser
和updateUser
都被回滚)
还有个问题,有些业务代码直接整个try-catch
代码块包住,比如如下代码
@Service
public class XXXService {
@Autowired
UserService userService;
@Transactional
public void transaction() {
try {
userService.addUser();
userService.updateUser();
} catch(Throwable ex) {
ex.printStackTrace();
}
}
}
这种情况下,伪代码变成了如下
createTransactionIfNecessary // 如果必要的话,就创建一个新的事务
try {
try {
// 下面是addUser方法的被代理逻辑
try {
// 执行目标方法......
} catch(Throwable ex) {
throw ex; // 把捕获的异常往上抛
}
// 下面是updateUser方法的被代理逻辑
try {
// 执行目标方法......
} catch(Throwable ex) {
throw ex; // 把捕获的异常往上抛
}
} catch(Throwable ex) {
ex.printStackTrace();
}
} catch(Throwable ex) {
// 回滚.......
throw ex; // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务
这种情况下,按照上面的分析,transaction
的代理方法很明显在最外层捕获不到异常,因此就不会进行回滚,但是其实Spring
这个框架考虑的很周到,只要内部抛出了异常,那么就会将一个标志位flag
改为true
,在执行transaction
代理方法进行判断时,也会直接去进行回滚,而不是去提交事务。
还有另一种情况,在addUser
/deleteUser
当中对整个代码使用try-catch
去进行包住了,那么Spring
肯定没机会将flag
改为true,最终肯定是不能对整个事务回滚,毕竟异常都被你抓了,Spring
可感知不到抛了异常的情况呢!
所以我们可以总结一个点:transaction
方法可以try-catch
,也可以不try-catch
,效果没什么不同,因为Spring
都能感知到并进行处理。但是如果你在addUser
、updateUser
方法上try-catch
,Spring
就感知不到异常的发生了,就需要根据你的业务来判断要不要try-catch
!
3. 对于REQUIRES_NEW
的情况进行分析
我们将UserService
的addUser
方法的事务传播属性改为了REQUIRES_NEW
。REQUIRES_NEW
代表的意思就是在执行这个方法的createTransactionIfNecessary
方法时,会创建一个事务(并将之前的事务挂起),既然是它创建的事务,它就有资格去进行提交。
@Service
public class UserService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addUser() {
// doSomething
}
@Transactional
public void updateUser() {
// doSomething
}
}
根据上面的分析,我们得到如下的伪代码
createTransactionIfNecessary // 如果必要的话,就创建一个新的事务
try {
// 下面是addUser方法的被代理逻辑
createTransactionIfNecessary // 如果必要的话,就创建一个新的事务
try {
// 执行目标方法......
} catch(Throwable ex) {
// 回滚.......
throw ex; // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务
// 下面是updateUser方法的被代理逻辑
try {
// 执行目标方法......
} catch(Throwable ex) {
throw ex; // 把捕获的异常往上抛
}
} catch(Throwable ex) {
// 回滚.......
throw ex; // 把捕获的异常往上抛
}
commitTransactionAfterReturning // 提交事务
我们再来分析各种情况:
- 1.如果各个方法都正常执行,
addUser
把自己的方法的事务提交了,transaction
也把自己的事务提交了(它的事务当中没有addUser
这个部分)。(结果:addUser
和updateUser
均成功提交) - 2.如果
addUser
方法出现异常(updateUser
不会被执行),因为它(REQUIRES_NEW
)有资格去回滚自己的事务,因此它先将自己的事务回滚了,然后再把异常抛给transaction
代理方法,然后transaction
代理方法捕获到了异常,因此它也将自己的事务进行了回滚,并且直接将异常抛给处理transaction
方法的上一级方法。(结果:addUser
被回滚,updateUser
没执行) - 3.如果
updateUser
方法出现异常(addUser
没出现异常,而它有资格提交自己的事务,因此addUser
执行成功了!),和之前的情况一样,它没资格回滚事务,将异常抛给transaction
代理方法,然后transaction
代理方法捕获到了异常,将自己的事务进行了回滚,并且直接将异常抛给处理transaction
方法的上一级方法。(结果:addUser
提交成功,updateUser
被回滚了) - 4.如果
transaction
方法在addUser
之前就抛出了异常,那么addUser
想新开事务也没机会,transaction
整个事务直接回滚。(结果:addUser
和updateUser
都没机会执行) - 5.如果在
transaction
方法在addUser
之后抛出了异常,那么addUser
肯定将自己的事务提交了!但是transaction
这个发生事务回滚了!(结果:addUser
提交成功,addUser
要么没执行,要么被回滚,反正最终是失败了)
4. 对于NESTED
的情况进行分析
对于NESTED
(翻译成中文叫嵌套),主要涉及到的内容有:
- 1.对于
NESTED
的事务方法,不会创建一个新的事务,它不能执行提交事务,但是它能回滚事务到它自己设置的检查点(其实这是Safepoint
的特点)。 - 2.在
NESTED
的事务代理方法的执行时,会首先创建一个Safepoint
,然后去执行目标方法。- 如果目标方法执行成功,那么会将刚才创建的
Safepoint
删掉,并将REQUIRED
中提到的那个flag
置为false
(这个flag
的点很关键) - 如果目标方法执行失败,将回滚到刚刚设置的
Safepoint
处去。
- 如果目标方法执行成功,那么会将刚才创建的
对于Safepoint
的概念,其实在数据库层面(比如MySQL
)上已经给我们进行了提供支持,在JDBC
中也给我们提供了相关的支持。比如在如下的代码当中,设置了一个回滚点(Safepoint
),执行了一堆操作之后,因为某些原因我突然想回滚到我刚刚设置到的回滚点处的情况去。
Connection connection = DriverManager.getConnection("...");
Savepoint savepoint = connection.setSavepoint("wanna");
// doSomething...
connection.rollback(savepoint);
我们将UserService
中将两个方法的事务传播行为都改成NESTED
@Service
public class UserService {
@Transactional(propagation = Propagation.NESTED)
public void addUser() {
// doSomething
}
@Transactional(propagation = Propagation.NESTED)
public void updateUser() {
// doSomething
}
}
XXXService
中的内容保持不变
@Service
public class XXXService {
@Autowired
UserService userService;
@Transactional
public void transaction() {
userService.addUser();
userService.updateUser();
}
}
理想情况下的执行顺序如下:
1.在transaction的代理方法中开启事务
2.添加一个Safepoint(假设为sp1)
3.执行addUser方法
4.删除Safepoint(sp1)
5.添加一个Safepoint(假设为sp2)
6.执行updateUser方法
7.删除Safepoint(sp2)
8.提交事务......
我们来假想一种情况:假如addUser
执行没抛异常,updateUser
执行抛出了异常,我们使用Safepoint
想要实现的目标就是updateUser
给我回滚了,但是addUser
能成功提交。
但是事实上执行第6步时抛出异常,回滚到sp2(updateUser
回滚了)并且直接抛给transaction
代理方法,transaction
代理方法拿到这个异常,把整个事务给回滚了,最终addUser
也被回滚了,很显然不符合我们想要实现的预期,但是Spring
对这个点的实现其实很巧妙,它将flag
修改为false
了。
如果我们将整个方法块用try-catch
包起来,并且不把异常抛给上级,因为flag
为false
,直接就执行提交操作,但是这个过程中updateUser
其实已经被回滚掉了,addUser
则得到了提交。
@Service
public class XXXService {
@Autowired
UserService userService;
@Transactional
public void transaction() {
try {
userService.addUser();
userService.updateUser();
} catch (Throwable ex) {
ex.printStackTrace();
}
}
}
所以我们可以总结一个点:在使用NESTED
时,要想实现我们的目标效果,需要对整个方法进行try-catch
进行捕获,别让Spring
捕获到异常。
网友评论