架构模式
事件驱动、CQRS 与 Pub/Sub。
30 分钟
架构模式:Saga、Outbox 与 Strangler Fig
这一课你将能做到
- 用 Saga 模式实现"提交-判题-更新练习记录-发通知"的分布式事务
- 用 Outbox 模式保证事件消息"发了就存,存了必达"
- 用 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);
}
}
}