还在手搓分布式事务?我把 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 云原生实战:

相关推荐
掘金者阿豪1 分钟前
PDO连金仓数据库(下篇):预处理语句、大对象和批量操作
后端
RealPluto4 分钟前
Rancher证书轮换过期导致不能访问UI问题处理
后端
Asize4 分钟前
Bun + TypeScript 实战:从接口约束到 RESTful 路由设计
后端·typescript·代码规范
鱼人20 分钟前
Go 操作 MySQL:常用写法与最佳实践
后端
挖坑的张师傅23 分钟前
方便 Mac 本机运行 e2b 的沙箱方案 e2b-local
人工智能·后端
开心猴爷25 分钟前
Flutter 如何自动上传 可以 IPA 把构建和上传分开处理
后端·ios
二月龙26 分钟前
defer 执行顺序与底层原理,90% 的人都理解不全
后端
长大198826 分钟前
新手常犯的 Go 语法错误,一次性帮你避坑
后端
小强198827 分钟前
深入理解 Go 协程 Goroutine:并发编程的核心精髓
后端
chengliu050837 分钟前
后端学习地图
后端