Seata做分布式事务时,报错后事务不回滚的问题

文章描述了一位开发者在使用Seata进行分布式事务管理时遇到的问题,即服务A调用服务B,服务B的事务在服务A异常时无法回滚。问题根源在于自定义的WebMvcConfig类错误地继承了WebMvcConfigurationSupport,导致Seata的拦截器未正确注册。修复这个问题后,两个服务的事务都能正常回滚。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、Seata做分布式事务时,报错后事务不回滚的问题?
具体问题举例:有两个服务A和服务B,服务A通过openfiegn调用服务B的接口,服务A和服务B都能正常注册到seat服务。接口正常添加@GlobalTransactional注解。方法报错是单个服务内事务均能回滚。但当服务A的serviceA调用服务B的serviceB时,serviceB执行成功并返回结果到serviceA中,然后serviceA报错,些时serviceA能回滚,serviceB不能回滚。

serviceA:

    @Override
    @GlobalTransactional
    public void createTeacherAndStudent() {
        System.out.println("事务id---------------------->" + RootContext.getXID());
        logger.info("========= 开始调用TEACHER");
        Teacher teacher = new Teacher();
        teacher.setName("卫国老师");
        teacher.setAge(58);
        teacher.setEmail("weiguo@teacher.com");
        mapper.add(teacher);
        mapper.delete(11L);

        // 抛出异常
        logger.info("========= 开始调用Studen");
        logger.info("========= Student将会出现异常");
        StudentVo studentVo = new StudentVo();
        studentVo.setName("分布式学生");
        studentVo.setAge(10);
        studentVo.setEmail("1@qq.com");
        studentVo.setCreateTime(new Date());

        JsonNode add = studentService.addV2("分布式",10,"1@11.com");
        logger.info("======== 接收到学生服微服务返回内容:{}", add.toString());
        int code = add.get("code").asInt();
        String msg = add.get("msg").asText();
        // 可以抛出异常  也可以不抛出异常
        // 本系统调用其他微服务  其他微服务抛出异常  也会触发分布式事务
        if (code != 200) {
            throw new BusinessException(code, msg);
        }
    }

运行后事务ID打印:

事务id---------------------->10.100.6.183:8091:6755797768365363267

serviceB:

   @Override
   //作为被调用方,加不加这个注解,全局事务都会生效。
   @GlobalTransactional
    public Long create(StudentVo studentVo) {
        System.out.println("事务id---------------------->" + RootContext.getXID());
        // 将VO对象转换为POJO对象
        BaseConverter<StudentVo, Student> baseConverter = new BaseConverter<>();
        Student student = baseConverter.convert(studentVo, Student.class);
        mapper.insert(student);
        return student.getId();
    }

事务id打印结果:

事务id---------------------->10.100.6.183:8091:6755797768365363269

可以看出两次事务id不一致,这就说明两次数据库操作并不是在一个全局事务中,所以造成了问题。我们的问题变为为什么事务id不一致?

一:seat全局事务id是在放在heard中在服务间传递的,然而通过断点,确实在服务B的请求request.header中有事务id。但为什么拿不到?

二: 查看RootContext.getXID()方法源码,发现是从线程变量中取的值,如果没有取到则会新开户一个全局事务。那么问题变成了设置本次请求初始化时设置线程变量出了问题。

三:通过断点分析,正常情况下,服务器接收到请求后,会通过一系列的拦截器列表,其中就有SeataHandlerInterceptor。然而我服务A和服务B启动时初始化的拦截器中根本就没有Seata这个拦截器。那一定是seata配置出了问题,我来来回回对比配置N次就是没有发现配置有什么问题。

四:继续分析Springboot源码,最终发现,正常情况下初始化拦截器会调用DelegatingWebMvcConfiguration 这个类的addInterceptors方法:

    protected void addInterceptors(InterceptorRegistry registry) {
        this.configurers.addInterceptors(registry);
    }

而我的服务启动却执行的:WebMvcConfigurationSupport类的方法,而这个方法是空方法。而这个类是DelegatingWebMvcConfiguration 的父类。

    protected void addInterceptors(InterceptorRegistry registry) {
    }

五:最终我发现我的config包里面有几个config类,其中一个是WebMvcConfig.java类。这个类只是用来配置swagger的,而这个类正好是继承至:WebMvcConfigurationSupport。我怀疑就是这里搞的鬼,果不其然,我把WebMvcConfigurationSupport 替换为DelegatingWebMvcConfiguration 后,终于看到两个两个服务都事务回滚了。


