在市区有一家名叫“美味斋”的餐厅,生意好得让人羡慕,可老板却愁眉苦脸。每到饭点,服务员在大堂和厨房之间来回奔忙:顾客催着点菜,后厨催着上菜,所有人都挤在一本“总账本”前——记订单要写账本,查菜色要翻账本,账本被抢来抢去,队伍越排越长,出菜越来越慢。 老板想了个办法:把“记录订单”和“查询菜单”彻底分开。下单用一本“订单册”,只负责记;查菜改用另一本“菜单册”,只负责念。后厨每做好一道菜,就敲一下铃铛(事件),服务员听见铃声就去端菜,并顺手更新菜单册。两本册子各干各的,谁也不用等谁,饭点再也没了拥堵的账本。 这个故事,就是 CQRS(命令查询职责分离)模式的缩影。它把“写”与“读”彻底分开,再用“事件”把两边悄悄连起来,让系统既能扛住疯狂的写入,又能给用户飞快的查询。接下来的内容,我们将从这家餐厅出发,认识事件存储、投影重建、最终一致性,以及处理复杂业务的 Saga 模式。

岗位关联

理解 CQRS 与事件驱动,是设计高响应、可扩展分布式系统的核心能力之一,也是 AI 工程化架构师面对复杂业务时必备的思维工具。

目录
  1. 可视化演示
  2. CQRS 基础
  3. 事件驱动结合
  4. 事件存储
  5. 投影重建
  6. 最终一致性
  7. Saga 模式
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示CQRS读写分离架构与事件溯源流程,动态演示命令到事件存储到投影读模型的完整链路,交互切换CQRS、事件溯源与Saga模式。

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

CQRS:命令查询职责分离 靛蓝为命令侧(写操作验证与处理) 橙色为事件存储(持久化领域事件) 绿色为读模型(投影构建查询视图) Cmd -> Event -> Store -> Project -> ReadModel CQRS将写操作与读操作分离到不同模型,通过事件异步同步实现读写独立扩展

CQRS 基础

美味斋老板的痛点,你一定能共鸣:一本“总账本”既要记订单,又要被翻来翻去查菜色。写的人要排队,读的人也要排队,双方互相拖累。老板的解法很朴素——既然记和查是两件本质不同的事,为什么非要共用一本账本?他把账本一分为二:“订单册”只负责写下顾客要什么,“菜单册”只负责回答今天有什么。

这个“分家”的思路,就是 CQRS(Command Query Responsibility Segregation,命令查询职责分离)的核心。它告诉我们:写操作(命令)和读操作(查询)应该使用各自独立的模型,而不是挤在一个模型里将就。

在传统单体系统中,读写共用同一张数据库表,查询复杂了会影响写入性能,写入频繁了又会拖慢查询。CQRS 让写模型专注于业务规则与状态变更,让读模型专注于查询效率与展示形态。两个模型可以放在不同的数据库,比如写库用关系型数据库保证事务,读库用 Elasticsearch 或 Redis 加速检索。两者还能独立伸缩——饭点下单暴增时,只需扩写模型的机器;客人频繁翻菜单时,只需扩读模型的机器。当然,天下没有免费的午餐:读写分离增加了一套同步机制,因此它更适合读写负载差异大、查询模式复杂、需要弹性伸缩的系统。小饭店强行分两本账,反而浪费纸。

老板满意地看着两本册子:点单高峰时,记单的小二忙得满头汗,但查菜的服务员依然笑容满面——写和读,终于各忙各的了。

Rsystem=max⁡(Rcommand, Rquery)R_{\text{system}} = \max(R_{\text{command}},\, R_{\text{query}})
读写分离后,系统总响应时间约等于“命令响应”和“查询响应”中较慢的那个,而不是两者叠加。
  1. R_command表示写模型中处理一个命令(如下单)所需的响应时间。
    RcommandR_{\text{command}}
  2. R_query表示读模型中处理一次查询(如查菜色)所需的响应时间。
    RqueryR_{\text{query}}
  3. max因为读写并行执行、互不阻塞,系统整体响应时延由两者中的瓶颈决定。
    max⁡(a,b)\max(a, b)

事件驱动结合

