美食街的中央厨房里,张大厨立了一条规矩:每个订单从进门到出餐,全程记流水账。顾客问“我的寿司到哪一步了”,他不看白板,而是翻账本——从头读到尾,最新一行写到哪,状态就是哪。这本账从不涂改、从不撕页,出了任何纠纷都能翻旧账说清楚。后来订单变复杂了,一份套餐要寿司摊、炸鸡摊、饮品摊三个摊位协作。张大厨又发明了一张“协作单”:先寿司、后炸鸡、再饮品,每一步打勾才走下一步;万一炸鸡摊今天没货,就按相反顺序通知前面已完成的步骤“取消、退款”,把流程安全地送回去。 这套“流水账 + 协作单”的组合,正是微服务世界里大名鼎鼎的事件溯源(Event Sourcing)与 Saga 模式。事件溯源告诉系统:不要只保存“现在是什么”,要如实记录“发生了什么”;Saga 告诉系统:跨服务业务流不能一步到位,要一步步推进、失败了就逆序补偿。两者叠加,既能完整回溯每一次业务事实,又能保证跨服务流程最终达成一致——这是现代分布式系统最优雅的账房方案之一。

岗位关联

对 AI 从业者而言,事件溯源提供了高质量的全量行为数据底座,Saga 保证了数据最终一致,是构建可靠 AI 业务系统(推荐、风控、客服)不可绕过的工程基石。

目录
  1. 可视化演示
  2. 事件溯源基础——流水账本从不涂改
  3. Saga模式核心——一张逆序撤销的协作单
  4. 组合设计思路——账本与协作单的二重奏
  5. 可靠投递——传菜员不能丢单
  6. 性能优化——账本太厚怎么办
  7. 订单实战——一份套餐的完整旅程
  8. 工程挑战——老账本与新分店
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示事件溯源与Saga组合:事件流跨服务传递、不可变事件日志与状态重建、Saga分步执行与失败补偿,通过交互理解跨服务业务流。

本节配有交互动效(进入视口后自动加载,约 3KB)。

事件流:跨服务传递 事件在服务间传递 Saga失败触发补偿 Event 事件驱动解耦服务

事件溯源基础——流水账本从不涂改

美食街某家中央厨房里,总管张大厨有本规矩:每个订单从进来到出餐,全程记流水账,只往上加新行,绝不涂改旧行。谁问“现在做到哪了”,张厨就从头读到尾,最新读到哪,状态就是哪。这套“记流水账”的思想,就是事件溯源(Event Sourcing)。

传统架构里,数据库表像一块白板,一个订单我们只保存“当前状态”这一行,状态变了就 update 覆盖。而事件溯源换了一种思维方式:状态是结果,事件才是事实。系统不保存“现在是什么”,只保存“发生了什么”,所有“发生了什么”按时间顺序追加在一个只增不改的日志里。任何实体的最新状态都可以通过对事件日志进行重放(Replay)计算出来——从最早的“订单已创建”到最新的“订单已完成”,逐个应用,状态自然水落石出。

事件溯源与传统的 CRUD 直观对比:CRUD 是“读-改-写覆盖”,事件溯源是“只追加-回放”。读模型与写模型分离后,甚至衍生出 CQRS(命令查询职责分离)架构。对数据分析而言,事件日志就是一份天然的、不可篡改的行为事实表——这正是 AI 系统最爱的特征原料。账本从不撕页,给了系统一份“后悔药”和“照妖镜”:任何时刻的状态都能查,任何错误都能追溯。

Staten=apply(Staten−1, Eventn)\text{State}_n = \text{apply}(\text{State}_{n-1},\ \text{Event}_n)
这个公式说的是:最新状态 = 把旧状态按最新事件往前推一格。账本翻到最新一行时,你自然知道“现在这张订单进行到哪一步”。
  1. 历史状态 \(\text{State}_{n-1}\)这是处理完前 n-1 条事件后得到的中间状态,相当于账本翻到倒数第二笔时的进度。
    Staten−1\text{State}_{n-1}
  2. 应用函数 \(\text{apply}\)把一条事件“翻译”成一次状态变更。张厨看到一行新流水,就把进度往前推一格。
    apply(State, Event)\text{apply}(\text{State},\ \text{Event})
  3. 最新事件 \(\text{Event}_n\)刚刚追加到日志里的一条新事实,比如“炸鸡已出餐”。
    Eventn\text{Event}_n

