市图书馆新馆开张的第一天,馆长发现一个尴尬现象:读者想查《三体》在哪个书架,得和正在办新书入库的采编员挤同一个窗口。查书的队伍绕了花坛一圈,入库的推车却在后面干等。更要命的是,采编员每次低头登记,就被问路的读者打断;读者每次查到一半,又被搬运新书的推车堵住。一个柜台,两拨人,谁都没办好。馆长痛定思痛,在大厅里画了一条红线:左边是“办事”的采编台,右边是“查书”的目录厅,两套人、两套台账、两条工作流。从此,图书馆安静了。 这个“把办事和查书彻底分开”的改造,就是软件世界里 CQRS 模式的雏形——Command Query Responsibility Segregation,命令查询职责分离。CQRS 不是一个复杂的算法,而是一个朴素的管理学道理:让写入的专心写入,让读取的专心读取,各管各的模型、各配各的存储。当你读懂了这座图书馆,也就读懂了 CQRS:命令模型像采编台,负责改变状态;查询模型像检索卡,负责被人查阅;两者之间是一条异步的“传话通道”,馆藏变化会迟到一会儿,但总会到达。下面,我们就从这座图书馆的七个角落,把 CQRS 一次看个明白。

岗位关联

对架构师和高级后端工程师而言,掌握 CQRS 意味着能在高读写比、查询形态复杂的系统中,设计出可独立扩展、吞吐量成倍提升的读写链路;它也是架构岗位面试中绕不开的核心考点。

目录
  1. 可视化演示
  2. 模式概述:一个柜台管两件事的混乱
  3. 命令模型:采编台的办事流程
  4. 查询模型:检索卡的秘密
  5. 读写分离与同步:两个柜台之间的传话
  6. 事件溯源:一本流水账本
  7. 最终一致性:迟到的检索卡
  8. 适用场景与权衡:什么时候该开分馆
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示CQRS核心架构:命令端写入与查询端读取分离、事件溯源重建状态、异步同步与最终一致性三种模式。

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

读写分离:命令与查询职责分离 命令端处理写操作,优化写入吞吐量 查询端使用反规范化视图,加速读取 异步同步保持读写数据库最终一致 Write Model != Read Model CQRS通过分离读写模型实现各自独立优化。

模式概述:一个柜台管两件事的混乱

老图书馆只有一个服务台,读者问“《三体》在哪个架”和采编员办“500 本新书入库”挤在一起。查一本书只要 3 秒,但读者必须等采编员把手头的登记做完;入库一件事只要 5 分钟,却总被读者的提问打断。传统软件系统里的单个模块也是这样:一个 User 模型既被下单接口写入,又被列表接口反复读取;写操作要加锁防并发,读操作又抢这把锁。读写挤在同一个模型里,两败俱伤。

CQRS 的主张非常朴素:把系统拆成两个模型。命令模型(Command Model)只接收改状态的操作,比如“创建订单”“扣减库存”;查询模型(Query Model)只回答读问题,比如“这个用户最近的 20 个订单是什么”。两者不再是同一个类、同一张表、同一套服务,各按各的节奏演化和扩容。

在动手拆分前,先要算清一笔账:系统的读写比到底是多少?定义读请求量为 \(Q_{\text{read}}\),写请求量为 \(Q_{\text{write}}\),读写比 \(R\) 为两者之比:

馆长开馆前做的第一件事不是买书架,而是翻上个月的台账:1000 次窗口请求里,940 次是查书,60 次是办业务。十次请求里有九次半在“读”,他这才下定决心拉红线分柜台。CQRS 的第一步永远是:先看清你的读写比。

