沉浸式体验事务的一周

背景

业务逻辑

单据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)

相关推荐
PAK向日葵27 分钟前
【算法导论】PDD 0817笔试题题解
算法·面试
uzong2 小时前
技术故障复盘模版
后端
GetcharZp2 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程3 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研3 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack5 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9655 小时前
pip install 已经不再安全
后端