Saga模式核心——一张逆序撤销的协作单

一份套餐订单需要寿司摊、炸鸡摊、饮品摊三方协作,张厨写下一张协作单:先寿司备好、再炸鸡炸好、最后由饮品摊出品。每一步做完打勾再走下一步。万一炸鸡摊今天没货,不能就这么算了——得按相反顺序通知寿司摊取消、饮品摊停止,把已经推进的步骤一步步“送回去”,顾客的钱原路退回。这张写清了“正着走、倒着撤”的协作单,就是 Saga 模式。

传统分布式事务想用数据库锁保证所有服务同步提交或回滚,但跨服务的网络不可靠,长时间锁着资源也不现实。Saga 反其道而行:把一个分布式事务拆成一组本地事务,一个接一个执行。每个本地事务 \(T_i\) 都有对应的补偿事务 \(C_i\),一旦某一步失败,就逆序执行前面所有已完成步骤的补偿。

Saga 保证的是最终一致性:某一瞬间各服务状态可能不一致,但经过一段时间的推进与补偿,所有服务最终会达成一致。它有两种常见组织方式:编排式(Orchestration)由一个中央协调者像指挥家一样统一发令;协作式(Choreography)则像足球队员互相传球,没有中间人,由各服务订阅事件自行响应。流程复杂、需要集中管控选编排式;服务自治、链路简单选协作式。协作单的每一行背后,都写好了“如果这一步白做了,该怎么收场”——这就是补偿事务。

\text{Saga}: (T_1,\ T_2,\ \dots,\ T_n); \qquadT_k \text{ 失败时 } \Rightarrow C_k,\ C_{k-1},\ \dots,\ C_1
意思是:正向一串事务逐步执行,第 k 步如果失败,就从第 k 步开始反着做补偿,一直退到第一步。
  1. 事务序列 \(T_1,\ T_2,\ \dots,\ T_n\)一个跨服务流程被拆成的若干个本地事务,像协作单上的一行行工序。
    T1, T2, …, TnT_1,\ T_2,\ \dots,\ T_n
  2. 失败点 \(T_k\)第 k 个本地事务执行失败,流水线在此中断。
    TkT_k
  3. 逆序补偿 \(C_k,\ C_{k-1},\ \dots,\ C_1\)从失败点开始,逐一向回执行补偿事务,抵消已经产生的业务影响。
    Ck, Ck−1, …, C1C_k,\ C_{k-1},\ \dots,\ C_1

组合设计思路——账本与协作单的二重奏

如果只有流水账,张厨能知道“发生了什么”,却没法协调多家摊档;如果只有协作单,出了问题只知道“该取消”,却不知道“哪些步骤真的做过了”。两个工具配合,才能既知情又可控:流水账负责记录事实,协作单负责调度行动。这正是事件溯源与 Saga 的完美互补。

在工程实现上,事件溯源 + Saga 的组合把事件当作服务间协作的通用语言。每个服务完成一步本地工作后,发出一条事实事件(比如 PaymentCompleted),写入事件日志;接下来该做什么,由 Saga 逻辑决定——要么推进下一步,要么启动补偿。跨服务关联需要关联 ID(Correlation ID)贯穿全程:一条订单从 OrderCreated 到 OrderCancelled,所有事件都带着同一个订单号,像一条丝线把散落各处的珠子串起来。事件消息的标准结构如下:

每个服务内部是一个“事件 → 状态转换”的状态机:待支付收到 PaymentCompleted 就变已支付。Saga 相当于整个业务流状态机“总编导”,让所有服务的本地状态机在同一张总谱上演奏;消息总线(如 Kafka)则负责把事件广播给所有订阅者。流水账记录事实,协作单负责调度,事件就是这两本账之间流通的票据。