美味斋的后厨有一个默契:厨师做好菜,不会端着盘子满大堂找顾客,而是“叮”地敲一下铃铛。铃铛一响,闲着的服务员自然会来端菜。这声铃铛,就是事件。它不指定“必须由谁来端”,只陈述一个事实——“有一道菜出锅了”。

在事件驱动架构中,“事件”是对已发生事实的描述:订单已创建、菜品已上桌、顾客已结账。它和“命令”有本质区别:命令表达了“我想要做什么”的意图,是尚未发生的请求;事件则表达“已经发生了什么”,是不可改变的事实。CQRS 的写端接收命令、执行业务逻辑,然后把结果发布为事件,供读端和其他系统消费。

这种异步解耦带来的好处立竿见影:生产者(后厨)不需要等消费者(服务员)响应,两者通过消息中间件(如 Kafka、RabbitMQ)连接。生产者往管道里丢一个事件就继续干自己的活,消费者按自己的节奏处理。即便某个消费者暂时故障,事件也会在管道中积压等待,不会丢失。事件流因此具备严格的先后顺序——先下单后上菜,这个顺序必须被保持,否则业务就乱套了。

铃铛声此起彼伏,却井然有序。厨师、服务员、洗碗工各听各的铃,谁也不会堵在谁门口——这就是事件驱动解耦后的节奏感。

e0≺e1≺⋯≺ene_0 \prec e_1 \prec \cdots \prec e_n
事件之间通过因果先后关系形成一条有序序列,就像铃声有先来后到,不能颠倒。
  1. 序号系数每个事件 e 都有一个位置下标,标识它们在业务中的发生顺序。
    eie_i
  2. 因果先后符号 \prec 表示“先于”,前一个事件在因果上早于后一个事件。
    ek≺ek+1e_k \prec e_{k+1}
  3. 完整链条从 e₀ 到 eₙ 的整条链,构成一个业务故事的全部事实。
    e0≺e1≺⋯≺ene_0 \prec e_1 \prec \cdots \prec e_n

事件存储

美味斋后厨角落挂着一本牛皮封面的“流水账”。每一道菜出锅,账房先生就添一行:“12:03 红烧肉出锅”“12:17 清蒸鱼出锅”。这本账只许往上写,不许拿橡皮擦改。谁要问“3 号桌到底上了几个菜”,翻账便知。老板立下规矩:账本不许涂改,这就是餐厅的“铁面流水账”。

事件存储(Event Store)就是系统的这本流水账。它与普通数据库最大的不同:只追加(append-only),不修改、不删除。每一行记录一个不可变的事件事实,并按发生顺序排列。这个特性让事件存储成为事件驱动系统的“真相之源”——它是所有业务事实的唯一权威记录。

基于事件存储,我们可以实现事件溯源(Event Sourcing):不再保存对象的最新状态,而是保存产生这个状态的全部事件。想要当前状态?把事件从头到尾重放一遍即可。这带来惊人的能力:可以回退到任意历史时刻、可以诊断“当初是怎么变成这样的”、可以随时生成新的视角。代价则是事件会无限增长,需要定期做快照来压缩(把历史事件折叠成一个状态快照,事件继续保留)。

账本越写越厚,但老板心里反而踏实了——哪怕客人投诉“菜上错了”,翻一翻流水账,谁的责任一目了然,谁也改不了历史。

St=S0+∑k=1tΔEkS_t = S_0 + \sum_{k=1}^{t} \Delta E_k
系统的当前状态,等于初始状态加上历史上所有事件带来的增量之和。
  1. 当前状态S_t 表示经过 t 个事件之后,某个聚合对象的当前状态。
    StS_t
  2. 初始状态S_0 是系统的起点,通常是空容器或初始配置。
    S0S_0
  3. 累加求和把从第 1 个到第 t 个事件分别带来的增量全部加起来。
    ∑k=1t\sum_{k=1}^{t}
  4. 事件增量ΔE_k 是第 k 个事件对状态造成的改变量。
    ΔEk\Delta E_k

投影重建

服务员手里的“菜单册”是怎么来的?不是后厨现问的,而是账房先生每天早晨根据流水账整理出来的:看见“红烧肉出锅”就记一道红烧肉,看见标记“红烧肉售罄”就把菜单上的红烧肉划掉。菜单册是对流水账的一种“整理视角”——它只关心“今天能点什么菜”,不关心做菜的过程细节。

