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捕获到异常。