R=QreadQwrite\displaystyle R = \frac{Q_{\text{read}}}{Q_{\text{write}}}
读写比是决定是否引入 CQRS 的第一指标:R 越大于 1,读流量越占主导,拆出独立查询模型的收益越大;若 R 接近 1,拆分只会增加复杂度。
  1. Qread(读请求量)时间窗口内系统收到的查询请求总数,相当于图书馆窗口前问路的读者人次。
    QreadQ_{\text{read}}
  2. Qwrite(写请求量)同一窗口内到达的命令或写入请求总数,相当于采编员办理的新书入库单数量。
    QwriteQ_{\text{write}}
  3. R(读写比)读请求量除以写请求量,衡量负载结构。R 越大,拆出独立查询模型的收益越高。
    R=QreadQwrite\displaystyle R = \frac{Q_{\text{read}}}{Q_{\text{write}}}

命令模型:采编台的办事流程

采编台重新开张后,窗台上多了一叠印刷好的申请单:“新书入库单”上要填书名、ISBN、数量、入库时间。采编员只认单子不认人——单子没填全,直接退回;ISBN 重复了,直接驳回。每张单子办完,采编员在回执上盖章:“已入库”。

这些申请单就是命令(Command)。命令是“意图的载体”:它不描述怎么一步步做,只声明“我想让系统变成什么状态”。命令模型要做的只有三件事:接收命令、校验业务规则、应用状态变更。至于数据将来怎么被查询、被展示成什么样子,命令模型一概不管。