Event=⟨eventId, aggId, type, payload, version⟩\text{Event} = \langle \text{eventId},\ \text{aggId},\ \text{type},\ \text{payload},\ \text{version} \rangle
一条事件就是一个信封:独一无二的编号、属于哪个订单、是什么类型的事、携带什么业务数据、当前是第几个版本。
  1. 事件编号 \(\text{eventId}\)全局唯一的流水号,用于幂等去重和日志定位。
    eventId\text{eventId}
  2. 聚合 ID \(\text{aggId}\)这条事件属于哪个业务实体,比如订单号 O1001。
    aggId\text{aggId}
  3. 事件类型 \(\text{type}\)表示发生了什么,比如 OrderCreated、PaymentCompleted。
    type\text{type}
  4. 载荷与版本 \(\text{payload},\ \text{version}\)payload 携带业务细节,version 标记事件结构的版本号,为将来的兼容演进留后路。
    payload, version\text{payload},\ \text{version}

可靠投递——传菜员不能丢单

张厨的传菜员在前厅后厨之间来回跑。就算菜已经送到,也可能因为人多口杂再跑回来确认一遍:“炸鸡真的到了吗?”摊档师傅不能因为被问两次就把菜再炒两遍——得学会说“这份我已经出过了”。在分布式系统里,消息队列通常承诺至少一次(at-least-once)投递:不丢,但可能重复。所以消费方必须实现幂等:同一个事件处理多次的结果,与处理一次完全相同。

另一个关键问题是可靠投递。如果先写业务、再发事件,服务在两步之间崩溃就会出现“账记了但广播没发”。工程实践常用 Outbox(发件箱)模式:把“业务变更”和“待发送事件”放在同一个本地事务里写库,再由后台扫描器把发件箱里的记录可靠地送到消息总线。实在送不动的,放进死信队列(DLQ)保留原始数据,供脚本或人工处理后重新投递。同时要注意顺序保证:同一聚合的事件必须顺序消费,不同聚合之间才可以并行。传菜员可以多问几遍,但厨房永远不能多做——幂等是分布式系统的“礼貌”。