这个“从事件流整理出查询视图”的过程,就是投影(Projection)。读模型(Read Model)是投影的产物:一张为查询量身定制的表,上面的每一行都来自对事件流的折叠计算。比如电商系统里,“用户订单列表”视图就是由“OrderCreated 订单已创建”“OrderShipped 订单已发货”等事件投影而来的。

投影重建(Rebuild Projection)是 CQRS 的一把利器:既然事件流永久保留,读模型就可以随时推倒重建。菜单册被咖啡弄脏了?没关系,照流水账重新抄一份,几分钟后焕然一新。代码升级后想加一个字段?同样重放事件,生成包含新字段的读模型。读模型还可以按需构建多种形态——同一条事件流,既能投影成“今日菜单”,也能投影成“食材消耗统计”,互不影响。

菜单册旧了、脏了、被改乱了,都不怕——账房先生闭着眼都能重抄一本。读模型永远是“可抛弃、可重建”的临时产物,这份底气来自那本永不磨灭的流水账。

∀e∈E:  R←apply⁡(R,e)\forall e \in \mathcal{E}:\; R \leftarrow \operatorname{apply}(R, e)
对事件流中的每一个事件,都执行一次 apply 折叠,最终得到读模型 R。
  1. 全部事件∀e ∈ E 表示遍历事件流中的每一个事件,一个都不漏。
    ∀e∈E\forall e \in \mathcal{E}
  2. 折叠操作对当前读模型 R 应用事件 e,产生新的读模型。
    apply⁡(R,e)\operatorname{apply}(R, e)
  3. 持续更新← 表示反复执行、滚动更新,直到事件流处理完毕。
    R←⋯R \leftarrow \cdots

最终一致性

顾客催菜时,老板常说一句话:“菜正在做呢,马上到!”下单和上菜之间,天然存在一段等待时间。如果顾客下单后立刻要求后厨显示“菜已上桌”,那显然不现实。CQRS 系统同样如此:命令写入事件存储后,投影需要一点点时间才会更新读模型。这个“时间差”,就是最终一致性(Eventual Consistency)的延迟窗口。

一致性并不是只有“强”和“弱”两级。强一致性要求任何时刻读到的都是最新值;弱一致性允许读到旧值;最终一致性介于两者之间——它允许短时间内读到旧数据,但保证在没有新写入的情况下,所有读取最终会收敛到最新状态。CQRS 绝大多数场景选择最终一致性,换来的是更低的写入延迟和更高的系统吞吐。

要让最终一致性真正可靠,必须做好两件事:第一,幂等设计(Idempotency),消费者可能收到重复事件(比如消息重投),处理逻辑必须保证同一个事件执行两遍和执行一遍效果相同——就像服务员对同一个铃铛不应两次端同一道菜。第二,补偿机制,当某个事件处理失败时,通过反向操作抵消已经发生的影响。延迟窗口不是系统缺陷,而是分布式世界为了性能和可用性付出的合理代价。

老顾客都懂美味斋的脾气:菜是等出来的,急不得。系统设计者也要管理好这份预期——客户要的是“最终能吃到菜”,而不是“点单的瞬间菜就摆在桌上”。

lim⁡t→∞Read⁡(t)=Write⁡(t−δ)\lim_{t \to \infty} \operatorname{Read}(t) = \operatorname{Write}(t - \delta)
在足够长的时间后,读模型会收敛到写入模型在 δ 时刻之前的状态——最终会一致,但不是瞬间一致。
  1. 极限收敛lim 表示随时间推移,读取结果逐渐逼近目标值。
    lim⁡t→∞\lim_{t \to \infty}
  2. 读模型状态Read(t) 是 t 时刻查询返回的数据。
    Read⁡(t)\operatorname{Read}(t)
  3. 延迟窗口δ 是投影更新的时间差,写入发生在 t-δ 时刻。
    δ\delta
  4. 收敛等式读模型最终等于“稍早时刻”的写入状态,两边握手言和。
    Read⁡(t)=Write⁡(t−δ)\operatorname{Read}(t) = \operatorname{Write}(t - \delta)

