CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
软件设计与架构入门/课时/架构模式
RawBlame
课时目录12 节

软件设计与架构入门

  1. 01整洁代码
  2. 02设计原则
  3. 03面向对象
  4. 04编程范式
  5. 05设计模式
  6. 06架构原则
  7. 07架构风格
  8. 08架构模式
  9. 09领域驱动设计
  10. 10企业模式
  11. 11综合架构练习
  12. 12路线图对照复习

架构模式

事件驱动、CQRS 与 Pub/Sub。

30 分钟

架构模式:Saga、Outbox 与 Strangler Fig

这一课你将能做到

  1. 用 Saga 模式实现"提交-判题-更新练习记录-发通知"的分布式事务
  2. 用 Outbox 模式保证事件消息"发了就存,存了必达"
  3. 用 Strangler Fig 模式把老单体系统逐步拆成微服务

为什么要学

架构风格告诉你"怎么搭骨架",架构模式告诉你"遇到具体问题怎么解决"。判题平台里,一次提交要动数据库、调判题引擎、写练习记录、发 WebSocket,这些步骤跨了多个服务,怎么保证一致性?Outbox 解决"消息发了但数据库没存"的问题,Strangler Fig 解决"老系统不敢拆"的问题。

先建立图像

Saga                    Outbox                   Strangler Fig
──────                  ──────                   ─────────────
1. 提交入库              业务操作 + Outbox 事件     老单体
2. 发 JudgeEvent  →    同事务写入               ↓ 新功能走新服务
3. 判题完成   ← 消费    Outbox 轮询投递 MQ       老功能逐步迁移
4. 更新记录   ← 消费    ↓                       ↓
5. 通知用户   ← 消费    事件最终一致             新服务完全接管

跟着做

步骤 1:Saga 编排——判题分布式事务

Saga 把大事务拆成小事务,每步失败都有补偿:

public class SubmissionSaga {
    private final SubmissionRepository submissionRepo;
    private final JudgeClient judgeClient;
    private final PracticeRecordService practiceService;
    private final NotificationService notifyService;

    public void execute(SubmitCommand cmd) {
        // 步骤 1:保存提交
        Submission submission = submissionRepo.save(Submission.create(cmd));
        try {
            // 步骤 2:发起判题
            JudgeResult result = judgeClient.judge(submission);
            submission.markAs(result);

            // 步骤 3:更新练习记录
            practiceService.record(cmd.userId(), cmd.lessonId(), result);

            // 步骤 4:通知用户
            notifyService.notifyJudged(cmd.userId(), result);
        } catch (Exception e) {
            // 补偿:标记提交为失败,不发通知
            submission.markAs(SubmissionStatus.FAILED);
            submissionRepo.save(submission);
            throw new SubmissionSagaException("判题流程失败", e);
        }
    }
}

每个步骤是本地事务,失败时执行补偿操作(回滚已完成的步骤)。

步骤 2:Outbox 模式——事件可靠投递

判题完成事件要发 MQ,但"先存数据库"和"后发 MQ"之间可能宕机。Outbox 把事件和业务数据放在同一个本地事务里:

@Entity
public class OutboxEvent {
    @Id private Long id;
    private String aggregateType;   // "Submission"
    private Long aggregateId;       // submissionId
    private String eventType;       // "JudgeFinished"
    private String payload;         // JSON 事件体
    private LocalDateTime createdAt;
    private Boolean published;      // 是否已投递 MQ
}

@Service
public class JudgeService {
    private final SubmissionRepository submissionRepo;
    private final OutboxEventRepository outboxRepo;

    @Transactional
    public void finishJudge(SubmissionId id, JudgeResult result) {
        Submission s = submissionRepo.findById(id).orElseThrow();
        s.markAs(result);
        submissionRepo.save(s);

        // 同事务写入 Outbox,保证原子性
        OutboxEvent event = new OutboxEvent("Submission", id.value(), "JudgeFinished", toJson(result));
        outboxRepo.save(event);
    }
}

@Component
public class OutboxPublisher {
    @Scheduled(fixedDelay = 1000)
    public void publishPendingEvents() {
        List<OutboxEvent> pending = outboxRepo.findByPublishedFalse();
        for (OutboxEvent e : pending) {
            mqTemplate.send(e.getEventType(), e.getPayload());
            e.markPublished();
            outboxRepo.save(e);
        }
    }
}

数据库提交成功 = Outbox 事件一定存在,轮询任务保证事件最终发到 MQ。

步骤 3:Strangler Fig——逐步替换老系统

判题平台老版本是单体,新版本要拆成微服务。Strangler Fig(绞杀者无花果)模式让新老共存,逐步替换:

// 老单体 Controller(保留)
@RestController
@RequestMapping("/api/v1/submissions")
public class LegacySubmissionController {
    // 老逻辑,逐步废弃
}