handle(e)={skip,if seen(eventId)apply(e),otherwise\text{handle}(e) = \begin{cases} \text{skip}, & \text{if } \text{seen}(\text{eventId}) \\ \text{apply}(e), & \text{otherwise} \end{cases}
翻译成大白话:处理事件前先查“这个编号我见过吗”,见过就跳过,没见过才真正执行业务逻辑。
  1. 查重函数 \(\text{seen}(\text{eventId})\)在去重表或 Redis 里查这个事件编号是否已经处理过。
    seen(eventId)\text{seen}(\text{eventId})
  2. 跳过分支 \(\text{skip}\)已处理过就直接返回,不产生副作用,比如不重复扣款。
    skip\text{skip}
  3. 应用分支 \(\text{apply}(e)\)第一次看到的事件才真正执行状态变更。
    apply(e)\text{apply}(e)

性能优化——账本太厚怎么办

美食街干了五年,账本从一本变成一书柜。张厨每次回答“现在状态如何”,都得从头读到尾,顾客等得不耐烦。后来他想了个办法:每隔一段时间(比如 100 笔)夹一页“状态小结”——“截至此刻,摊位 A 已接单 5000 单”。下次再查就不用从第一页翻起,只看最近的小结和后面的流水。这个“小结页”就是事件溯源中的快照(Snapshot)。

除了快照,还要做事件分区与并行消费:事件按聚合 ID 哈希到不同分区(Partition),不同分区的消费互不干扰,可以并行处理;同一聚合的事件必须留在同一分区、保持先后顺序。快照频率是调优关键——太勤浪费存储,太疏重放变慢。对已完成的历史事件,还可以做冷热分层,把老数据迁到廉价存储。小结页加流水账的组合,让账本无论多厚,查起来都像昨天刚记的一样快。

Staten≈apply(Snapshots, Events[s+1..n])\text{State}_n \approx \text{apply}(\text{Snapshot}_s,\ \text{Events}_{[s+1..n]})
快照把重建路径从“从头重放全部事件”缩短为“从最近快照重放增量事件”。
  1. 快照 \(\text{Snapshot}_s\)在时刻 s 保存的一份聚合状态,像账本里的“小结页”。
    Snapshots\text{Snapshot}_s
  2. 增量事件 \(\text{Events}_{[s+1..n]}\)快照之后新发生的所有事件。
    Events[s+1..n]\text{Events}_{[s+1..n]}
  3. 合并应用 \(\text{apply}\)从快照出发,只重放增量,得到最新状态。
    apply(Snapshots, Events[s+1..n])\text{apply}(\text{Snapshot}_s,\ \text{Events}_{[s+1..n]})

订单实战——一份套餐的完整旅程

一个真实套餐订单,就是订单服务、支付服务、库存服务三个角色的一场接力赛。张厨把每一棒怎么跑、掉棒了怎么撤都写清楚。完整流程如下:订单服务先创建订单,写入 OrderCreated 事件,状态变为“待支付”;支付服务扣款,写入 PaymentCompleted,状态变为“已支付”;库存服务扣减库存,写入 StockDeducted,状态变为“备货中”;订单服务最后确认,写入 OrderCompleted,状态变为“已完成”。

如果用公式表达,成功链是一条笔直的四连拍;如果库存不足,则是“断拍 + 回退”:

注意补偿不是数据库回滚,而是新的业务操作:退款要真实调用支付渠道,释放库存要真实把商品加回仓库。这些补偿操作本身也被记录为事件,整个“波折史”在事件日志里一清二楚。如果补偿也失败,状态停在“待人工处理”,配合死信队列人工介入。张厨的账本上,失败的订单不是一个空洞的标记,而是完整记录了从点单到取消的每一步——这就是可审计、可追溯的价值。

SUCCESS: T1→T2→T3→T4;FAIL: T1→T2→F3⇒C2→C1\text{SUCCESS: } T_1 \to T_2 \to T_3 \to T_4; \qquad\text{FAIL: } T_1 \to T_2 \to F_3 \Rightarrow C_2 \to C_1
成功链四个事务依次走完;失败链在第三棒掉链子,随后逆序执行第二棒退款、第一棒取消。
  1. 成功链 \(T_1 \to T_2 \to T_3 \to T_4\)创建订单 → 支付扣款 → 扣减库存 → 完成订单,四步全部提交。
    T1→T2→T3→T4T_1 \to T_2 \to T_3 \to T_4
  2. 失败点 \(F_3\)库存步骤抛错,正向推进戛然而止。
    F3F_3
  3. 逆序补偿 \(C_2 \to C_1\)先退款(PaymentRefunded),再取消订单(OrderCancelled)。
    C2→C1C_2 \to C_1

工程挑战——老账本与新分店

五年后张厨在隔壁街开了分店,可老账本是繁体字,新伙计看不懂,总得找老师傅翻译;账本又不能重新抄,抄的过程中可能漏单。事件溯源落地最大的坎正是事件版本管理:线上跑着的事件结构是 v1,半年后字段变了,旧事件仍躺在日志里。工程实践用 Upcasting:读取旧版本事件时,在内存中翻译成最新结构再应用,不改历史文件,只在读的时候转换。

架构演进上,新服务要用历史数据,可以靠事件流回放自动建库,不用停机迁移。观测上,用关联 ID 把所有服务的日志串成一条时间线,监控面板实时展示每个 Saga 卡在哪一步。测试上,把生产事件流采样后在测试环境回放,新版本是否正确一目了然——这就是事件回放测试。账本越老越值钱,关键在于“翻译”与“回放”的能力要跟上。

Event′=upcast(Event);Staten=apply(Staten−1, Event′)\text{Event}' = \text{upcast}(\text{Event}); \qquad\text{State}_n = \text{apply}(\text{State}_{n-1},\ \text{Event}')
老事件不需要改写,只要在读出来之后套一层“翻译器”,新代码就能直接消费。
  1. 翻译函数 \(\text{upcast}\)把旧版本事件升级为新版本结构的转换函数。
    upcast(Event)\text{upcast}(\text{Event})
  2. 新版本事件 \(\text{Event}'\)转换后的、能被当前代码理解的事件。
    Event′\text{Event}'
  3. 应用转换后事件状态重建时统一使用升级后的事件,老账本也变得“可读”。
    apply(Staten−1, Event′)\text{apply}(\text{State}_{n-1},\ \text{Event}')

AI 应用场景

智能风控识别“补偿风暴”

阶段:实时特征工程与异常检测

解决的问题:传统订单表只保存最新状态,AI 风控模型看不到“下单 → 支付 → 立刻退款”的波折过程。事件溯源完整记录每一步事实,Saga 的补偿事件(PaymentRefunded、OrderCancelled)构成天然的行为特征。

具体案例:某电商平台把订单事件流接入 Kafka,实时特征平台按用户维度统计“过去 5 分钟 Saga 补偿率”。当某账号的补偿率突然飙升,说明可能在下单后疯狂退款,风控模型立即触发拦截,将恶意账号加入黑名单。

客服智能助手还原订单时间线

阶段:RAG 检索与状态推理

解决的问题:客服查询订单历史时,传统状态表只能回答“当前是什么”,客户问“中途发生了什么”就哑口无言。事件日志可以按关联 ID 串出一条完整时间线。

具体案例:智能客服 Agent 检索订单事件链,用大模型生成自然语言摘要:“12:01 下单 → 12:03 扣款成功 → 12:05 库存不足 → 12:06 退款 → 12:07 订单取消。”用户不需要问第二遍,服务满意度显著提升。

供应链预警的跨仓事件回放

阶段:时序预测与异常检测

解决的问题:跨仓调拨、库存移动、物流揽收由多个系统协作,传统数据库难还原链路细节。事件溯源记录全量事实,AI 预测模型可以直接消费事件序列。

具体案例:某零售企业把各仓库的库存扣减事件、调拨事件接入预测管道,时序模型发现某 SKU 的“库存扣减失败”事件连续增多,提前预警补货窗口,避免大面积缺货。

Python 演示

python101 行
class Event:    def __init__(self, event_id, agg_id, event_type, payload):        self.event_id = event_id        self.agg_id = agg_id        self.event_type = event_type        self.payload = payload     def __repr__(self):        return "<{} {}>".format(self.event_type, self.agg_id)  class OrderAggregate:    def __init__(self, order_id):        self.order_id = order_id        self.status = "NONE"        self.paid = False        self.version = 0     def apply(self, event):        t = event.event_type        if t == "OrderCreated":            self.status = "CREATED"        elif t == "PaymentCompleted":            self.paid = True        elif t == "StockDeducted":            self.status = "PREPARING"        elif t == "OrderCompleted":            self.status = "COMPLETED"        elif t == "PaymentRefunded":            self.paid = False        elif t == "OrderCancelled":            self.status = "CANCELLED"        self.version += 1     def rebuild(self, events):        for evt in events:            self.apply(evt)        return self  class StockClient:    def __init__(self, initial):        self.available = initial     def deduct(self, sku):        if self.available <= 0:            raise RuntimeError("库存不足: " + sku)        self.available -= 1  class PayClient:    def refund(self, payload):        print("退款成功: ", payload)  class SagaCoordinator:    def __init__(self, stock_client, pay_client):        self.stock_client = stock_client        self.pay_client = pay_client        self.event_log = []     def emit(self, order_id, event_type, payload):        evt = Event(len(self.event_log) + 1, order_id, event_type, payload)        self.event_log.append(evt)        return evt     def run(self, order_id, amount):        self.emit(order_id, "OrderCreated", {"amount": amount})        self.emit(order_id, "PaymentCompleted", {"amount": amount})        try:            self.stock_client.deduct("SKU-001")            self.emit(order_id, "StockDeducted", {})            self.emit(order_id, "OrderCompleted", {})            return "SUCCESS"        except RuntimeError:            self.emit(order_id, "PaymentRefunded", {"amount": amount})            self.pay_client.refund({"amount": amount})            self.emit(order_id, "OrderCancelled", {"reason": "stock_not_enough"})            return "ROLLBACK"     def states(self):        result = []        for order_id in ["O1001", "O1002", "O1003"]:            events = [e for e in self.event_log if e.agg_id == order_id]            state = OrderAggregate(order_id).rebuild(events)            result.append((order_id, state.status, state.paid))        return result  if __name__ == "__main__":    stock = StockClient(initial=2)    pay = PayClient()    saga = SagaCoordinator(stock, pay)     saga.run("O1001", 99)    saga.run("O1002", 99)    saga.run("O1003", 99)     for order_id, status, paid in saga.states():        print(order_id, status, "paid={}".format(paid))    print("事件总数:", len(saga.event_log))
目的:用一个迷你订单 Saga 演示“事件日志追加 + 状态重放 + 库存失败后逆序补偿”三条核心链路。
  1. class Event定义事件的结构,含 event_id、agg_id、event_type、payload,对应文中的“流水账的一行”。
  2. def apply(self, event)聚合根的状态变更逻辑,每吃进一条事件就推进一步状态。
  3. def rebuild(self, events)从事件日志重建聚合状态,对应“从头翻账本”。
  4. def emit(self, order_id, event_type, payload)向事件日志追加一条新事件,编号自动递增。
  5. try: self.stock_client.deduct(...)库存步骤是 Saga 中最容易“掉链子”的一环。
  6. except RuntimeError:库存不足时进入补偿分支。
  7. self.emit(order_id, "PaymentRefunded", ...)补偿事务也被记入事件日志,保证完整可审计。
  8. saga.run("O1003", 99)第三个订单因库存耗尽触发 ROLLBACK。
  9. saga.states()用事件重放还原三个订单的最终状态。

常见误区

误区“我们把 Kafka 的 topic 消息存下来,不就是在做事件溯源吗?” 很多初学者认为用了消息队列就等于事件溯源。

正解Kafka 是传输管道,事件溯源是存储与重建模式。就像外卖平台的“骑手实时定位消息”和“订单完整流水账”不是一回事:消息送达消费后可以被清理,而事件日志是权威事实源,必须支持从零重建状态并长期可查。Kafka 默认的日志保留策略可能删除历史数据,不能替代事件存储。

为何常见:两者都叫“日志”,形式上都是追加写入一串数据,名字相近极易混淆。

误区“Saga 不就是分布式回滚吗?失败了我把前面的事务都 rollback 掉。” 不少工程师把 Saga 的补偿理解成数据库的 undo。

正解Saga 无法回滚已经提交的本地事务,只能执行补偿事务。补偿是新的业务操作(比如发起退款、加回库存),不是数据库层面的撤销。就像张厨不能把已经炸好的鸡“回滚”成生鸡,只能做业务处理把它卖掉或报废。

为何常见:因为数据库事务里“回滚”的概念太深入人心,Saga 的“补偿”名相似但语义完全不同。

误区“流程失败后,补偿总是能一次执行成功。” 画流程图时总把补偿画得理所当然。

正解补偿操作也会失败——下游宕机、网络抖动、接口超时。因此补偿必须有重试机制、状态持久化和人工介入兜底。Saga 状态机要显式标记“补偿中”“补偿失败”,配合死信队列处理。

为何常见:因为只画了理想流程,忽略了分布式系统的常态就是故障。

误区“反正只追加,就一直记着呗,重放慢就加机器。” 有些团队上线事件溯源后从不做快照,几年后重放越来越慢。

正解事件体积无限增长,重放耗时线性增加,工程上必须用快照截断重放路径、用分区并行消费。快照不是删事件,而是给重建状态提供一条捷径——就像张厨每隔 100 页夹一张“小结页”。

为何常见:“事件不可删除”的属性被误读为“不能做任何优化”,其实历史永远保留,但重建路径可以优化。

面试题

Q1. 事件溯源中,系统的最新状态通常是如何得到的?

单选题 · easy

  • A.回放事件日志得到← 正确答案
  • B.查询数据库当前行
  • C.由定时任务计算
  • D.从缓存直接读取

答案:A。事件溯源不保存“当前状态”,只保存事件序列;最新状态通过对事件日志逐个 apply 重放计算而来。B 是传统 CRUD 的做法,C 和 D 都不符合事件溯源的“状态由事件推导”原则。

Q2. 在 Saga 模式中,“补偿”(Compensation)指的是什么?

单选题 · easy

  • A.回滚已提交的事务
  • B.删除失败操作记录
  • C.执行反向业务操作← 正确答案
  • D.重启下游服务

答案:C。已提交的本地事务无法撤销,Saga 只能通过执行反向业务操作(如退款、取消)来抵消影响。A 混淆了数据库回滚与业务补偿;B、D 都不是补偿的语义。补偿是一次全新的业务事件。

Q3. 由一个中央协调者统一指挥各服务执行和补偿的 Saga 模式,被称作?

单选题 · mid

  • A.编排式(Orchestration)← 正确答案
  • B.编舞式(Choreography)
  • C.双阶段提交(2PC)
  • D.TCC 模式

答案:A。Orchestration 像交响乐的指挥,中央协调者发指令;Choreography 则像球员互相传球,没有指挥。2PC 是强一致分布式事务协议,TCC 是另一种补偿式事务框架,二者都不是 Saga 的组织模式。

Q4. 事件溯源存储的核心特性是?

单选题 · mid

  • A.允许更新覆盖旧值
  • B.定期清空历史记录
  • C.只追加且不修改删除← 正确答案
  • D.仅保存最新的一条

答案:C。事件是已经发生的“事实”,事实不能被修改或删除,只能追加新事件。“只追加”(append-only)是事件溯源与 CRUD 最本质的区别。A、B、D 都会破坏历史事实的完整性。

Q5. 系统在时刻 s 生成了快照,重建最新状态的正确路径是?

单选题 · mid

  • A.从最早事件逐条重放
  • B.从快照重放后续事件← 正确答案
  • C.直接读取快照数据
  • D.随机选取事件重放

答案:B。快照保存了时刻 s 的完整状态,重建时只需要从快照出发,重放 s 之后的新事件,即 apply(Snapshot_s, Events[s+1..n])。A 效率低,C 会丢失快照后的变化,D 会得到错误状态。

Q6. Saga 补偿操作本身也失败,工程上通常的处理方式是?

单选题 · extreme

  • A.放弃并标记脏数据
  • B.忽略错误继续流程
  • C.重试补偿直至人工介入← 正确答案
  • D.重新执行正向的步骤

答案:C。补偿失败不能直接放弃或忽略,否则数据永远不一致。正确做法是持久化补偿状态、持续重试,仍不行则告警并人工介入。D 重新执行正向步骤会加剧混乱,A、B 会留下脏数据。

Q7. 以下哪项是 Saga 模式无法提供的保证?

单选题 · mid

  • A.业务补偿的闭环
  • B.系统的最终一致性
  • C.强实时的一致性← 正确答案
  • D.全链路的事件审计

答案:C。Saga 是典型的最终一致性方案,过程中各服务可能短暂不一致,靠补偿逐步收敛,无法提供强实时一致性。A、B、D 都是 Saga 与事件溯源配合能够达成的能力。

Q8. 以下哪些是事件溯源的合理优化手段?

多选题 · mid

  • A.定期生成状态快照← 正确答案
  • B.按聚合 ID 进行分区← 正确答案
  • C.删除过期的历史事件
  • D.并行消费不同聚合事件← 正确答案

答案:ABD。快照缩短重放路径,分区与并行消费提升吞吐,三者都是成熟实践。C 删除历史事件违背“事件是事实、不可删除”的原则——历史可以冷存,但不能删。

Q9. 关于消息可靠投递与幂等消费,下列说法正确的有?

多选题 · hard

  • A.使用事件 ID 作为幂等键← 正确答案
  • B.消费前查询是否已处理← 正确答案
  • C.重复到达时再次执行业务
  • D.同一聚合事件乱序消费

答案:AB。幂等消费的核心是“先查重、再执行”,用事件 ID 做唯一键是标准做法。C 重复执行业务会引发重复扣款等问题;D 乱序消费会破坏事件溯源的状态重建逻辑,顺序必须保证。

Q10. 事件溯源架构适合哪些应用场景?

多选题 · easy

  • A.电商订单全链路审计← 正确答案
  • B.金融交易流水追溯← 正确答案
  • C.临时缓存键值存储
  • D.跨服务事件回放测试← 正确答案

答案:ABD。订单审计、金融流水这类“必须知道每一笔发生了什么”的场景是事件溯源的主场;回放测试利用历史事件验证新代码也非常自然。C 是纯临时数据存储,不需要完整事实历史,用普通缓存即可。