Saga 模式

有个顾客点了一份“全家福套餐”:红烧肉、清蒸鱼、酸辣汤。这三道菜分别由三位厨师负责。厨师 A 红烧肉做好了,厨师 B 清蒸鱼也上蒸锅了,可厨师 C 一看冰箱——酸辣汤的料没备齐!此时怎么办?老板一声令下:红烧肉撤回来放凉菜间,清蒸鱼也先别上,整套订单取消。这道“撤菜”的动作,就是补偿(Compensation)。

在分布式系统中,一个业务操作往往跨越多个独立服务,每个服务有自己独立的数据库,无法用传统数据库事务保证原子性。Saga 模式应运而生:把一个大型事务拆分成一串本地小事务,每个小事务都有对应的补偿操作。正常流程依次执行;一旦某一步失败,就沿着相反方向依次触发补偿,把已完成的步骤“撤销”回滚。

Saga 有两种组织方式:编排式(Choreography)让各服务通过事件自行接力,没有中央指挥,好比后厨里各厨师听见铃声自行配合,灵活但难追踪;协同式(Orchestration)则由一个 Saga 协调者发号施令,明确告诉每一步该做什么、失败后启动哪条补偿链,好比老板亲自盯流程,清晰可控但多了一个中心节点。无论哪种方式,Saga 都不能保证“绝对同时成功”,而是保证“要么全部成功,要么通过补偿回到原点”。

套餐最终没有上成,顾客有点失望,但绝不会吃到“只有红烧肉没有汤”的残缺套餐。Saga 用一系列“撤菜”动作,守住了分布式世界里那份难得的整体正确。

Tx⁡i∘Comp⁡i≈Id⁡\operatorname{Tx}_i \circ \operatorname{Comp}_i \approx \operatorname{Id}
每个本地事务与其补偿操作组合起来,效果近似“什么都没发生”,就像把动作做了再倒放一遍。
  1. 本地事务Tx_i 表示 Saga 中的第 i 步本地操作,如“扣库存”。
    Tx⁡i\operatorname{Tx}_i
  2. 补偿操作Comp_i 是对第 i 步的撤销动作,如“释放库存”。
    Comp⁡i\operatorname{Comp}_i
  3. 组合执行∘ 表示先执行事务、再执行补偿的先后串联。
    A∘BA \circ B
  4. 回到原地≈ Id 表示组合效果等价于一次“原地不动”。
    ≈Id⁡\approx \operatorname{Id}

AI 应用场景

电商订单与库存分离

阶段:系统架构设计

解决的问题:传统电商把订单写入和库存查询放在一个数据库,大促时写库压力大,商品详情页频繁查库存拖垮主库。采用 CQRS 后,订单写入走独立写模型,库存展示由事件投影到 Redis/Elasticsearch 读模型,读写互不干扰,查询响应从百毫秒级降到个位数毫秒。

具体案例:某电商平台将订单系统和库存系统解耦。用户下单动作产生"OrderCreated"事件,事件存储持久化后,投影服务更新库存读模型。用户刷新商品页看到的最新库存可能滞后几百毫秒,但高并发下单再也不会因为查询把数据库打爆。大促峰值时,系统只需横向扩展写模型节点,读模型保持稳定。

金融交易风控事件流

阶段:数据处理与特征计算

解决的问题:金融系统对审计和追溯要求极高,传统“只存最新状态”的数据库无法回答“这笔钱怎么一步步变成这样”。基于事件溯源 + CQRS,每一笔交易、每一次风控规则命中都作为不可变事件追加到事件存储,读模型投影出实时账户视图和风控报表。需要复盘时,从历史事件流重放即可。

具体案例:某支付平台将对账逻辑改造为事件驱动。每笔支付命令产生"PaymentInitiated""PaymentAuthorized"等事件,分析师通过投影出的交易多维视图做实时风控,审计人员通过事件重放还原每一笔资金流转路径。任何一次异常都能精确追溯到产生它的命令和事件序列。

多仓物流履约 Saga

阶段:分布式业务编排

