目录
- 背景
- Propagation
- 测试程序1
- 测试程序2
- 分析
背景
前段时间,某个项目在部署时,被公司的一个检测拦截了,提示报错如下:
Your code exists Method or Class with @Transactional annotation that not use Propagation.REQUIRED.
有1个方法加了常见的@Transactional注解,但是事务传播行为设置的不是默认的Propagation.REQUIRED,而是Propagation.REQUIRES_NEW。
那么,默认的Propagation.REQUIRED做了些啥呢,不设置为这个又有什么隐患呢?
关于@Transactional注解,之前也写过一篇文章: @Transational踩坑。
Propagation
Propagation是org.springframework.transaction.annotation包下的枚举,一共有7种。
级别 | 英文释义 | 中文释义 | 是否获取新链接 |
---|---|---|---|
REQUIRED | Support a current transaction, create a new one if none exists. This is the default setting of a transaction annotation. | 支持当前事务,如果不存在,则创建一个新事务。这是事务注释的默认设置。 | × |
REQUIRES_NEW | Create a new transaction, and suspend the current transaction if one exists. | 创建一个新事务,如果存在当前事务,则暂停该事务。 | √ |
SUPPORTS | Support a current transaction, execute non-transactionally if none exists. | 支持当前事务,如果不存在,则以非事务方式执行。 | × |
NOT_SUPPORTED | Execute non-transactionally, suspend the current transaction if one exists. | 以非事务方式执行,如果存在当前事务,则暂停当前事务。 | √ |
NEVER | Execute non-transactionally, throw an exception if a transaction exists. | 以非事务方式执行,如果存在事务,则引发异常。 | × |
MANDATORY | Support a current transaction, throw an exception if none exists. | 支持当前事务,如果不存在则抛出异常。 | × |
NESTED | Execute within a nested transaction if a current transaction exists, behave like REQUIRED otherwise. | 如果当前事务存在,则在嵌套事务中执行,否则行为类似于 REQUIRED。 | × |
上面的表格中在描述事务传播行为时,都有前置条件,那就是是否自己是一个嵌套事务。也就是调用本方法的上层方法是否是一个事务,下面仔细分析在嵌套事务下的各种行为。
测试程序1
插入user表,2个用户,user1.年龄=10,user2.年龄=20;ClassA更新user1.年龄=11(事务,默认的Propagation.REQUIRED),ClassB更新user1.年龄=12(事务,测试不同的Propagation);ClassA和ClassB构成嵌套事务。
@Component
@RequiredArgsConstructor
public class Test {
private final ClassA classA;
public void test(Propagation propagation) {
// insert into user, user_name = 'user1', age = 10;
// insert into user, user_name = 'user2', age = 20;
classA.updateUserAge(propagation, "user1", "user2");
}
}
@Component
@RequiredArgsConstructor
@Slf4j
public class ClassA {
private final ClassB classB;
@Transactional
public void updateUserAge(Propagation propagation, String userName1, String userName2) {
// update user set age = 11 where user_name = userName1
try {
switch (propagation) {
case REQUIRED:
classB.required(userName1, userName2);
return;
case REQUIRES_NEW:
classB.requiresNew(userName1, userName2);
return;
case SUPPORTS:
classB.supports(userName1, userName2);
return;
case NOT_SUPPORTED:
classB.notSupported(userName1, userName2);
return;
case NEVER:
classB.never(userName1, userName2);
return;
case MANDATORY:
classB.mandatory(userName1, userName2);
return;
case NESTED:
classB.nested(userName1, userName2);
return;
default:
}
} catch (Exception e) {
log.error("ClassA#updateUserAge error: {}", e.getMessage(), e);
}
}
}
@Component
@RequiredArgsConstructor
public class ClassB {
@Transactional(propagation = Propagation.REQUIRED)
public void required(String userName1, String userName2) {
updateUserAge(userName1, userName2);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNew(String userName1, String userName2) {
updateUserAge(userName1, userName2);
}
@Transactional(propagation = Propagation.SUPPORTS)
public void supports(String userName1, String userName2) {
updateUserAge(userName1, userName2);
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupported(String userName1, String userName2) {
updateUserAge(userName1, userName2);
}
@Transactional(propagation = Propagation.NEVER)
public void never(String userName1, String userName2) {
updateUserAge(userName1, userName2);
}
@Transactional(propagation = Propagation.MANDATORY)
public void mandatory(String userName1, String userName2) {
updateUserAge(userName1, userName2);
}
@Transactional(propagation = Propagation.NESTED)
public void nested(String userName1, String userName2) {
updateUserAge(userName1, userName2);
}
private void updateUserAge(String userName1, String userName2) {
// update user set age = 12 where user_name = userName1
throw new RuntimeException("ClassB#updateUserAge RuntimeException");
}
}
测试结果:
行为 | 报错信息 | 执行结果 |
---|---|---|
REQUIRED | org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only | 年龄未被更新 |
REQUIRES_NEW | com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction | 年龄被更新=11(执行了ClassA.SQL,未执行ClassB.SQL) |
SUPPORTS | 同REQUIRED | 同REQUIRED |
NOT_SUPPORTED | 同REQUIRES_NEW | 同REQUIRES_NEW |
NEVER | org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation ‘never’ | 同REQUIRES_NEW |
MANDATORY | 同REQUIRED | 同REQUIRED |
NESTED | org.springframework.transaction.CannotCreateTransactionException: Could not create JDBC savepoint; nested exception is java.sql.SQLFeatureNotSupportedException: setSavepoint name | 同REQUIRES_NEW |
- REQUIRED
由于ClassB本身会发生异常,所以ClassB中的update会进行回滚,并且由于REQUIRED是复用父事务,所以链接也是同一个,在回滚时,会将ClassA中的update也进行回滚。
回滚完毕之后,会将异常抛至ClassA中,并被捕获,正常运行结束,进行提交时由于已经在ClassB中进行了回滚,所以会抛出异常UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only。 - REQUIRES_NEW
进入ClassB时,REQUIRES_NEW会获取新的链接,并且update的是同一条数据,会获取锁超时,导致ClassB的update会被回滚。
剩下的和REQUIRED一样,只是在最后提交的时候,由于ClassB是新起一个事务,所以ClassA不会出异常,成功update年龄=11。 - SUPPORTS
由于ClassA存在事务,所以跟REQUIRED的处理一样。 - NOT_SUPPORTED
ClassB以非事务执行,但update的是同一条数据,也会获取锁超时,跟REQUIRES_NEW的处理一样。 - NEVER
ClassA存在事务,因此ClassB抛出异常IllegalTransactionStateException: Existing transaction found for transaction marked with propagation ‘never’,ClassA捕获后不影响ClassA的update,因此成功update年龄=11。 - MANDATORY
同REQUIRED。 - NESTED
到ClassB后,先update,再抛异常,由于本身是事务,所以会回滚。
和REQUIRED不一样的地方是,虽然NESTED在子事务不会获取新的链接,但是会设置一个savepoint,即ClassB抛异常后,会回滚到ClassB未执行之前,此时抛出异常后,虽然会被ClassA捕获,但是能够成功提交,不会发生REQUIRED的异常,因此成功update年龄=11。
测试程序2
改一下程序1的ClassB,ClassB更新user2.年龄=12,即与ClassA更新2条不同的数据。
测试结果:
行为 | 报错信息 | 执行结果 |
---|---|---|
REQUIRED | org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only | 2个user.年龄均未被更新 |
REQUIRES_NEW | java.lang.RuntimeException: ClassB#updateUserAge RuntimeException | user1.年龄被更新=11,user2.年龄未被更新(执行了ClassA.SQL,未执行ClassB.SQL) |
SUPPORTS | 同REQUIRED | 同REQUIRED |
NOT_SUPPORTED | 同REQUIRES_NEW | user1.年龄被更新=11,user2.年龄被更新=12(执行了ClassA和ClassB.SQL) |
NEVER | org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation ‘never’ | 同REQUIRES_NEW |
MANDATORY | 同REQUIRED | 同REQUIRED |
NESTED | org.springframework.transaction.CannotCreateTransactionException: Could not create JDBC savepoint; nested exception is java.sql.SQLFeatureNotSupportedException: setSavepoint name | 同REQUIRES_NEW |
- REQUIRED
同程序1。 - REQUIRES_NEW
由于2个Class不更新同条数据,所以未出现程序1的获取锁超时,后续流程同程序1。 - SUPPORTS
同程序1。 - NOT_SUPPORTED
不存在获取锁超时,且ClassB以非事务执行,因此即使抛出异常,也不影响update,ClassA捕获异常也不影响update,因此2个update均被执行。 - NEVER
同程序1。 - MANDATORY
同程序1。 - NESTED
同程序1。
分析
通过2个测试程序,回到最开始的问题,项目里的那个方法不存在嵌套事务,因此指定为Propagation.REQUIRES_NEW其实是与Propagation.REQUIRED的效果一致,所以这里可删除掉Propagation.REQUIRES_NEW,避免歧义。
日常开发中,应该尽量避免嵌套事务,避免重复获取链接,获取新的链接本身会带来很多隐患:
- 浪费链接资源(挂起父事务后,无论当前支不支持事务,都需要重新获取链接,只是一个是在方法前在事务管理器里获取,而另一个则是方法执行到数据库操作时获取)。
- 获取锁超时,甚至导致死锁。
作者:曼特宁