背景:
业务逻辑
单据A上做了审核逻辑,具体包含以下大逻辑:
- 占用单据B的额度,如果校验不通过则不允许审核通过;
- 回写单据A的状态;
调用逻辑
- 用户手工在前台页面点击提交,日常业务会大批量进行,故提出需求要求部分成功部分失败;
- 后台定时任务多线程调用;
问题现象
生产环境大批量出现单据B的额度占用,但是单据A的状态还是未审核;
代码逻辑
java
public class AServiceImpl implements AService {
@Autowired
private BService bService;
@Transactional
@Override
public Map<String, Object> auditA(List<String> ids) throws Exception{
try{
//占用单据B的额度
bService.occupyB(ids);
//修改单据A的审核状态
updateA(ids);
}catch (Exception e) {
//map放入错误信息
return map;
});
}
public void syncTaskExecute(){
//取符合调度任务执行的数据
List<String> id = queryA();
//创建线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<String>> futures = new ArrayList<>();
for(String id : ids){
Future<String> future = executor.submit(() -> {
try{
this.auditA();
return id;
}catch (Exception e) {
log.error("审核异常+++ ", e);
//记录日志
} finally {
return mainCode;
}
});
futures.add(future);
}
list.forEach(future -> {
try {
future.get();
} catch (Exception e) {
log.error("多线程执行任务发生异常+++ ", e);
});
}
}
}
public class BServiceImpl implements BService {
public Map<String, Object> occupyB(List<String> ids) throws Exception{
this.checkB(ids);
this.saveB(ids);
}
private void checkB(List<String> ids) throws Exception{
}
private void saveB(List<String> ids) throws Exception{
this.checkB(ids);
this.saveB(ids);
}
}
处理步骤
DAY1
- 先提供SQL修改单据A的状态未已审核之后再手动取消审核,保证业务正常处理;
- 排查代码未发现明显的bug,由于是近期发现的偶发性问题,再观察观察;
DAY2
- 再次复现之后分析出来是 AService.auditA(ids) 为了实现部分成功部分失败把异常吃掉导致事务没有回滚
- 与用户沟通为了保障审核的安全性,将实现部分成功部分失败更改为一条校验不通过都不成功;
- 将catch里边的将异常throw出来; 测试通过后打补丁到生产上;
DAY3
-
- 业务上发现大多是定时任务调用的单据出现的报错问题;
-
- 怀疑到多线程提交的地方是按照单条去调用,且多线程内部将异常捕捉导致事务未回滚;
-
- 修改AService.auditA(ids)方法单独开启事务,修改注解@Transactional(propagation=Propagation.REQUIRES_NEW,isolation=Isolation.READ_COMMITTED),做到多线程调用的时候每条是独立事务;
DAY4
- 将AService.auditA(ids)方法的占用单据B的额度和修改单据A的审核状态中间手动写了个异常(int i = 1/0;)热部署到服务上,执行业务操作复现了此问题,问题定位到事务上,于是 将BService.occupyB(ids)方法上加上注解 @Transactional; 重复业务操作后问题仍存在;
- 重点排查BService.occupyB(ids)的事务控制,发现存在方法内调的saveB(ids)是本方法的private修饰的,不参与事务控制,故修改BService.occupyB(ids)中调用this.saveB(ids)方法提到上层方法中;重复业务操作后问题仍存在;
- 后一直在排查事务方面的问题,包括框架是否有整体的事务控制等等。最终在绝望的时候发现是 异常抛的不对:
throw new Exception("审核失败:"+e.getMessage());
总结
事务配置:
springBoot事务支持全注解和传统XML两种配置模式,一般项目上会统一配置;
排查思路
-
检查类或方法是否有 @Transactional 注解
- 类级别:该类下所有 public 方法默认开启事务。 方法级别:仅对当前方法生效。
-
同一个类内直接调用非 public 方法,确保调用方式正确(避免内部调用绕过代理)
- 原因: Spring 使用的是基于 AOP 的动态代理,默认只有通过外部调用才会触发事务控制。 类内部调用会绕过代理对象,导致事务失效。
- 解决方案: 将 methodB() 提取到另一个 Service 中。 或者使用 AopContext.currentProxy() 获取代理对象调用。
-
检查异常是否被吞掉或捕获但未抛出:
- 是否 catch 异常后没有重新 throw?
- 默认情况下,Spring 只对 unchecked exception(RuntimeException 和 Error) 回滚。
- 如果你抛出的是 checked exception(如 IOException),需要显式配置@Transactional(rollbackFor = Exception.class):
-
检查事务传播行为(propagation)
propagation 描述 REQUIRED 如果有事务则加入,没有则新建(默认) REQUIRES_NEW 总是新建事务,挂起已有事务 SUPPORTS 支持事务,无事务则以非事务方式执行 -
是否使用了不支持事务的操作
- 比如方法底层调用了RPC接口或者API接口,无法回滚;
-
检查事务配置是否启用
- 在 Spring Boot 中,事务是默认启用的。但在 XML 配置或老项目中需要手动开启;
注意事项:
-
try-catch 异常后未抛出,不会触发事务回滚;
-
非 public 方法使用 @Transactional 事务不会生效;
-
同一个类中调用带事务的方法,由于代理机制,事务可能不会生效;简单来说就是AService下的A方法(public)和B方法(非 public)都有@Transactional注解,但是A方法调用B方法时,事务不生效;
-
Spring 中的事务管理是基于 AOP 实现的,默认只对 unchecked exceptions(非受检异常) 回滚,因此throw new Exception不会触发事务回滚:
异常类型 是否默认回滚 示例 RuntimeException 及其子类 ✅ 是 RuntimeException 及其子类 Error 及其子类 ✅ 是 OutOfMemoryError, VirtualMachineError 等 其他异常(如 Exception) ❌ 否 IOException, SQLException 等 可以手动通过@Transactional 注解的 rollbackFor 让所有 Exception 都触发回滚: @Transactional(rollbackFor = Exception.class)