沉浸式体验事务的一周

背景

业务逻辑

单据A上做了审核逻辑,具体包含以下大逻辑:

  1. 占用单据B的额度,如果校验不通过则不允许审核通过;
  2. 回写单据A的状态;

调用逻辑

  1. 用户手工在前台页面点击提交,日常业务会大批量进行,故提出需求要求部分成功部分失败;
  2. 后台定时任务多线程调用;

问题现象

生产环境大批量出现单据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

  1. 先提供SQL修改单据A的状态未已审核之后再手动取消审核,保证业务正常处理;
  2. 排查代码未发现明显的bug,由于是近期发现的偶发性问题,再观察观察;

DAY2

  1. 再次复现之后分析出来是 AService.auditA(ids) 为了实现部分成功部分失败把异常吃掉导致事务没有回滚
  2. 与用户沟通为了保障审核的安全性,将实现部分成功部分失败更改为一条校验不通过都不成功;
  3. 将catch里边的将异常throw出来; 测试通过后打补丁到生产上;

DAY3

    • 业务上发现大多是定时任务调用的单据出现的报错问题;
    • 怀疑到多线程提交的地方是按照单条去调用,且多线程内部将异常捕捉导致事务未回滚;
    • 修改AService.auditA(ids)方法单独开启事务,修改注解@Transactional(propagation=Propagation.REQUIRES_NEW,isolation=Isolation.READ_COMMITTED),做到多线程调用的时候每条是独立事务;

DAY4

  1. 将AService.auditA(ids)方法的占用单据B的额度和修改单据A的审核状态中间手动写了个异常(int i = 1/0;)热部署到服务上,执行业务操作复现了此问题,问题定位到事务上,于是 将BService.occupyB(ids)方法上加上注解 @Transactional; 重复业务操作后问题仍存在;
  2. 重点排查BService.occupyB(ids)的事务控制,发现存在方法内调的saveB(ids)是本方法的private修饰的,不参与事务控制,故修改BService.occupyB(ids)中调用this.saveB(ids)方法提到上层方法中;重复业务操作后问题仍存在;
  3. 后一直在排查事务方面的问题,包括框架是否有整体的事务控制等等。最终在绝望的时候发现是 异常抛的不对:
    throw new Exception("审核失败:"+e.getMessage());

总结

事务配置:

springBoot事务支持全注解和传统XML两种配置模式,一般项目上会统一配置;

排查思路

  1. 检查类或方法是否有 @Transactional 注解

    • 类级别:该类下所有 public 方法默认开启事务。 方法级别:仅对当前方法生效。
  2. 同一个类内直接调用非 public 方法,确保调用方式正确(避免内部调用绕过代理)

    • 原因: Spring 使用的是基于 AOP 的动态代理,默认只有通过外部调用才会触发事务控制。 类内部调用会绕过代理对象,导致事务失效。
    • 解决方案: 将 methodB() 提取到另一个 Service 中。 或者使用 AopContext.currentProxy() 获取代理对象调用。
  3. 检查异常是否被吞掉或捕获但未抛出:

    • 是否 catch 异常后没有重新 throw?
    • 默认情况下,Spring 只对 unchecked exception(RuntimeException 和 Error) 回滚。
    • 如果你抛出的是 checked exception(如 IOException),需要显式配置@Transactional(rollbackFor = Exception.class):
  4. 检查事务传播行为(propagation)

    propagation 描述
    REQUIRED 如果有事务则加入,没有则新建(默认)
    REQUIRES_NEW 总是新建事务,挂起已有事务
    SUPPORTS 支持事务,无事务则以非事务方式执行
  5. 是否使用了不支持事务的操作

    • 比如方法底层调用了RPC接口或者API接口,无法回滚;
  6. 检查事务配置是否启用

    • 在 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)

相关推荐
郭尘帅6663 分钟前
Spring依赖注入的四种方式(面)
java·后端·spring
潘小磊5 分钟前
高频面试之10 Spark Core & SQL
sql·面试·spark
风象南12 分钟前
SpringBoot防重放攻击的5种实现方案
java·spring boot·后端
[email protected]13 分钟前
Asp.Net Core SignalR导入数据
前端·后端·asp.net·.netcore
callJJ1 小时前
从 0 开始理解 Spring 的核心思想 —— IoC 和 DI(1)
java·开发语言·spring boot·后端·spring·restful·ioc di
编程乐学(Arfan开发工程师)6 小时前
56、原生组件注入-原生注解与Spring方式注入
java·前端·后端·spring·tensorflow·bug·lua
Elcker8 小时前
Springboot+idea热更新
spring boot·后端·intellij-idea
GISer_Jing9 小时前
JWT授权token前端存储策略
前端·javascript·面试
拉不动的猪9 小时前
es6常见数组、对象中的整合与拆解
前端·javascript·面试
蒟蒻小袁9 小时前
力扣面试150题--单词接龙
算法·leetcode·面试