@Transational事务无法回滚的原因
事务无法回滚的原因
1.添加到非public方法上
2.使用try catch处理异常
3.调用类内部的Transactional注解(嵌套使用)
4.事务的传播机制使用不当
5.数据库不支持事务
6.多线程异步调用,Transational注解无法管理异步线程
try catch使用不当
我们的Spring的Transational注解要遇到抛出的异常的时候我们才会回滚,如果我们的try catch里面我们的catch{}中我们捕获到了异常,但是我们却没有throw把异常抛出,就会出现事务无法回滚
@Transactional
public void Kira() {
try{
数据库操作
}
catch(Exception e)
{
//我们抓了异常但是没有抛出异常
}
}
没指定捕获异常的类型(rollbackFor)
如果我们的Transational注解里面的rollbackFor我们没有指定成所有异常Exception,那么我们就默认只会遇到RuntimeException的时候我们才回滚
如果我们try catch中我们throw的是Exception而不是RuntimeException,那我们Spring事务还是无法回滚的
所以我们一般都要在Transational注解里面通过rollbackfor指定要捕获的异常类型为所有异常Exception.Class
@Transactional(rollbackFor = Exception.class)
public void Kira() {
数据库操作
}
多数据源情况下没有指定我们的数据源
我们要通过我们的transationalManager参数来指定我们的数据源
多线程异步调用
在多线程环境下,由于每个线程都有自己的上下文,@Transational 注解在不同线程中不会共享事务上下文,因此可能导致事务失效
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void saveUserInTransaction(User user) {
new Thread(() -> {
userRepository.save(user);
// 这里的操作不会在同一个事务中
}).start();
}
}
同一个类下嵌套调用Transational注解修饰的方法
Spring 的 @Transactional
注解是基于 AOP(面向切面编程)实现的。AOP 通过代理模式为目标对象创建代理对象,在代理对象中织入事务管理的逻辑,例如在方法调用前后开启、提交或回滚事务。
当外部调用带有 @Transactional
注解的方法时,实际上是调用的代理对象的方法,代理对象会处理事务相关的操作
当在同一个类的一个方法中调用另一个带有 @Transactional
注解的方法时,这种调用属于内部调用
在内部调用时,并没有经过代理对象,而是直接调用了目标对象的方法,绕过了 AOP 代理的事务增强逻辑,从而导致事务失效
Spring的大事务问题
大事务的典型问题
- 数据库连接占用
长时间未提交的事务会占用数据库连接,可能导致连接池耗尽,影响其他请求。 - 锁竞争与阻塞
事务未及时提交时,持有的行锁或表锁会阻塞其他操作,引发死锁或高延迟。 - 回滚成本高
事务内操作过多时,回滚需要撤销大量操作,可能消耗大量时间和资源。 - 内存压力
事务中大量未提交的数据可能占用数据库内存(如Undo Log),影响整体性能
常见的引发场景
混合耗时操作
我们在操作多个数据库的逻辑中间还有其他的大量操作,例如:远程调用(RPC)、文件处理、复杂计算等非数据库操作,增加了我们的事务的执行时间
@Transactional
public void Kira() {
数据库插入or修改表1
RPC调用
计算
计算
计算
数据库插入or修改表2
}
例如我们其实本来就是两个数据库操作,但是因为我们两个操作之间有大量耗时操作,导致事务执行时间较长,从而导致我们的事务占用行锁的时间较长
未合理划分事务的边界
将多个业务放到一个事务中,导致事务控制的范围过大,从而导致出错时回滚时间较长or事务占用行锁时间过长
@Transactional
public void Kira() {
数据库插入or修改表1
数据库插入or修改表2
数据库插入or修改表3
数据库插入or修改表4
数据库插入or修改表5
数据库插入or修改表6
数据库插入or修改表7
数据库插入or修改表8
数据库插入or修改表9
数据库插入or修改表10
数据库插入or修改表11
数据库插入or修改表12
数据库插入or修改表13
数据库插入or修改表14
数据库插入or修改表15
}
我们一旦出错,就会数据回滚,回滚时间会非常长
大事务涉及表过多导致的死锁
@Transactional
public void Kira() {
数据库插入or修改表1
数据库插入or修改表2
数据库插入or修改表3
数据库插入or修改表4
数据库插入or修改表5
数据库插入or修改表6
数据库插入or修改表7
数据库插入or修改表8
数据库插入or修改表9
数据库插入or修改表10
数据库插入or修改表11
数据库插入or修改表12
数据库插入or修改表13
数据库插入or修改表14
数据库插入or修改表15
}
@Transactional
public void Kira1() {
数据库插入or修改表2
数据库插入or修改表1
}
@Transactional
public void Kira2() {
数据库插入or修改表4
数据库插入or修改表3
}
我们这三个方法的使用会出现死锁问题
因为我们的大事务涉及的表过多所以导致死锁的概率也会更大
解决方案
编程式事务手动控制边界
使用TransationalTemplate手动控制事务,精确控制我们要事务管理的范围
减少事务管理范围避免大事务
问题所在:如果我们有多个事务控制的代码块,我们在一个代码块中的事务出错了,只能在当前代码块中的事务实现回滚而不能让其他事务控制的代码块回滚,不能实现统一回滚,不符合原子性
@Autowired
private TransactionTemplate transactionTemplate;
public void Kira(List<Data> dataList) {
dataList.forEach(data -> {
transactionTemplate.execute(status -> {
dao.insert(data);
return null;
});
});
}
调整事务超时时间配置
通过设置超时时间让我们的超时事务强制回滚,大事务占用锁过多时间
把我们Spring的大事务能够接受的时间写在Transational注解的timeout参数上,如果超过了我们可以接受的时间我们就回滚
@Transactional(timeout = 10) // 外层事务超时时间
public void Kira() {
数据库操作
}
调整事物的传播机制配置
这个事务的隔离级别是开启新事物
那我们就会在原来的事务上开启一个新的子事务,然后子事务完成就进行提交
而不是用一个Transational,管理多个数据库操作
问题所在:无法保证原子性,例如我们主线程事务出错了,我们此时子事务提交了,所以我们无法进行回滚,这样子就保证不了事务的原子性
解决方案:人为兜底,实现我们的最终一致性
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void Kira() {
数据库插入or修改
数据库插入or修改
数据库插入or修改
其他类的数据库插入or修改方法
其他类的数据库插入or修改方法
其他类的数据库插入or修改方法
其他类的数据库插入or修改方法
数据库插入or修改
数据库插入or修改
数据库插入or修改
}
我们默认的隔离级别是,其他类的事务会加入到本类的事务进行管理
也就是蓝色类操作的事务并不受当前线程事务的管理
相当于我们的本事务有多个子事务,这个子事务只管理自己的东西,结束后直接提交,不受当前主线程事务的管理,这样子我们的其他类事务的行锁就不用等当前事务的所有操作都结束才释放,而是自己执行完就把锁释放,从而减少事务管理范围避免大事务
强一致性与最终一致性
强一致性
我们要保证全部插入和全部回滚,那么我们就只能用一个Transational事务管理整个事务了
如果我们要并发优化就是要用我们的多线程事务,把我们不相关的事务放到异步线程执行,然后通过CoutDownLatch控制并发和Spring事务的手动提交来进行我们的业务逻辑优化了
最终一致性
通过编程式事务or设置Spring事务的传播机制来减少我们的事务控制的颗粒度,细化事务控制的范围
但是因为我们分为了多个事务,所以我们不能实现统一全部回滚
所以我们要做人为的业务补偿和兜底