在市区有一家名叫“美味斋”的餐厅,生意好得让人羡慕,可老板却愁眉苦脸。每到饭点,服务员在大堂和厨房之间来回奔忙:顾客催着点菜,后厨催着上菜,所有人都挤在一本“总账本”前——记订单要写账本,查菜色要翻账本,账本被抢来抢去,队伍越排越长,出菜越来越慢。 老板想了个办法:把“记录订单”和“查询菜单”彻底分开。下单用一本“订单册”,只负责记;查菜改用另一本“菜单册”,只负责念。后厨每做好一道菜,就敲一下铃铛(事件),服务员听见铃声就去端菜,并顺手更新菜单册。两本册子各干各的,谁也不用等谁,饭点再也没了拥堵的账本。 这个故事,就是 CQRS(命令查询职责分离)模式的缩影。它把“写”与“读”彻底分开,再用“事件”把两边悄悄连起来,让系统既能扛住疯狂的写入,又能给用户飞快的查询。接下来的内容,我们将从这家餐厅出发,认识事件存储、投影重建、最终一致性,以及处理复杂业务的 Saga 模式。
理解 CQRS 与事件驱动,是设计高响应、可扩展分布式系统的核心能力之一,也是 AI 工程化架构师面对复杂业务时必备的思维工具。
可视化演示
展示CQRS读写分离架构与事件溯源流程,动态演示命令到事件存储到投影读模型的完整链路,交互切换CQRS、事件溯源与Saga模式。
本节配有交互动效(进入视口后自动加载,约 3KB)。
CQRS:命令查询职责分离 靛蓝为命令侧(写操作验证与处理) 橙色为事件存储(持久化领域事件) 绿色为读模型(投影构建查询视图) Cmd -> Event -> Store -> Project -> ReadModel CQRS将写操作与读操作分离到不同模型,通过事件异步同步实现读写独立扩展
CQRS 基础
美味斋老板的痛点,你一定能共鸣:一本“总账本”既要记订单,又要被翻来翻去查菜色。写的人要排队,读的人也要排队,双方互相拖累。老板的解法很朴素——既然记和查是两件本质不同的事,为什么非要共用一本账本?他把账本一分为二:“订单册”只负责写下顾客要什么,“菜单册”只负责回答今天有什么。
这个“分家”的思路,就是 CQRS(Command Query Responsibility Segregation,命令查询职责分离)的核心。它告诉我们:写操作(命令)和读操作(查询)应该使用各自独立的模型,而不是挤在一个模型里将就。
在传统单体系统中,读写共用同一张数据库表,查询复杂了会影响写入性能,写入频繁了又会拖慢查询。CQRS 让写模型专注于业务规则与状态变更,让读模型专注于查询效率与展示形态。两个模型可以放在不同的数据库,比如写库用关系型数据库保证事务,读库用 Elasticsearch 或 Redis 加速检索。两者还能独立伸缩——饭点下单暴增时,只需扩写模型的机器;客人频繁翻菜单时,只需扩读模型的机器。当然,天下没有免费的午餐:读写分离增加了一套同步机制,因此它更适合读写负载差异大、查询模式复杂、需要弹性伸缩的系统。小饭店强行分两本账,反而浪费纸。
老板满意地看着两本册子:点单高峰时,记单的小二忙得满头汗,但查菜的服务员依然笑容满面——写和读,终于各忙各的了。
- R_command表示写模型中处理一个命令(如下单)所需的响应时间。
- R_query表示读模型中处理一次查询(如查菜色)所需的响应时间。
- max因为读写并行执行、互不阻塞,系统整体响应时延由两者中的瓶颈决定。
事件驱动结合
美味斋的后厨有一个默契:厨师做好菜,不会端着盘子满大堂找顾客,而是“叮”地敲一下铃铛。铃铛一响,闲着的服务员自然会来端菜。这声铃铛,就是事件。它不指定“必须由谁来端”,只陈述一个事实——“有一道菜出锅了”。
在事件驱动架构中,“事件”是对已发生事实的描述:订单已创建、菜品已上桌、顾客已结账。它和“命令”有本质区别:命令表达了“我想要做什么”的意图,是尚未发生的请求;事件则表达“已经发生了什么”,是不可改变的事实。CQRS 的写端接收命令、执行业务逻辑,然后把结果发布为事件,供读端和其他系统消费。
这种异步解耦带来的好处立竿见影:生产者(后厨)不需要等消费者(服务员)响应,两者通过消息中间件(如 Kafka、RabbitMQ)连接。生产者往管道里丢一个事件就继续干自己的活,消费者按自己的节奏处理。即便某个消费者暂时故障,事件也会在管道中积压等待,不会丢失。事件流因此具备严格的先后顺序——先下单后上菜,这个顺序必须被保持,否则业务就乱套了。
铃铛声此起彼伏,却井然有序。厨师、服务员、洗碗工各听各的铃,谁也不会堵在谁门口——这就是事件驱动解耦后的节奏感。
- 序号系数每个事件 e 都有一个位置下标,标识它们在业务中的发生顺序。
- 因果先后符号 \prec 表示“先于”,前一个事件在因果上早于后一个事件。
- 完整链条从 e₀ 到 eₙ 的整条链,构成一个业务故事的全部事实。
事件存储
美味斋后厨角落挂着一本牛皮封面的“流水账”。每一道菜出锅,账房先生就添一行:“12:03 红烧肉出锅”“12:17 清蒸鱼出锅”。这本账只许往上写,不许拿橡皮擦改。谁要问“3 号桌到底上了几个菜”,翻账便知。老板立下规矩:账本不许涂改,这就是餐厅的“铁面流水账”。
事件存储(Event Store)就是系统的这本流水账。它与普通数据库最大的不同:只追加(append-only),不修改、不删除。每一行记录一个不可变的事件事实,并按发生顺序排列。这个特性让事件存储成为事件驱动系统的“真相之源”——它是所有业务事实的唯一权威记录。
基于事件存储,我们可以实现事件溯源(Event Sourcing):不再保存对象的最新状态,而是保存产生这个状态的全部事件。想要当前状态?把事件从头到尾重放一遍即可。这带来惊人的能力:可以回退到任意历史时刻、可以诊断“当初是怎么变成这样的”、可以随时生成新的视角。代价则是事件会无限增长,需要定期做快照来压缩(把历史事件折叠成一个状态快照,事件继续保留)。
账本越写越厚,但老板心里反而踏实了——哪怕客人投诉“菜上错了”,翻一翻流水账,谁的责任一目了然,谁也改不了历史。
- 当前状态S_t 表示经过 t 个事件之后,某个聚合对象的当前状态。
- 初始状态S_0 是系统的起点,通常是空容器或初始配置。
- 累加求和把从第 1 个到第 t 个事件分别带来的增量全部加起来。
- 事件增量ΔE_k 是第 k 个事件对状态造成的改变量。
投影重建
服务员手里的“菜单册”是怎么来的?不是后厨现问的,而是账房先生每天早晨根据流水账整理出来的:看见“红烧肉出锅”就记一道红烧肉,看见标记“红烧肉售罄”就把菜单上的红烧肉划掉。菜单册是对流水账的一种“整理视角”——它只关心“今天能点什么菜”,不关心做菜的过程细节。
这个“从事件流整理出查询视图”的过程,就是投影(Projection)。读模型(Read Model)是投影的产物:一张为查询量身定制的表,上面的每一行都来自对事件流的折叠计算。比如电商系统里,“用户订单列表”视图就是由“OrderCreated 订单已创建”“OrderShipped 订单已发货”等事件投影而来的。
投影重建(Rebuild Projection)是 CQRS 的一把利器:既然事件流永久保留,读模型就可以随时推倒重建。菜单册被咖啡弄脏了?没关系,照流水账重新抄一份,几分钟后焕然一新。代码升级后想加一个字段?同样重放事件,生成包含新字段的读模型。读模型还可以按需构建多种形态——同一条事件流,既能投影成“今日菜单”,也能投影成“食材消耗统计”,互不影响。
菜单册旧了、脏了、被改乱了,都不怕——账房先生闭着眼都能重抄一本。读模型永远是“可抛弃、可重建”的临时产物,这份底气来自那本永不磨灭的流水账。
- 全部事件∀e ∈ E 表示遍历事件流中的每一个事件,一个都不漏。
- 折叠操作对当前读模型 R 应用事件 e,产生新的读模型。
- 持续更新← 表示反复执行、滚动更新,直到事件流处理完毕。
最终一致性
顾客催菜时,老板常说一句话:“菜正在做呢,马上到!”下单和上菜之间,天然存在一段等待时间。如果顾客下单后立刻要求后厨显示“菜已上桌”,那显然不现实。CQRS 系统同样如此:命令写入事件存储后,投影需要一点点时间才会更新读模型。这个“时间差”,就是最终一致性(Eventual Consistency)的延迟窗口。
一致性并不是只有“强”和“弱”两级。强一致性要求任何时刻读到的都是最新值;弱一致性允许读到旧值;最终一致性介于两者之间——它允许短时间内读到旧数据,但保证在没有新写入的情况下,所有读取最终会收敛到最新状态。CQRS 绝大多数场景选择最终一致性,换来的是更低的写入延迟和更高的系统吞吐。
要让最终一致性真正可靠,必须做好两件事:第一,幂等设计(Idempotency),消费者可能收到重复事件(比如消息重投),处理逻辑必须保证同一个事件执行两遍和执行一遍效果相同——就像服务员对同一个铃铛不应两次端同一道菜。第二,补偿机制,当某个事件处理失败时,通过反向操作抵消已经发生的影响。延迟窗口不是系统缺陷,而是分布式世界为了性能和可用性付出的合理代价。
老顾客都懂美味斋的脾气:菜是等出来的,急不得。系统设计者也要管理好这份预期——客户要的是“最终能吃到菜”,而不是“点单的瞬间菜就摆在桌上”。
- 极限收敛lim 表示随时间推移,读取结果逐渐逼近目标值。
- 读模型状态Read(t) 是 t 时刻查询返回的数据。
- 延迟窗口δ 是投影更新的时间差,写入发生在 t-δ 时刻。
- 收敛等式读模型最终等于“稍早时刻”的写入状态,两边握手言和。
Saga 模式
有个顾客点了一份“全家福套餐”:红烧肉、清蒸鱼、酸辣汤。这三道菜分别由三位厨师负责。厨师 A 红烧肉做好了,厨师 B 清蒸鱼也上蒸锅了,可厨师 C 一看冰箱——酸辣汤的料没备齐!此时怎么办?老板一声令下:红烧肉撤回来放凉菜间,清蒸鱼也先别上,整套订单取消。这道“撤菜”的动作,就是补偿(Compensation)。
在分布式系统中,一个业务操作往往跨越多个独立服务,每个服务有自己独立的数据库,无法用传统数据库事务保证原子性。Saga 模式应运而生:把一个大型事务拆分成一串本地小事务,每个小事务都有对应的补偿操作。正常流程依次执行;一旦某一步失败,就沿着相反方向依次触发补偿,把已完成的步骤“撤销”回滚。
Saga 有两种组织方式:编排式(Choreography)让各服务通过事件自行接力,没有中央指挥,好比后厨里各厨师听见铃声自行配合,灵活但难追踪;协同式(Orchestration)则由一个 Saga 协调者发号施令,明确告诉每一步该做什么、失败后启动哪条补偿链,好比老板亲自盯流程,清晰可控但多了一个中心节点。无论哪种方式,Saga 都不能保证“绝对同时成功”,而是保证“要么全部成功,要么通过补偿回到原点”。
套餐最终没有上成,顾客有点失望,但绝不会吃到“只有红烧肉没有汤”的残缺套餐。Saga 用一系列“撤菜”动作,守住了分布式世界里那份难得的整体正确。
- 本地事务Tx_i 表示 Saga 中的第 i 步本地操作,如“扣库存”。
- 补偿操作Comp_i 是对第 i 步的撤销动作,如“释放库存”。
- 组合执行∘ 表示先执行事务、再执行补偿的先后串联。
- 回到原地≈ Id 表示组合效果等价于一次“原地不动”。
AI 应用场景
电商订单与库存分离
阶段:系统架构设计
解决的问题:传统电商把订单写入和库存查询放在一个数据库,大促时写库压力大,商品详情页频繁查库存拖垮主库。采用 CQRS 后,订单写入走独立写模型,库存展示由事件投影到 Redis/Elasticsearch 读模型,读写互不干扰,查询响应从百毫秒级降到个位数毫秒。
具体案例:某电商平台将订单系统和库存系统解耦。用户下单动作产生"OrderCreated"事件,事件存储持久化后,投影服务更新库存读模型。用户刷新商品页看到的最新库存可能滞后几百毫秒,但高并发下单再也不会因为查询把数据库打爆。大促峰值时,系统只需横向扩展写模型节点,读模型保持稳定。
金融交易风控事件流
阶段:数据处理与特征计算
解决的问题:金融系统对审计和追溯要求极高,传统“只存最新状态”的数据库无法回答“这笔钱怎么一步步变成这样”。基于事件溯源 + CQRS,每一笔交易、每一次风控规则命中都作为不可变事件追加到事件存储,读模型投影出实时账户视图和风控报表。需要复盘时,从历史事件流重放即可。
具体案例:某支付平台将对账逻辑改造为事件驱动。每笔支付命令产生"PaymentInitiated""PaymentAuthorized"等事件,分析师通过投影出的交易多维视图做实时风控,审计人员通过事件重放还原每一笔资金流转路径。任何一次异常都能精确追溯到产生它的命令和事件序列。
多仓物流履约 Saga
阶段:分布式业务编排
解决的问题:一笔订单跨多个仓库发货,需要“扣库存 + 生成出库单 + 通知物流”多步协作。任一步失败,传统分布式事务难以处理。用 Saga 模式把履约链路拆成多个本地事务,任一步失败则反向触发补偿,保证订单状态要么全履约、要么全取消。
具体案例:某智能物流系统用 Saga 协调三个仓库的库存扣减。订单进入后,Saga 编排器依次调用各仓服务;若第三个仓库存不足,则自动触发前两个仓的库存释放补偿,同时向用户返回“部分缺货,订单取消”的明确结果。整体一致性的达成依靠补偿而非锁,系统吞吐显著提升。
Python 演示
#!/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()))
self.store.append(event)事件存储只允许追加,天然留下完整审计日志。self.projection.apply(self.store.events[-1])投影消费每个新事件,折叠进读模型。self.orders[event["order_id"]]["status"] = "APPROVED"读模型只维护查询友好的现状。try ... except RuntimeError捕获本地事务失败,进入 Saga 补偿分支。self._cancel(order_id)补偿操作追加“订单已取消”事件,让状态回到业务可接受的终点。
常见误区
误区我只要把数据同时写到两个数据库,一个负责读一个负责写,就算用了 CQRS。很多新手一看 CQRS 的示意图就以为核心是“两个库”。
正解CQRS 的本质是职责分离,它允许读模型和写模型有不同的数据结构、不同的业务逻辑、不同的存储技术,甚至可以由不同的团队维护。两个存储只是实现手段,真正的价值在于读写模型各自的独立演化和独立伸缩。就像美味斋不是把同一本账抄两遍,而是让“点单”和“查菜”使用完全不同的工作模式。
为何常见:CQRS 的图形资料往往用“两个数据库加一条同步线”来示意,容易让人误以为“双写”就是模式本身。
误区用了 CQRS 和事件溯源后,我随时查询都能拿到最新数据,系统是强一致的。
正解绝大多数 CQRS 系统是最终一致性。命令写入事件存储后,投影异步更新读模型,中间存在延迟窗口 δ。读模型可能短暂显示旧状态,这种取舍是为了降低写入时延和系统耦合。设计时要做的是管理好这个窗口,比如给前端提示“数据同步中”,而不是幻想着强一致。
为何常见:人们习惯于关系型数据库的即时可见,下意识地把“写入成功”等同于“立刻能读到”。
误区事件存储不就是 Kafka 吗?把消息往里面一扔,消费者拉取处理,这不就是事件驱动了?
正解消息队列本质是传输管道,消息消费后通常会被删除,它不是历史档案;事件存储的重点是永久保留全部历史事件,支持任意时刻重放和状态重建。Kafka 可以作为传输层,但事件存储关心的是“事实的完整历史”,两者定位不同。流水账和传菜铃铛是两回事:铃铛响完就没了,账本却要永远留着。
为何常见:两者都处理“事件”,表面上高度相似,容易混为一谈。
误区把所有系统都改造成 CQRS,查询肯定快、写入肯定稳,性能问题一劳永逸。
正解CQRS 增加了系统复杂度:需要维护事件存储、投影任务、读模型的一致性。对读写负载均衡、查询简单的小型系统,引入 CQRS 反而拖慢交付。美味斋如果一天只有十桌客人,分两本账纯属多余。Fowler 本人也强调:CQRS 是“有门槛的模式”,只有在收益明显时才值得引入。
为何常见:架构师容易迷信“高并发标配”的套路,忽视技术方案的适用边界。
误区用了 Saga 就不用担心分布式事务了,数据一定不会出错。
正解Saga 并不提供强一致,它通过补偿操作保证“业务最终正确”。它要求每个事务都能设计出可执行的补偿逻辑,且系统要处理补偿本身可能失败的情况。它是一个工程权衡方案,而不是银弹。套餐撤菜的前提是每道菜都能被原样撤回,如果红烧肉已经被顾客吃了,补偿就无从谈起。
为何常见:Saga 能处理“跨服务失败回滚”的痛点,容易让人误以为它等价于传统 ACID 事务。
面试题
Q1. 在 CQRS 模式中,字母 C 代表的含义是什么?
- A.Command(命令,负责写操作)← 正确答案
- B.Create(创建,仅指新增操作)
- C.Cache(缓存,负责加速读取)
- D.Combine(合并,将读写合并处理)
答案:A。CQRS 全称 Command Query Responsibility Segregation,C 代表 Command(命令),负责修改状态;Q 代表 Query(查询),负责读取状态。Create 只是命令的一种,不能代表整体;缓存和合并与 CQRS 命名无关。
Q2. 事件存储最基本的特性是什么?
- A.只允许追加写入且事件不可变← 正确答案
- B.支持按主键随意修改数据行
- C.自动清理超过七天的旧数据
- D.只保存聚合的最新状态快照
答案:A。事件存储(Event Store)的核心是 append-only + 不可变性,所有历史事件永久保留,用于事件溯源和状态重建。它不对已有事件做修改或删除,也不只保存最新状态。随意修改和定期清理都会破坏历史事实的完整性。
Q3. 在事件驱动架构中,命令与事件的根本区别是什么?
- A.命令表达意图,事件记录事实← 正确答案
- B.命令用于读取,事件用于写入
- C.命令必须丢弃,事件必须保留
- D.事件有返回值,命令没有返回值
答案:A。命令是“我想要做什么”的请求,面向未来且可能被拒绝;事件是“已经发生了什么”的事实,面向过去且不可更改。命令是写入端的输入,事件是业务执行完的输出。命令通常不读取数据,事件也不是“必须有返回值”。
Q4. 投影(Projection)在 CQRS 中的主要作用是什么?
- A.将事件流折叠成查询友好的读模型← 正确答案
- B.将命令直接转发给底层数据库
- C.删除历史事件以节省存储空间
- D.对命令结果进行加密签名校验
答案:A。投影消费事件流,通过 apply 折叠操作生成面向查询的读模型表。它不负责命令转发,也不能删除历史事件(事件是不可变的),更不是做加密的工具。它的使命是让查询更快、更贴合业务展示形态。
Q5. 最终一致性允许系统在什么情况下返回旧数据?
- A.在短暂的延迟窗口内读到稍旧的版本← 正确答案
- B.在系统任意运行时长内都返回旧数据
- C.只有系统重启之后才能读到新数据
- D.仅当用户主动刷新页面时才能读到新数据
答案:A。最终一致性允许在事件写入到投影更新完成之间的 δ 窗口内,读到稍旧的数据,但保证在无新写入时最终收敛到最新状态。它不是“永远返回旧数据”,也不依赖重启或刷新才更新。分布式系统以可接受的短期延迟换取更高的可用性和吞吐。
Q6. 事件溯源中,重建聚合当前状态的正确做法是什么?
- A.从初始状态重放全部历史事件← 正确答案
- B.读取数据库中最新的状态行
- C.查询消息队列的剩余积压量
- D.从文件系统中读取配置文件
答案:A。事件溯源不保存最新状态,而是保存产生状态的全部事件。重建当前状态需要从初始状态出发,把事件流中的每个事件按顺序重放(replay)一遍。最新状态行、消息积压、配置文件都不是事件溯源的依据。这正是它可以回退到任意历史时刻的原因。
Q7. 在 Saga 模式中,补偿操作(Compensation)的组合效果是什么?
- A.与对应的本地事务叠加后近似回到执行前← 正确答案
- B.让分布式事务获得数据库级强一致性保证
- C.将失败事务自动重试直到成功为止
- D.删除所有事件日志以隐藏失败痕迹
答案:A。Saga 的补偿操作与对应本地事务组合后,效果近似“恒等变换”,即回到事务执行前的状态。它并不能提供 ACID 强一致,也不保证重试成功,更不会删除事件日志。补偿的实质是通过反向操作把已经造成的影响“擦掉”,从而保证业务层面的整体正确。
Q8. 以下哪些场景适合采用 CQRS 模式?
- A.读写负载差异非常大的业务系统← 正确答案
- B.查询模式复杂且需要独立扩展读← 正确答案
- C.所有简单的增删改查型小应用
- D.同一业务需要多种不同读模型视图← 正确答案
答案:ABD。CQRS 适合读写差距大、查询复杂、读模型多样化的系统,它允许读模型独立演进和按需重建。简单的 CRUD 小应用引入 CQRS 只会徒增复杂度,并非所有场景都适用。选项 C 是典型的“过度设计”反面示例。
Q9. 要保证最终一致性系统的正确性,通常需要哪些配套措施?
- A.消费者对重复事件做幂等处理← 正确答案
- B.投影服务持续消费事件流← 正确答案
- C.所有查询强制等待写库完成
- D.用 Saga 对失败操作进行补偿← 正确答案
答案:ABD。幂等处理防止重复事件造成副作用,投影持续消费保证读模型持续推进,Saga 补偿让失败业务回到正确状态。而“所有查询强制等待写库完成”等于要求同步强一致,与最终一致性的设计初衷相悖,会把延迟窗口直接变回阻塞等待。
Q10. 事件存储与消息队列在架构中的区别体现在哪些方面?
- A.事件存储永久保留历史事件← 正确答案
- B.消息队列侧重传输与路由← 正确答案
- C.两者功能完全相同可以混用
- D.事件存储支持事件溯源重放← 正确答案
答案:ABD。事件存储定位是持久化的真相之源,保留全部历史并支持重放和状态重建;消息队列定位是传输管道,负责把事件投递给消费者,消息消费后可删除。两者可以配合使用(消息队列传输 + 事件存储归档),但功能并不相同,C 显然错误。