解决的问题:一笔订单跨多个仓库发货,需要“扣库存 + 生成出库单 + 通知物流”多步协作。任一步失败,传统分布式事务难以处理。用 Saga 模式把履约链路拆成多个本地事务,任一步失败则反向触发补偿,保证订单状态要么全履约、要么全取消。

具体案例:某智能物流系统用 Saga 协调三个仓库的库存扣减。订单进入后,Saga 编排器依次调用各仓服务;若第三个仓库存不足,则自动触发前两个仓的库存释放补偿,同时向用户返回“部分缺货,订单取消”的明确结果。整体一致性的达成依靠补偿而非锁,系统吞吐显著提升。

Python 演示

python81 行
#!/usr/bin/env python3"""CQRS + 事件溯源 + 投影 + Saga 的最小模拟""" class EventStore:    """事件存储:只追加,不可变"""    def __init__(self):        self.events = []     def append(self, event):        self.events.append(event)     def replay(self):        return list(self.events)  class OrderProjection:    """读模型投影:把事件折叠成订单视图"""    def __init__(self):        self.orders = {}     def apply(self, event):        etype = event["type"]        if etype == "OrderCreated":            self.orders[event["order_id"]] = {                "items": event["items"], "user": event["user"], "status": "CREATED"            }        elif etype == "OrderApproved":            self.orders[event["order_id"]]["status"] = "APPROVED"        elif etype == "OrderCancelled":            self.orders[event["order_id"]]["status"] = "CANCELLED"     def get(self, order_id):        return self.orders.get(order_id)  class Saga:    """Saga 编排:下单 -> 扣库存 -> 扣款,失败则补偿取消"""    def __init__(self, store, projection):        self.store = store        self.projection = projection     def start(self, order_id, items, user):        self.store.append({"type": "OrderCreated", "order_id": order_id,                           "items": items, "user": user})        self.projection.apply(self.store.events[-1])        try:            self._reserve_stock(order_id, items)            self._charge(user, 100)        except RuntimeError as e:            print("[失败]", e)            self._cancel(order_id)            return        self.store.append({"type": "OrderApproved", "order_id": order_id})        self.projection.apply(self.store.events[-1])        print("[成功]", order_id, "approved")     def _reserve_stock(self, order_id, items):        if order_id == "bad":            raise RuntimeError("库存不足")     def _charge(self, user, amount):        if user == "bad":            raise RuntimeError("余额不足")     def _cancel(self, order_id):        self.store.append({"type": "OrderCancelled", "order_id": order_id})        self.projection.apply(self.store.events[-1])        print("[补偿] 已取消订单并释放库存", order_id)  if __name__ == "__main__":    store = EventStore()    proj = OrderProjection()    saga = Saga(store, proj)     saga.start("A001", ["红烧肉", "清蒸鱼"], "小明")    print("读模型查询:", proj.get("A001"))     saga.start("bad", ["酸辣汤"], "小红")    print("读模型查询:", proj.get("bad"))    print("事件日志条数:", len(store.replay()))
目的:用纯 Python 模拟事件驱动 CQRS 的最小闭环——命令进入写模型后产生事件,事件追加到不可变事件存储,投影消费事件更新读模型,Saga 在失败时沿补偿路径取消订单。
  1. self.store.append(event)事件存储只允许追加,天然留下完整审计日志。
  2. self.projection.apply(self.store.events[-1])投影消费每个新事件,折叠进读模型。
  3. self.orders[event["order_id"]]["status"] = "APPROVED"读模型只维护查询友好的现状。
  4. try ... except RuntimeError捕获本地事务失败,进入 Saga 补偿分支。
  5. self._cancel(order_id)补偿操作追加“订单已取消”事件,让状态回到业务可接受的终点。

常见误区

误区我只要把数据同时写到两个数据库,一个负责读一个负责写,就算用了 CQRS。很多新手一看 CQRS 的示意图就以为核心是“两个库”。

正解CQRS 的本质是职责分离,它允许读模型和写模型有不同的数据结构、不同的业务逻辑、不同的存储技术,甚至可以由不同的团队维护。两个存储只是实现手段,真正的价值在于读写模型各自的独立演化和独立伸缩。就像美味斋不是把同一本账抄两遍,而是让“点单”和“查菜”使用完全不同的工作模式。