命令模型通常由命令处理器(Command Handler)负责接收。处理器先做业务校验(余额是否充足、库存是否够),再调用领域模型完成状态迁移。记当前状态为 \(S\),命令为 \(C\),应用函数 \(\text{apply}\) 负责校验并迁移状态,那么新状态 \(S'\) 可以写成:

采编台永远不会顺手帮你查书,也永远不会在登记簿之外另造一本“读者指南”——它只管把状态改对。改了状态之后的事,交给另一条柜台。这就是命令模型的本分。

S′=apply(S,C)\displaystyle S' = \text{apply}(S, C)
命令的“应用”不是简单的字段赋值,而是先校验、再迁移的完整业务动作;实际工程中命令还应携带幂等键,保证网络重试和消息重复投递不会造成重复计算。
  1. S(当前状态)命令应用前系统的状态,相当于书库里当前馆藏的集合。
    SS
  2. C(命令对象)携带意图与参数的命令,相当于一张填好的新书入库申请单。
    CC
  3. apply(应用函数)先校验规则、再迁移状态的完整动作,相当于采编员核对单子后把书摆上书架。
    apply\text{apply}
  4. S'(新状态)命令成功应用后的状态。命令侧只关心状态是否正确(馆藏多了哪些书),不关心怎么展示。
    S′=apply(S,C)\displaystyle S' = \text{apply}(S, C)

查询模型:检索卡的秘密

目录厅里那一排检索卡,是整座图书馆最聪明的设计。卡片上只写三样东西:书名、作者、架位。读者按字母翻,三秒定位。注意,卡片上没有任何“图书分类法”“借阅历史”——那些信息对读者找书毫无帮助,只会让卡片又厚又乱。

查询模型(Query Model,也叫读模型)就是这套检索卡。它不复制业务系统的完整语义,而是按界面与查询场景,把数据“重新排版”一遍。这个过程叫投影(Projection):从事件流或写侧数据中挑选字段,重组为利于查询的视图。写侧越严谨越好,读侧越“好查”越好。

投影可以形式化地写成:从一组事件 \(E_1, E_2, \ldots, E_n\) 中挑选所需字段并重组为视图 \(V\):

读者不需要理解图书分类法,就像前端不需要理解领域模型。检索卡替读者做了“翻译”,把复杂的世界折叠成一张好用的卡片——这正是查询模型存在的全部意义。

V=Πfields(E1,E2,…,En)\displaystyle V = \Pi_{\text{fields}}(E_1, E_2, \ldots, E_n)
读模型可以放心地反规范化:把订单和商品冗余在一张宽表里,省的查询时 JOIN 七八张表;可以放进 Redis 做缓存,也可以同步进 Elasticsearch 支持全文检索。领域模型负责“真实”,查询模型负责“好查”。
  1. E(事件流)写侧产生的一系列有序事件,相当于流水账本上的每一条入库通知。
    E1,E2,…,EnE_1, E_2, \ldots, E_n
  2. Πfields(投影操作)从事件中挑选需要的字段并重组为视图,相当于把书目信息摘成一张张检索卡。
    Πfields\Pi_{\text{fields}}
  3. V(视图模型)面向查询场景的结果结构,相当于读者手里那张只写“书名、作者、架位”的卡片。
    VV

读写分离与同步:两个柜台之间的传话

分柜台后第一个麻烦来了:新书凌晨 3 点入库,可目录厅的检索卡还是昨天下午的版本。最初馆长让采编员入库后立刻跑去目录厅插卡,结果采编台又堵了——同步变成了新的瓶颈。后来馆长加了条“传话通道”:采编员入库完毕,只要写一张“新书通知单”丢进传送篮,自有专人每 10 分钟收一次篮子,送去目录厅统一更新卡片。采编台不再跑腿,目录厅也不再干等。

技术上,这张“通知单”就是领域事件(Domain Event)。命令模型完成写入后发布事件,同步组件异步地把事件投递给查询模型,查询模型重新执行投影,刷新读数据。两个存储就此物理隔离:写库用关系型数据库保证事务与一致性,读库可以用 Elasticsearch、Redis 或文档数据库专攻查询。

异步带来一个关键指标——同步延迟。设写侧提交时刻为 \(t_{\text{write}}\),读侧完成同步时刻为 \(t_{\text{read}}\),则延迟为:

传话通道让两个柜台彻底解耦,代价是检索卡永远比真实书库慢半拍。馆长对读者的答复只有一句:“查到就是查到,查不到,过十分钟再来看。”这句话在技术圈有个正式的名字——最终一致性。

Δt=tread−twrite\displaystyle \Delta t = t_{\text{read}} - t_{\text{write}}
常见的三种同步策略:同步直写(强一致但耦合,写路径变慢);异步消息(解耦、写路径快,但读存在滞后);定时批处理(实现最简单,滞后最大)。大多数 CQRS 落地选择异步消息,用延迟换取吞吐和独立扩展能力。
  1. twrite(写侧提交时刻)命令模型完成写入并发布事件的时间点,相当于采编员把通知单丢进传送篮的那一刻。
    twritet_{\text{write}}
  2. tread(读侧同步时刻)查询模型消费事件、刷新投影完成的时间点,相当于目录厅卡片更新完毕的那一刻。
    treadt_{\text{read}}
  3. Δt(同步延迟)两个时间点相减,衡量检索卡比真实书库“慢”了多久。
    Δt=tread−twrite\displaystyle \Delta t = t_{\text{read}} - t_{\text{write}}

事件溯源:一本流水账本

有一年洪灾泡了目录厅一半卡片,馆长却一点不慌。因为他上任第一天就要求采编台写流水账:某年某月某日 14:30 入《三体》3 本,15:05 下架 1 本……每一条都按时间顺序记。对着一本没被水泡的流水账,馆长派两个实习生从开馆第一天逐条重放,三天后,一套全新的馆藏清单完整还原。事实不分对错,账本永远是对的。

事件溯源(Event Sourcing)就是这个流水账本。传统系统保存“当前状态”,比如余额 70 元;事件溯源保存的是一串“事实”:先入账 100,再支出 30。当前状态不再是事实,而是从事件流里计算出来的投影,随时可以丢弃、随时可以重建。

形式化地写,\(t\) 时刻的状态等于对初始状态 \(S_0\) 依次应用事件 \(e_1\) 到 \(e_t\) 做归约(fold):

馆长从此不再担心卡片被烧。只要流水账在,任何时刻的馆藏都能“重放”出来。这就是事件溯源给 CQRS 的底气:状态会丢,事实不会。

St=fold(S0,[e1,e2,…,et])\displaystyle S_t = \text{fold}(S_0, [e_1, e_2, \ldots, e_t])
CQRS 与事件溯源天然合拍:命令侧追加事件(append-only),查询侧消费事件流做投影。审计、回放、排查 bug 都变得轻松;代价是事件版本管理、重放性能、存储增长三大问题。
  1. S0(初始状态)开馆时空的馆藏,对应系统的初始版本。
    S0S_0
  2. e1…et(事件序列)从第 1 个到第 \(t\) 个不可变事件,顺序就是事实发生的先后。
    e1,e2,…,ete_1, e_2, \ldots, e_t
  3. fold(归约函数)从左到右把每个事件“叠加”进状态的迭代动作,相当于实习生逐条誊写流水账。
    fold\text{fold}
  4. St(重放状态)按序重放完 \(t\) 个事件后得到的当前状态,随时可以丢弃重建。
    St=fold(S0,[e1,e2,…,et])\displaystyle S_t = \text{fold}(S_0, [e_1, e_2, \ldots, e_t])

最终一致性:迟到的检索卡

读者渐渐习惯了检索卡“慢半拍”,但馆长给自己定了三条规矩:第一,卡片迟到最多不能超过 10 分钟,而且 99% 的情况下要做到;第二,同一张新书通知单绝不重复插卡;第三,每周日晚闭馆后全员对账——把真实书库和检索卡逐本比对,发现差异当场修正。

这些规矩翻译成技术语言,就是最终一致性(Eventual Consistency)的三个配套机制:一致性 SLO、幂等消费、对账补偿。CQRS 把“一致性达成时间”从写入提交那一刻推迟到了同步完成那一刻,业务必须接受一个延迟窗口 \(\Delta t\)。

工程上可以给一致性定一个可量化的目标:延迟 \(\Delta t\) 不超过阈值 \(\varepsilon\) 的概率不低于 \(1-\delta\),即:

馆长的三句话,成了 CQRS 落地团队的座右铭:迟到可以,错账不行;重放安全,幂等护航;平时不管,周日对账。最终一致性不是“没有一致性”,而是把一致性做成一个有界、可度量、可修复的工程承诺。

P(Δt≤ε)≥1−δ\displaystyle P(\Delta t \le \varepsilon) \ge 1 - \delta
落地时还要配合两个动作:用幂等键防止事件重复消费导致余额重复加;用定时对账任务扫描写侧与读侧的偏差。少数关键操作(如支付结果查询)可以强制绕过读模型直查写侧,保证“读自己的写”。
  1. Δt(延迟随机变量)每次同步的滞后时间,随负载抖动而变化,相当于每次传话通道实际耗费的分钟数。
    Δt\Delta t
  2. ε(容忍阈值)业务能接受的最大延迟,相当于馆长定的“不超过 10 分钟”。
    ε\varepsilon
  3. P(Δt≤ε) ≥ 1−δ以不低于 \(1-\delta\) 的概率保证延迟达标,\(\delta\) 是允许违约的少数场景比例。
    P(Δt≤ε)≥1−δ\displaystyle P(\Delta t \le \varepsilon) \ge 1 - \delta

适用场景与权衡:什么时候该开分馆

消息传开后,隔壁社区小馆的馆长也来取经,照着图纸拉了一条红线。结果呢?全馆一天只有 30 次请求,两个柜台互相闲着,还要多养一个人。小馆长懊恼地拆掉红线。市馆馆长笑着说:分馆不是标准答案,是一道成本题。先算读写比,再算养两个柜台的成本,最后才动红线。

判断是否引入 CQRS,核心是权衡。经验判据有几条:读写比悬殊(R 远大于 1)、查询形态多样(列表、详情、报表、搜索)、写侧与读侧扩展方向不同、团队有精力维护两套模型和同步链路。反过来,简单 CRUD 系统、读写比接近 1、团队小或交付压力大时,CQRS 是负资产。

把权衡写成判据:设同步链路的额外维护成本为 \(C_{\text{sync}}\),单体读写模型的基础成本为 \(C_{\text{base}}\),当读写比满足以下条件时,分离的收益才值得付出:

半年后,市馆的年度报告写着:查询吞吐翻了三倍,入库效率提升两成,检索卡和书库的偏差率降到千分之一。而隔壁小馆,依然一个柜台,岁月静好。CQRS 从来不是最时髦的答案,而是最清醒的成本题。答案只属于读懂了自身读写比的人。

R>1+CsyncCbase\displaystyle R > 1 + \frac{C_{\text{sync}}}{C_{\text{base}}}
也可以渐进式落地:先只拆模型不拆库,再给查询侧加缓存,再引入独立读库,最后才考虑事件溯源——每一步都可以停下来。CQRS 不是非黑即白,而是一条可以分段走的路。
  1. R(读写比)业务的读写比值,越大说明分离的收益越明显。
    RR
  2. Csync(同步成本)维护两套模型与同步链路的持续成本:消息中间件、对账任务、排障工时。
    CsyncC_{\text{sync}}
  3. Cbase(基础成本)单体读写模型下的基础开发与维护成本。
    CbaseC_{\text{base}}
  4. 判据(收益压过成本)当读写比高出“基础成本加一份同步成本”的分界时,CQRS 才划算。
    R>1+CsyncCbase\displaystyle R > 1 + \frac{C_{\text{sync}}}{C_{\text{base}}}

AI 应用场景

电商订单中心的高读写比改造

阶段:系统架构设计阶段

解决的问题:传统单体订单表同时承担下单写入与订单列表查询,大促时写锁拖慢查询,列表查询的聚合统计又把订单表压垮。运营后台、推荐展示又需要多种形态的读视图(列表、详情、聚合报表)。CQRS 把下单命令与订单查询投影分离:命令侧保持事务强一致,查询侧走只读副本与缓存,读写互不抢占。

具体案例:某电商平台把用户订单查询切换为 CQRS:写侧 MySQL 存订单事实,读侧 Elasticsearch 投影出订单列表视图;下单后通过 MQ 异步同步,用户端 99% 的查询延迟控制在 200ms 内,大促期间订单查询接口 TPS 提升 3 倍。

银行交易流水的不可变审计

阶段:数据存储与溯源建模阶段

解决的问题:传统余额表只存当前值,无法回答“这个余额是怎么来的”;对账、审计、反欺诈又需要完整的交易轨迹。事件溯源把所有交易存为不可变事件流,CQRS 的命令侧负责追加事件,查询侧投影出余额、日账单、流水明细,AI 反欺诈模型可以直接消费同一份事件流做特征提取。

具体案例:某支付系统改造后,每笔交易作为事件追加到事件存储;查询侧投影出“账户余额”“当日流水”“月对账单”三类读模型。审计人员要对账时,从事件流重放即可还原任意时点的账本,对账时间从天级降到分钟级。

物联网设备状态上报与监控大屏

阶段:数据接入与查询解耦阶段

解决的问题:海量设备高频上报(写多),监控大屏与 AI 异常检测又要做高频聚合查询(读多)。如果读写共用一张表,写入的锁和查询的聚合会互相拖垮。CQRS 让命令侧专收设备事件,查询侧按设备、区域、时间维度投影出监控视图,两侧可以完全独立扩容。

具体案例:车联网平台每小时接入千万级点位上报事件,命令侧用消息通道削峰;读侧投影出实时轨迹、区域热度、异常告警三类视图,支撑监控大屏与 AI 检测任务并发读取,查询 P99 稳定在 500ms 内。

Python 演示

python106 行
import timefrom collections import defaultdictfrom dataclasses import dataclass  @dataclassclass Command:    """A write-side intent: what to do and with which payload."""    account_id: str    kind: str  # 'deposit' or 'withdraw'    amount: float  @dataclassclass Event:    """An immutable fact appended to the event store."""    seq: int    account_id: str    kind: str    amount: float  class EventStore:    """Write-side: source of truth, an append-only event log."""     def __init__(self):        self._events = []        self._seq = 0     def append(self, cmd: Command) -> Event:        """Validate business rules, then persist the fact."""        if cmd.kind == 'withdraw' and cmd.amount > self.balance(cmd.account_id):            raise ValueError('insufficient balance')        self._seq += 1        event = Event(self._seq, cmd.account_id, cmd.kind, cmd.amount)        self._events.append(event)        return event     def balance(self, account_id: str) -> float:        """Rebuild current state from events (event sourcing replay)."""        total = 0.0        for e in self._events:            if e.account_id != account_id:                continue            total += e.amount if e.kind == 'deposit' else -e.amount        return total     def stream(self):        """Return all events in order for projection."""        return list(self._events)  class ReadModel:    """Query-side: a denormalized projection for fast reads."""     def __init__(self):        self.balances = defaultdict(float)        self.counts = defaultdict(int)        self.synced_seq = 0     def apply(self, e: Event) -> None:        """Apply one event into the projection view."""        if e.kind == 'deposit':            self.balances[e.account_id] += e.amount        else:            self.balances[e.account_id] -= e.amount        self.counts[e.account_id] += 1        self.synced_seq = e.seq  class Projector:    """Async sync: replay events into the read model."""    # 最终一致性: 读模型延迟一小段时间后才追上写侧     def __init__(self, store: EventStore, read_model: ReadModel, delay: float = 0.01):        self.store = store        self.read_model = read_model        self.delay = delay     def sync(self) -> None:        for e in self.store.stream():            if e.seq <= self.read_model.synced_seq:                continue            time.sleep(self.delay)  # simulate sync lag            self.read_model.apply(e)  def main():    store = EventStore()    read_model = ReadModel()    projector = Projector(store, read_model)     store.append(Command('alice', 'deposit', 100))    store.append(Command('alice', 'withdraw', 30))    store.append(Command('bob', 'deposit', 50))     print('before sync, alice balance =', read_model.balances['alice'])    projector.sync()    print('after sync,  alice balance =', read_model.balances['alice'])    print('event store size =', len(store.stream()))    print('read model synced_seq =', read_model.synced_seq)    print('replayed write-side balance =', store.balance('alice'))  if __name__ == '__main__':    main()
目的:用内存事件存储模拟 CQRS 最小闭环:命令侧只校验并追加事件,查询侧通过投影器异步同步生成读模型,直观演示“同步前读不到、同步后读到”的最终一致性。
  1. class EventStore:命令侧的“事实库”:只允许追加不可变事件,是系统的唯一事实来源,就像采编台的流水账本。
  2. if cmd.kind == 'withdraw' and cmd.amount > self.balance(cmd.account_id):命令校验:余额不足直接拒绝,确保写侧业务规则不被破坏。
  3. total += e.amount if e.kind == 'deposit' else -e.amount事件溯源重放:不保存余额,从每笔事件累加还原当前值。
  4. class ReadModel:查询侧的投影:用最方便查询的字典结构保存余额与交易次数,结构与事件流完全不同。
  5. if e.seq <= self.read_model.synced_seq:幂等消费:按事件序号跳过已处理事件,防止重复投影。
  6. time.sleep(self.delay)模拟同步延迟,这正是最终一致性“慢半拍”的来源。
  7. store.append(Command('alice', 'deposit', 100))写侧只接收命令对象,由事件存储完成校验与落库。

常见误区

误区我以为 CQRS 就是把数据库搞成主库和从库,主库写、从库读,这样就是“读写分离”了,不用再学什么模型不模型的。

正解主从复制只是数据层的冗余,两个库的表结构和读取逻辑几乎一样;而 CQRS 是模型层的职责分离:命令模型和查询模型是两套不同的设计,连数据结构都可能不同——写侧是范式化的领域模型,读侧是反范式化的投影宽表。就像图书馆里“书库”和“检索卡”是两种完全不同的东西,而不是同一本书抄两份。

为何常见:因为大多数入门资料都用“读写分离”四个字概括 CQRS,而主从复制是很多开发者接触到的第一个读写分离方案,自然容易画等号。

误区既然读和写都分开了,写侧随便写、读侧随便读,反正最后会“最终一致”,事务、锁、约束都可以不做了。

正解最终一致性释放的是读侧,写侧从来都是强一致的主场。命令校验、唯一约束、事务边界在写侧一样不能少,否则会写入一堆非法状态,脏数据顺着事件流污染整个读侧。一致性只是从“写提交时”推迟到了“同步完成时”,并且必须用 SLO、幂等、对账来兜底。

为何常见:“最终一致”四个字太容易被理解成“不用管一致性”,而忽略了它其实是一套有界、可度量、可修复的工程承诺。

误区听说 CQRS 能让吞吐量翻倍,那我这个新项目一上来就按 CQRS 设计,肯定没错,越早用越好。

正解CQRS 只有在读写比悬殊、两侧扩展方向不同时才划算;简单 CRUD 系统、读写比接近 1 时,两套模型加同步链路的维护成本远超收益。选择前先算读写比和复杂度账——那道 \(R > 1 + C_{\text{sync}}/C_{\text{base}}\) 的判据,就是在动手前让你冷静下来。

为何常见:架构文章总爱讲亮眼的成功案例,很少讲失败案例和前置条件,读者容易把“高读写比场景的最佳实践”当成“通用最佳实践”。

误区学习资料里提到的 CQRS 都画着事件流和事件存储,所以我觉得不用事件溯源就不算真正的 CQRS,两个必须一起上。

正解CQRS 和事件溯源是两个独立的正交模式:CQRS 关注读写模型的职责分离;事件溯源关注状态的存储方式。完全可以用普通 ORM 加数据库实现 CQRS 的写侧,也可以用事件溯源但只有一个模型。两者组合是常见实践,但不是唯一答案。

为何常见:因为国内外 CQRS 教程几乎都拿“事件溯源 + CQRS”当标准演示案例,看得多了就以为二者绑定。

面试题

Q1. 下列关于 CQRS 的核心思想,描述最准确的是?

单选题 · easy

  • A.将数据存储拆成主库与从库
  • B.将读写操作分离成两套模型← 正确答案
  • C.用消息队列异步处理请求
  • D.将数据库替换为缓存系统

答案:B。CQRS 的本质是把“改状态”和“读数据”拆成两个模型来设计。主从复制(A)只是数据层的冗余,两个副本模型相同;消息队列(C)是 CQRS 常配合的工具,但不是定义本身;把查询换成缓存(D)也只是手段。选 B。

Q2. 在图书馆比喻中,对应 CQRS 查询模型的是?

单选题 · easy

  • A.采编员按申请单办理入库
  • B.馆长决策增购新书采购单
  • C.读者按检索卡查询书目← 正确答案
  • D.清洁工整理书架和桌椅

答案:C。图书馆比喻里,采编台对应命令模型,检索卡对应查询模型。读者按卡查书(C)是典型的查询行为。A、B 都是写侧动作,D 是物理整理,都与查询模型无关。

Q3. CQRS 中命令模型的核心职责是?

单选题 · easy

  • A.校验业务规则并执行状态变更← 正确答案
  • B.为前端缓存整页查询结果
  • C.定时生成统计报表快照
  • D.维护所有读者的浏览记录

答案:A。命令侧的职责是接收命令、校验规则、执行状态变更(A)。B、C、D 都是查询侧的活:为展示优化、生成报表、记录浏览行为,不应混入命令模型。

Q4. 下列关于最终一致性的描述,正确的是?

单选题 · mid

  • A.读模型立即反映最新写入结果
  • B.所有写操作必须严格串行执行
  • C.查询永远返回同一个固定结果
  • D.读模型稍后延迟才完成同步← 正确答案

答案:D。最终一致性的定义就是“读侧经过一小段延迟后最终达到一致”(D)。A 是强一致的表现;B 说的是写侧调度策略;C 说的是静态数据,都与最终一致性无关。

Q5. 事件溯源中,系统如何恢复当前状态?

单选题 · mid

  • A.从历史备份库中恢复快照
  • B.重新执行命令生成新的状态
  • C.按顺序重放全部累计事件← 正确答案
  • D.手工修正当前状态字段值

答案:C。事件溯源不保存最终状态,只保存事件流水;恢复状态唯一的标准做法是按事件顺序从头重放(C)。A 是备份恢复,B 重放的是命令而非事件,D 是手工修正,都不符合溯源思想。

Q6. 下列场景中,适合采用 CQRS 的有哪些?

多选题 · mid

  • A.读写比接近一的简单内部应用
  • B.读写比悬殊且并发量极高的系统← 正确答案
  • C.只读静态页面展示的营销官网
  • D.查询形态多样且需要独立扩展← 正确答案

答案:BD。适合 CQRS 的场景是读多写少、查询形态多样且需要两端独立扩展(B、D)。A 读写均衡时收益很小;C 静态页面几乎没有写侧,谈不上分离。

Q7. 关于 CQRS 与事件溯源的关系,正确的是?

单选题 · hard

  • A.二者必须同时使用不可单独拆分
  • B.事件溯源可以完全替代查询模型
  • C.命令侧存事件流供读侧投影← 正确答案
  • D.读模型必须保存全部原始事件

答案:C。CQRS 与事件溯源是正交模式,可自由组合:命令侧存事件流,读侧从事件流投影出读模型(C)。A 说必须同时使用,错;B 事件溯源只是命令侧的存储风格,不替代查询模型;D 读模型存的是投影结果而非全部历史事件。

Q8. 读侧数据滞后时,应优先采取的措施是?

单选题 · hard

  • A.立即回滚取消所有刚提交的写入
  • B.重放事件并做对账修正同步偏差← 正确答案
  • C.让所有查询请求直接阻塞等待
  • D.删除读模型改回主库直查直返

答案:B。读侧滞后是异步同步的正常现象,正确做法是保证事件不丢不乱:重放事件并对账修正(B)。A 回滚已提交写入会破坏写侧事实;C 阻塞查询会拖垮读路径;D 放弃读模型等于退回单体架构。

Q9. CQRS 带来的收益包括哪些?

多选题 · mid

  • A.读写模型可以各自独立扩展← 正确答案
  • B.查询能够按场景定制投影← 正确答案
  • C.事务与一致性需求完全消失
  • D.命令校验逻辑更加清晰聚焦← 正确答案

答案:ABD。CQRS 的收益包括读写模型独立扩展(A)、查询按场景定制投影(B)、命令校验更聚焦(D)。但 C 错——写侧的事务与一致性需求不会消失,它只是从读侧转移到了写侧。

Q10. 关于最终一致性的补偿机制,下列做法合理的有?

多选题 · extreme

  • A.定期对账修正两端的偏差数据← 正确答案
  • B.用幂等键防止事件重复消费← 正确答案
  • C.同步失败后直接静默丢弃事件
  • D.保留事件版本便于按序重放← 正确答案

答案:ABD。处理最终一致性需要三件套:对账修正偏差(A)、幂等键防重复消费(B)、保留事件版本便于重放与修复(D)。C 静默丢弃事件会永久丢失事实,正如流水账被撕掉一页,是事故温床。