@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport{

    @Resource
    ConstantProperties constantProperties;

    /**
     * 静态资源配置
     * @param registry
     * @author
     * @createTime 2020/07/14
     * @description
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        String apiDocStatus = "true";
        if(apiDocStatus.equalsIgnoreCase(constantProperties.API_DOC)){
            // doc文档接口
            registry.addResourceHandler("/doc/**").addResourceLocations("classpath:/doc/");
            // swagger静态资源配置
            registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
            registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
            super.addResourceHandlers(registry);
        }
    }
}

另一种是实现WebMvcConfigurer接口

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Resource
    ConstantProperties constantProperties;
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        String apiDocStatus = "true";
        if(apiDocStatus.equalsIgnoreCase(constantProperties.API_DOC)){
            // doc文档接口
            registry.addResourceHandler("/doc/**").addResourceLocations("classpath:/doc/");
            // swagger静态资源配置
            registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
            registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
        }
    }
}

<think>好的,用户的问题是关于分布式事务中删除操作数据存在导致的报错,需要解决方案。首先,我需要理解这个问题的具体场景。在分布式系统中,多个服务可能同操作同的数据库,当执行删除操作,如果目标数据存在,可能会抛出异常,影响整个事务的一致性。用户希望找到处理这种情况的方法,避免报错并确保事务正常进行。 接下来,我应该回顾已有的知识。根据提供的引用内容,特别是引用[3]和引用[4],提到了TCC方案和可靠消息最终一致性方案。这可能对解决问题有帮助。比如,TCC的Try阶段可以预先检查数据是否存在,Confirm或Cancel阶段处理相应操作。另外,可靠消息最终一致性方案中,本地事务检查和消息确认机制也可能适用。 然后,我需要考虑常见的分布式事务解决方案,比如TCC、可靠消息、最大努力通知等,并结合用户的具体问题进行分析。例如,在TCC模式中,可以在Try阶段锁定资源或检查数据是否存在,避免在Confirm阶段删除存在的记录。另外,在删除操作前加入存在性检查,结合事务型消息,确保操作幂等性,可能也是有效的方法。 同,要注意到用户提到的删除数据存在的情况可能引发异常,因此需要异常处理机制,比如在代码中捕获异常并忽略特定错误,或者在业务逻辑中设计容错机制。此外,幂等性设计也很重要,确保重复删除操作会影响系统状态。 还需要参考用户提供的引用内容。例如,引用[3]提到TCC适用于严格资金场景,而可靠消息适用于一般场景,这可能意味着根据业务敏感度选择同方案。引用[4]中的checkLocalTransaction方法可以用于查询本地事务状态,可能在可靠消息方案中用于确认数据是否存在后再执行删除。 最后,综合这些思路,整理出具体的解决方案步骤,包括异常捕获、预检查、TCC模式、可靠消息结合检查、状态标记和异步补偿等。同,考虑如何结合现有分布式事务框架如Seata或RocketMQ的事务消息来实现这些方案,并确保回答结构清晰,符合用户的要求。</think>在分布式事务中处理删除操作数据存在导致的报错,可通过以下方案实现事务的健壮性: ### 解决方案 1. **异常捕获与静默处理** - 在删除操作代码段中捕获 `DataNotFoundException` 异常 - 记录警告日志但**中断事务流程** ```java try { dataService.deleteById(id); } catch (DataNotFoundException e) { log.warn("删除目标数据存在, ID: {}", id); } ``` - 适用于对数据一致性要求高的场景[^3] 2. **预检查机制** - 在Try阶段增加存在性检查 ```sql SELECT status FROM table WHERE id = ? FOR UPDATE ``` - 若检查结果为存在,跳过后续删除操作 - 需配合数据库行锁保证检查的实性[^2] 3. **TCC模式实现** - **Try阶段**:冻结资源并标记删除状态 ```java int affected = update table set status = 'DELETING' where id = ? and status = 'ACTIVE' ``` - **Confirm阶段**:执行物理删除 ```sql DELETE FROM table WHERE id = ? AND status = 'DELETING' ``` - **Cancel阶段**:恢复数据状态 ```sql UPDATE table SET status = 'ACTIVE' WHERE id = ? AND status = 'DELETING' ``` 该方案通过状态机转换确保删除操作的幂等性 4. **可靠消息+二次校验** - 发送事务消息前执行本地检查 ```java @Transactional public void prepareDelete() { if(dataRepository.exists(id)) { // 插入待删除记录到日志表 deleteLogRepository.insertPendingDelete(id); } } ``` - 消息消费通过 `checkLocalTransaction` 二次确认[^4] 5. **软删除+异步清理** - 先执行逻辑删除 ```sql UPDATE table SET deleted = 1 WHERE id = ? ``` - 通过定任务执行物理删除 - 可避免分布式事务中的瞬状态冲突 ### 方案选型建议 | 方案 | 适用场景 | 一致性级别 | 实现复杂度 | |------|----------|------------|------------| | 异常捕获 | 非核心业务 | 最终一致 | ★★☆☆☆ | | TCC模式 | 资金交易等强一致性场景 | 强一致 | ★★★★☆ | | 可靠消息 | 订单/库存等一般业务 | 最终一致 | ★★★☆☆ | | 软删除 | 有审计要求的场景 | 最终一致 | ★★☆☆☆ | ### 实施注意事项 1. 必须配合唯一事务ID实现幂等性 2. 删除操作日志需保留足够间(建议≥72小) 3. 监控需覆盖删除失败率、事务回滚率等指标 4. 对核心业务建议采用TCC+异步对账的混合模式
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wolf犭良

谢谢您的阅读与鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值