为何常见:CQRS 的图形资料往往用“两个数据库加一条同步线”来示意,容易让人误以为“双写”就是模式本身。

误区用了 CQRS 和事件溯源后,我随时查询都能拿到最新数据,系统是强一致的。

正解绝大多数 CQRS 系统是最终一致性。命令写入事件存储后,投影异步更新读模型,中间存在延迟窗口 δ。读模型可能短暂显示旧状态,这种取舍是为了降低写入时延和系统耦合。设计时要做的是管理好这个窗口,比如给前端提示“数据同步中”,而不是幻想着强一致。

为何常见:人们习惯于关系型数据库的即时可见,下意识地把“写入成功”等同于“立刻能读到”。

误区事件存储不就是 Kafka 吗?把消息往里面一扔,消费者拉取处理,这不就是事件驱动了?

正解消息队列本质是传输管道,消息消费后通常会被删除,它不是历史档案;事件存储的重点是永久保留全部历史事件,支持任意时刻重放和状态重建。Kafka 可以作为传输层,但事件存储关心的是“事实的完整历史”,两者定位不同。流水账和传菜铃铛是两回事:铃铛响完就没了,账本却要永远留着。

为何常见:两者都处理“事件”,表面上高度相似,容易混为一谈。

误区把所有系统都改造成 CQRS,查询肯定快、写入肯定稳,性能问题一劳永逸。

正解CQRS 增加了系统复杂度:需要维护事件存储、投影任务、读模型的一致性。对读写负载均衡、查询简单的小型系统,引入 CQRS 反而拖慢交付。美味斋如果一天只有十桌客人,分两本账纯属多余。Fowler 本人也强调:CQRS 是“有门槛的模式”,只有在收益明显时才值得引入。

为何常见:架构师容易迷信“高并发标配”的套路,忽视技术方案的适用边界。

误区用了 Saga 就不用担心分布式事务了,数据一定不会出错。

正解Saga 并不提供强一致,它通过补偿操作保证“业务最终正确”。它要求每个事务都能设计出可执行的补偿逻辑,且系统要处理补偿本身可能失败的情况。它是一个工程权衡方案,而不是银弹。套餐撤菜的前提是每道菜都能被原样撤回,如果红烧肉已经被顾客吃了,补偿就无从谈起。

为何常见:Saga 能处理“跨服务失败回滚”的痛点,容易让人误以为它等价于传统 ACID 事务。

面试题

Q1. 在 CQRS 模式中,字母 C 代表的含义是什么?

单选题 · easy

  • A.Command(命令,负责写操作)← 正确答案
  • B.Create(创建,仅指新增操作)
  • C.Cache(缓存,负责加速读取)
  • D.Combine(合并,将读写合并处理)

答案:A。CQRS 全称 Command Query Responsibility Segregation,C 代表 Command(命令),负责修改状态;Q 代表 Query(查询),负责读取状态。Create 只是命令的一种,不能代表整体;缓存和合并与 CQRS 命名无关。

Q2. 事件存储最基本的特性是什么?

单选题 · easy

  • A.只允许追加写入且事件不可变← 正确答案
  • B.支持按主键随意修改数据行
  • C.自动清理超过七天的旧数据
  • D.只保存聚合的最新状态快照

答案:A。事件存储(Event Store)的核心是 append-only + 不可变性,所有历史事件永久保留,用于事件溯源和状态重建。它不对已有事件做修改或删除,也不只保存最新状态。随意修改和定期清理都会破坏历史事实的完整性。

Q3. 在事件驱动架构中,命令与事件的根本区别是什么?

单选题 · mid

  • A.命令表达意图,事件记录事实← 正确答案
  • B.命令用于读取,事件用于写入
  • C.命令必须丢弃,事件必须保留
  • D.事件有返回值,命令没有返回值

答案:A。命令是“我想要做什么”的请求,面向未来且可能被拒绝;事件是“已经发生了什么”的事实,面向过去且不可更改。命令是写入端的输入,事件是业务执行完的输出。命令通常不读取数据,事件也不是“必须有返回值”。

Q4. 投影(Projection)在 CQRS 中的主要作用是什么?

