还在手搓分布式事务?我把 Saga + Outbox 模板化后,新服务接入从 5 天压到 1 天

作者: jiangbodev 发布时间: 2026-05-07 技术栈: .NET 10 + PostgreSQL + OpenTelemetry + k3s 阅读时长: 10 分钟

很多团队做 Saga 会陷入一个循环:

第一个服务能跑,第二个服务重写一遍,第三个服务再踩一遍老坑。

最后你会看到这些熟悉场景:

  • 每个服务 Outbox 写法都不一样;
  • 有的消费端做了幂等,有的没做;
  • 补偿策略全靠个人习惯;
  • CI 只跑 happy path,线上再"随机开奖"。

我在项目里做了一个转折:

把 Saga + Outbox 从"方案"升级成"模板 + CI 守门"

结果很直接:

  • 新服务接入时间从 3-5 天降到 0.5-1 天;
  • 一致性事故从"月内多次"降到"月均 0-1 次";
  • 排障路径从"日志大海捞针"变成"状态机 + Trace 定位"。

下面只讲可落地做法。


1. 模板化到底要解决什么问题

我给模板定了 5 个硬目标:

  1. 新服务 1 天内接入 Saga + Outbox;
  2. 业务同学只改 Handler,不重复造基础设施;
  3. 默认透传 saga_id + trace_id
  4. 消费端幂等默认开启;
  5. CI 自动拦截高风险一致性改动。

一句话:把一致性能力变成团队默认配置,而不是少数人经验。


2. 模板拆成 4 个组件

我把能力拆成 4 个模块(共享项目或私有 NuGet 都可以):

  1. Saga.Core:状态机、步骤编排、补偿入口
  2. Outbox.Core:同事务落库 + relay 发布
  3. Messaging.Idempotency:消费端去重
  4. Observability.Conventions:统一日志和 OTel 标签

核心接口(示意)

csharp 复制代码
public interface ISagaOrchestrator<TCommand>
{
    Task<SagaResult> ExecuteAsync(TCommand command, CancellationToken ct = default);
}

public interface ISagaStep<TContext>
{
    string Name { get; }
    Task ExecuteAsync(TContext context, CancellationToken ct);
    Task CompensateAsync(TContext context, CancellationToken ct);
}
csharp 复制代码
public interface IOutboxWriter
{
    Task AddAsync<TEvent>(TEvent @event, IDictionary<string, string>? headers, CancellationToken ct);
}

public interface IMessageIdempotencyStore
{
    Task<bool> ExistsAsync(string messageId, CancellationToken ct);
    Task MarkProcessedAsync(string messageId, CancellationToken ct);
}

3. 业务侧最小接入代码

模板接入后,业务代码基本只剩"执行/补偿"本身:

csharp 复制代码
public class CreatePlanSagaOrchestrator : ISagaOrchestrator<CreatePlanCommand>
{
    private readonly IReadOnlyList<ISagaStep<CreatePlanContext>> _steps;

    public CreatePlanSagaOrchestrator(IReadOnlyList<ISagaStep<CreatePlanContext>> steps)
        => _steps = steps;

    public async Task<SagaResult> ExecuteAsync(CreatePlanCommand cmd, CancellationToken ct = default)
    {
        var ctx = CreatePlanContext.From(cmd);
        var executed = new Stack<ISagaStep<CreatePlanContext>>();

        try
        {
            foreach (var step in _steps)
            {
                await step.ExecuteAsync(ctx, ct);
                executed.Push(step);
            }
            return SagaResult.Success(ctx.SagaId);
        }
        catch (Exception ex)
        {
            while (executed.Count > 0)
            {
                var step = executed.Pop();
                await step.CompensateAsync(ctx, ct);
            }
            return SagaResult.Failed(ctx.SagaId, ex.Message);
        }
    }
}

这段代码不是炫技,而是为了统一"失败就按逆序补偿"的团队行为。


4. CI 四层守门:把事故挡在 PR 阶段

我把一致性检查分成 4 层:

第 1 层:静态规则

  • 消费 Handler 必须调用幂等检查;
  • Saga Step 必须有 ExecuteAsyncCompensateAsync
  • 不允许绕过 Outbox 直接发布消息。

第 2 层:单元测试

  • 第二步失败时,第一步补偿必须触发;
  • 同一 message_id 重复消费只执行业务一次;
  • Saga 超时能进入补偿态。

第 3 层:集成测试(Testcontainers)

  • 落库后服务崩溃,重启后 Outbox 仍可发布;
  • 重复投递不产生重复业务数据;
  • 补偿事件链路完整。

第 4 层:轻量回归压测

关注 4 个指标:

  • Outbox 积压量
  • 平均重试次数
  • Saga 超时率
  • 死信队列增速

5. GitHub Actions 示例

yaml 复制代码
name: saga-consistency-check

on:
  pull_request:
    branches: [ main ]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 10.0.x

      - run: dotnet restore
      - run: dotnet build --configuration Release --no-restore
      - run: dotnet test tests/UnitTests --configuration Release --no-build
      - run: dotnet test tests/IntegrationTests --configuration Release --no-build
      - run: dotnet test tests/ConsistencyGuards --configuration Release --no-build

我单独保留 ConsistencyGuards 测试项目,失败原因会比混在普通业务测试里更清晰。


6. 模板化最容易踩的 4 个坑

  1. 模板过度抽象:接入反而变慢。

    策略:模板只管骨架,业务流程顺序可配置。

  2. 只有代码模板,没有约定模板。

    策略:补齐消息命名、重试策略、死信处理 SOP。

  3. CI 只测 happy path。

    策略:必须加入失败注入,验证补偿链路。

  4. 告警阈值粗暴。

    策略:使用滑窗阈值,避免短抖动触发告警风暴。


7. 结果对比

指标 模板化前 模板化后
新服务接入 Saga 时间 3-5 天 0.5-1 天
一致性相关缺陷 每月多次 月均 0-1 次
排障平均耗时 30 分钟+ 10 分钟内
团队实现风格 离散 统一

真正的收益不是"少写了几百行代码",而是分布式事务从"个人手艺"变成"团队工业化能力"。


结语

如果你已经做完 Saga 第一版,不要再继续手搓。

下一步就三件事:

  1. 抽模板
  2. 建约定
  3. 接 CI

做完这三步,你的一致性治理才算真正进入可规模化阶段。


持续更新 .NET 云原生实战:

相关推荐
BING_Algorithm1 小时前
深入理解JVM垃圾回收
jvm·后端·面试
RainCity2 小时前
Java Swing 自定义组件库分享(六)
java·笔记·后端
techdashen2 小时前
深入 Rust enum 的内存世界
开发语言·后端·rust
龙码精神2 小时前
TimescaleDB 物联网设备属性历史数据表设计及常用SQL文档
后端
小小小小宇2 小时前
Go 后端锁机制详解
后端
挖坑的张师傅2 小时前
你的仓库 Agent Ready 了吗?
后端
客场消音器3 小时前
如何使用codex进行UI重构,让AI开发的前端页面不再千篇一律
前端·后端·微信小程序
Full Stack Developme3 小时前
spring-beans 解析
java·后端·spring
苏三说技术3 小时前
为什么大厂都不推荐在MySQL中使用NULL值?
后端