记一次排查 Seata 分布式事务无法回滚的完整过程

背景
在分布式系统中,我们使用 Seata 管理跨服务的事务。某次业务操作中,调用方和被调用方均报告成功,但在异常回滚时,全局事务标记为回滚成功(Rollback global transaction successfully),但未发现分支事务的回滚记录。进一步排查发现被调用方的 XID(通过 RootContext.getXID() 获取)为空,导致事务上下文未正确传递。尽管通过 FeignRequestInterceptor 手动传递了 XID,事务仍无法回滚。最终发现 undo_log 表未写入,Seata TC 日志显示 resourceIds=‘null’。通过手动代理数据源后,问题解决。

问题排查过程

  1. 现象描述
    全局事务回滚成功,但分支事务无记录
    Seata TC 日志显示全局事务回滚成功(xid = 19),但未找到分支事务的回滚日志。
    被调用方 XID 为空
    通过 RootContext.getXID() 获取被调用方的 XID 时返回 null,事务上下文未正确传递。
    手动传递 XID 无效
    使用 FeignRequestInterceptor 手动传递 XID 后,问题仍未解决。
    undo_log 表无数据
    检查数据库发现 undo_log 表未写入,Seata 无法记录事务操作。
  2. 关键排查步骤
    (1) 检查 Seata TC 日志
    发现 resourceIds=‘null’,表明 RM 未正确注册资源。
    log
    RM register success, message: RegisterRMRequest{resourceIds=‘null’, …}
    (2) 验证数据源代理
    问题定位:Seata 依赖 DataSourceProxy 代理数据源以拦截 SQL 并写入 undo_log。
    检查代码:发现未显式配置 DataSourceProxy,导致数据源未被 Seata 代理。
java
// 错误示例:未代理数据源
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
    return new DruidDataSource();
}

(3) 手动代理数据源
解决方案:通过 DataSourceProxy 包装原始数据源,确保 Seata 能拦截 SQL。

java
@Configuration
public class DataSourceProxyConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }
 
    @Primary
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource); // 关键:代理数据源
    }
}

(4) 重启验证
在这里插入图片描述

结果:
Seata TC 日志显示 resourceIds 正常(如 db_inventory)。
undo_log 表成功写入事务记录。
分支事务回滚成功。
根本原因
数据源未代理:未通过 DataSourceProxy 包装数据源,导致 Seata 无法拦截 SQL 并生成 undo_log。
resourceIds 未配置:RM 注册时 resourceIds 为 null,Seata 无法关联资源与事务。
解决方案总结
显式代理数据源
使用 DataSourceProxy 包装原始数据源,确保 Seata 能拦截 SQL 并管理事务。

java
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
    return new DataSourceProxy(dataSource);
}

检查 resourceIds 配置
在 RM 的配置文件中明确指定 resourceIds(如数据库表名):
properties
seata.resource.ids=db_inventory
验证事务上下文传递
确保 FeignRequestInterceptor 正确传递 XID:

java
public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        String xid = RootContext.getXID();
        if (xid != null) {
            template.header("TX-XID", xid);
        }
    }
}

监控 undo_log 表
确保 undo_log 表被正确创建且 Seata 有权限写入。
经验教训
数据源代理是关键:Seata 依赖 DataSourceProxy 管理事务,未代理数据源会导致事务失效。
日志分析优先:通过 Seata TC 日志快速定位 resourceIds 或 XID 问题。
测试覆盖场景:在事务成功、回滚、异常等场景下验证 undo_log 和日志记录。
通过以上步骤,我们成功解决了 Seata 分布式事务无法回滚的问题,确保了分布式系统的数据一致性。