单选题 · mid

  • A.将事件流折叠成查询友好的读模型← 正确答案
  • B.将命令直接转发给底层数据库
  • C.删除历史事件以节省存储空间
  • D.对命令结果进行加密签名校验

答案:A。投影消费事件流,通过 apply 折叠操作生成面向查询的读模型表。它不负责命令转发,也不能删除历史事件(事件是不可变的),更不是做加密的工具。它的使命是让查询更快、更贴合业务展示形态。

Q5. 最终一致性允许系统在什么情况下返回旧数据?

单选题 · mid

  • A.在短暂的延迟窗口内读到稍旧的版本← 正确答案
  • B.在系统任意运行时长内都返回旧数据
  • C.只有系统重启之后才能读到新数据
  • D.仅当用户主动刷新页面时才能读到新数据

答案:A。最终一致性允许在事件写入到投影更新完成之间的 δ 窗口内,读到稍旧的数据,但保证在无新写入时最终收敛到最新状态。它不是“永远返回旧数据”,也不依赖重启或刷新才更新。分布式系统以可接受的短期延迟换取更高的可用性和吞吐。

Q6. 事件溯源中,重建聚合当前状态的正确做法是什么?

单选题 · hard

  • A.从初始状态重放全部历史事件← 正确答案
  • B.读取数据库中最新的状态行
  • C.查询消息队列的剩余积压量
  • D.从文件系统中读取配置文件

答案:A。事件溯源不保存最新状态,而是保存产生状态的全部事件。重建当前状态需要从初始状态出发,把事件流中的每个事件按顺序重放(replay)一遍。最新状态行、消息积压、配置文件都不是事件溯源的依据。这正是它可以回退到任意历史时刻的原因。

Q7. 在 Saga 模式中,补偿操作(Compensation)的组合效果是什么?

单选题 · extreme

  • A.与对应的本地事务叠加后近似回到执行前← 正确答案
  • B.让分布式事务获得数据库级强一致性保证
  • C.将失败事务自动重试直到成功为止
  • D.删除所有事件日志以隐藏失败痕迹

答案:A。Saga 的补偿操作与对应本地事务组合后,效果近似“恒等变换”,即回到事务执行前的状态。它并不能提供 ACID 强一致,也不保证重试成功,更不会删除事件日志。补偿的实质是通过反向操作把已经造成的影响“擦掉”,从而保证业务层面的整体正确。

Q8. 以下哪些场景适合采用 CQRS 模式?

多选题 · mid

  • A.读写负载差异非常大的业务系统← 正确答案
  • B.查询模式复杂且需要独立扩展读← 正确答案
  • C.所有简单的增删改查型小应用
  • D.同一业务需要多种不同读模型视图← 正确答案

答案:ABD。CQRS 适合读写差距大、查询复杂、读模型多样化的系统,它允许读模型独立演进和按需重建。简单的 CRUD 小应用引入 CQRS 只会徒增复杂度,并非所有场景都适用。选项 C 是典型的“过度设计”反面示例。

Q9. 要保证最终一致性系统的正确性,通常需要哪些配套措施?

多选题 · hard

  • A.消费者对重复事件做幂等处理← 正确答案
  • B.投影服务持续消费事件流← 正确答案
  • C.所有查询强制等待写库完成
  • D.用 Saga 对失败操作进行补偿← 正确答案

答案:ABD。幂等处理防止重复事件造成副作用,投影持续消费保证读模型持续推进,Saga 补偿让失败业务回到正确状态。而“所有查询强制等待写库完成”等于要求同步强一致,与最终一致性的设计初衷相悖,会把延迟窗口直接变回阻塞等待。

Q10. 事件存储与消息队列在架构中的区别体现在哪些方面?

多选题 · hard

  • A.事件存储永久保留历史事件← 正确答案
  • B.消息队列侧重传输与路由← 正确答案
  • C.两者功能完全相同可以混用
  • D.事件存储支持事件溯源重放← 正确答案

答案:ABD。事件存储定位是持久化的真相之源,保留全部历史并支持重放和状态重建;消息队列定位是传输管道,负责把事件投递给消费者,消息消费后可删除。两者可以配合使用(消息队列传输 + 事件存储归档),但功能并不相同,C 显然错误。