// 新服务 Controller
@RestController
@RequestMapping("/api/v2/submissions")
public class SubmissionController {
    private final SubmitUseCase submitUseCase;
    @PostMapping
    public ResponseEntity<SubmissionId> submit(@RequestBody SubmitRequest req) {
        return ResponseEntity.ok(submitUseCase.handle(req.toCommand()));
    }
}

// API 网关路由策略
@Component
public class SubmissionRouter {
    public String route(HttpServletRequest request) {
        String version = request.getHeader("X-API-Version");
        if ("v2".equals(version)) return "http://new-service/api/v2/submissions";
        return "http://legacy-monolith/api/v1/submissions";   // 默认走老系统
    }
}

新功能走新服务,老功能逐步迁移,直到老系统完全下线。

讲清楚要点

  1. Saga:跨服务的本地事务链 + 补偿机制,适合长流程(提交→判题→通知)。
  2. Outbox:事件和业务数据同事务写入,轮询投递,保证消息不丢。
  3. Strangler Fig:新老系统共存,通过路由逐步替换,降低重构风险。
  4. Saga 的补偿不是回滚:补偿是"业务意义上的撤销",不是数据库回滚。比如"已通知"的补偿是"发撤销通知",不是删除通知记录。
  5. Outbox 的轮询可以优化:用 CDC(Change Data Capture)如 Debezium 监听 binlog,代替定时轮询。

你可能会懵的地方

  • Saga 和本地事务什么区别? 本地事务是 ACID,Saga 是最终一致性。Saga 每一步是本地事务,但整体没有全局锁,靠补偿保证最终一致。
  • Outbox 表会不会越来越大? 会。需要定期清理已投递的事件,或者把历史事件归档到冷库。表结构要加索引(published + created_at)。
  • Strangler Fig 会不会导致双写问题? 会。迁移期间要保证新老系统数据一致,通常用 CDC 或双写 + 对账。判题平台里,提交记录先双写,稳定后切读,最后下线老系统。
  • Saga 的补偿失败怎么办? 补偿也要重试,重试失败要告警人工介入。补偿逻辑要幂等,防止重复执行。
  • 初学者什么时候该用这些模式? 单体项目不需要 Saga;微服务跨库才需要。Outbox 在"发消息"场景通用。Strangler Fig 只在"拆老系统"时用。

错法对照

错法问题正解
跨服务直接调 RPC 当本地事务用网络分区导致不一致Saga:本地事务 + 补偿
先发 MQ 再存数据库MQ 成功但 DB 失败,事件丢失Outbox:同事务写入,轮询投递
一次性重写老系统风险高,周期长Strangler Fig:逐步替换,先共存
Saga 补偿直接删数据破坏审计和追溯补偿是业务撤销,不是物理删除
Outbox 用定时任务但无索引扫表慢,拖垮数据库加 (published, created_at) 复合索引

卡点急救

  1. Saga 步骤太多,补偿复杂:把 Saga 步骤画成状态机图,每个状态对应一个补偿动作;用框架(如 Temporal、Camunda)管理状态流转。
  2. Outbox 轮询延迟高:改用 CDC 工具(Debezium + Kafka Connect)监听 binlog,实时投递。
  3. Strangler Fig 路由规则复杂:用 API 网关(如 Spring Cloud Gateway)统一路由,按 header、路径、权重逐步切流。

面试常问

  • Saga 和 2PC 什么区别? 答:2PC 是强一致,但协调者单点、阻塞资源、性能差;Saga 是最终一致,每步本地事务,失败补偿。判题平台用 Saga,因为判题耗时长,不能锁住数据库等资源。
  • Outbox 模式怎么保证消息不丢? 答:事件和业务数据同事务写入 Outbox 表,数据库提交成功 = 事件一定存在;后台任务轮询未投递事件,发到 MQ 后标记已投递。即使服务宕机,重启后继续投递。
  • Strangler Fig 怎么保证数据一致性? 答:迁移期双写新老系统,用 CDC 或定时对账保证一致;切读时先小流量验证,再全量切换;下线前确保老系统无写入。

自测三问

  1. 判题平台"提交→判题→更新记录→通知"四步,用 Saga 怎么设计补偿?
  2. Outbox 模式为什么不用"先发 MQ 再存数据库"?
  3. Strangler Fig 迁移期间,如果新老系统数据不一致,怎么发现和处理?

小结一句话

Saga 让跨服务流程最终一致,Outbox 让事件消息可靠投递,Strangler Fig 让老系统安全退休。

延伸

  • 《微服务架构设计模式》Chris Richardson,第 4 章(Saga)、第 3 章(Outbox)
  • Strangler Fig - Martin Fowler
  • Debezium 官方文档
  • 下一课:领域驱动设计——实体、值对象、聚合与限界上下文。
加载本节练习…
← 上一课架构风格下一课 →领域驱动设计