注解 - @Transactional 事务
# 属性
# isolation 隔离级别
Isolation.DEFAULT
DEFAULT
:默认值,表示使用底层数据库的默认隔离级别。大部分数据库为 READ_COMMITTED(MySQL 默认隔离级别为 REPEATABLE))
# propagation 事务传播行为
Propagation.REQUIRED
REQUIRED
:默认值,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。SUPPORTS
:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。MANDATORY
:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。REQUIRES_NEW
:创建一个新的事务,如果当前存在事务,则把当前事务挂起。NOT_SUPPORTED
:以非事务方式运行,如果当前存在事务,则把当前事务挂起。NEVER
:以非事务方式运行,如果当前存在事务,则抛出异常。NESTED
:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED
。
# readOnly 是否只读
true
时只读,不能更新、删除等写操作
# 触发回滚的方式
- 抛异常(注意
rollbackFor
的 Exception 级别) - 调用
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
方法
# 失效场景
参考:一口气说出 6 种,@Transactional 注解的失效场景 - 掘金 (opens new window)
# 1. 注解在非 public 修饰的方法上
笔记
在实践中发现导致事务失效的似乎并不是修饰符,而是 4. 同一个类中方法调用
导致的。
# 2. 注解属性 propagation 设置错误
出现概率低,默认的 propagation 值 REQUIRED
不会出现这个问题,如果要设定其他值时应当清楚明白其作用。
# 3. 注解属性 rollbackFor 设置错误
这是一个比较常见的失效场景,但如果有安装 Alibaba Java Coding Guidelines
插件会提示 方法【XXXXX】需要在Transactional注解指定rollbackFor或者在方法中显式的rollback。
。
# 4. 同一个类中方法调用⭐
这是一个很难发现的场景,在笔者尝试测试非 public 修饰符对事务的影响时发现了这个问题。
@Service
public class TestService{
public void publicTest() {
this.publicRollback();
}
@Transactional(rollbackFor = Exception.class)
public void publicRollback() {
this.save(......);
Assert.isTrue(false, "测试public");
}
public void privateTest() {
this.privateRollback();
}
@Transactional(rollbackFor = Exception.class)
private void privateRollback() {
this.save(......);
Assert.isTrue(false, "测试private");
}
public void protectedTest() {
this.protectedRollback();
}
@Transactional(rollbackFor = Exception.class)
protected void protectedRollback() {
this.save(......);
Assert.isTrue(false, "测试protected");
}
}
由于 Controller 无法直接访问 Service 的 private、protected 修饰的方法,因此使用了一个 public 方法间接调用,经测试发现即便是 publicTest
也出现了事务失效的问题。
提示
上述写法 IDEA 会在 this.xxxxRollback()
处警告: @Transactional self-invocation (in effect, a method within the target object calling another method of the target object) does not lead to an actual transaction at runtime
@Transactional 自调用 (实际上,目标对象中的方法调用目标对象的另一个方法) 在运行时不会产生实际的事务
解决办法:在 Service 的入口方法上添加 @Transactional
注解
# 5. 异常被捕获
代码中 try...catch 了异常,导致无法触发回滚。
解决办法:在 catch 中抛出一个 RuntimeException 异常
# 6. 数据库引擎不支持事务
略
# 常见问题
1. 控制台打印了检测但不回滚
遇到异常检测不回滚,原因:默认 RuntimeException 级别才回滚,如果是 Eexception 级别的异常需要手动添加
@Transactional(rollbackFor=Exception.class)
捕捉异常后事物不生效,原因:捕捉处理了异常导致框架无法感知异常,自然就无法回滚了。建议:若非实际业务要求,则在业务层统一抛出异常,然后在控制层统一处理
@Transactional(rollbackFor=Exception.class)
public void test() {
try {
//业务代码
} catch (Exception e) {
// TODO: handle exception
} //主动捕捉异常导致框架无法捕获,从而导致事物失效
}
2. 如何在多线程中使用事务回滚?
需求:多线程执行,任意一个线程抛出异常,所有线程事务回滚。
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean batchPutOnShelf(List<Integer> idList) {
idList.parallelStream().forEach(this::putOnShelf);
return Boolean.TRUE;
}
相比于直接使用线程池,使用 parallelStream 方法有以下优缺点:
优点:
- 代码简单,易于理解和使用;
- 无需手动管理线程池,避免因线程池设置不当导致的性能问题;
- 对于数据规模较小或处理逻辑较简单的情况,可以自动利用系统的多核资源,提高处理效率。
缺点:
- 对于数据规模较大或处理逻辑较复杂的情况,可能无法最优地利用系统资源,导致性能下降;
- 无法手动调整并行度,可能会导致过多的线程竞争和资源占用;
- 无法控制线程池的参数,如线程数、队列大小等,可能会影响系统的稳定性和可靠性。
3. 事务方法修饰符对事务回滚的影响
参考:Spring Framewor 文档 - 1.4.6. Using @Transactional (opens new window)- Method visibility and @Transactional
应该只对具有 public
可见性的方法应用 @Transactional
注解。如果注解在 protected
、 private
的或包可见的方法,虽然不会报错,但被注解的方法不会表现出配置的事务性设置。如果你需要注解非 public 的方法,请考虑下一段中关于基于类的代理的提示,或者考虑使用 AspectJ 编译时或加载时织入(后面描述)。
当在 @Configuration
类中使用 @EnableTransactionManagement
时,通过注册一个自定义的 transactionAttributeSource
Bean,也可以使基于类的代理的 protected
或包可见的方法成为事务性的,就像下面的例子一样。然而,请注意,基于接口的代理中的事务性方法必须始终是 public
的,并定义在被代理的接口中。
笔记
在测试 protected
、 private
修饰符对事务的影响时发现,
参考资料