市图书馆新馆开张的第一天,馆长发现一个尴尬现象:读者想查《三体》在哪个书架,得和正在办新书入库的采编员挤同一个窗口。查书的队伍绕了花坛一圈,入库的推车却在后面干等。更要命的是,采编员每次低头登记,就被问路的读者打断;读者每次查到一半,又被搬运新书的推车堵住。一个柜台,两拨人,谁都没办好。馆长痛定思痛,在大厅里画了一条红线:左边是“办事”的采编台,右边是“查书”的目录厅,两套人、两套台账、两条工作流。从此,图书馆安静了。 这个“把办事和查书彻底分开”的改造,就是软件世界里 CQRS 模式的雏形——Command Query Responsibility Segregation,命令查询职责分离。CQRS 不是一个复杂的算法,而是一个朴素的管理学道理:让写入的专心写入,让读取的专心读取,各管各的模型、各配各的存储。当你读懂了这座图书馆,也就读懂了 CQRS:命令模型像采编台,负责改变状态;查询模型像检索卡,负责被人查阅;两者之间是一条异步的“传话通道”,馆藏变化会迟到一会儿,但总会到达。下面,我们就从这座图书馆的七个角落,把 CQRS 一次看个明白。
对架构师和高级后端工程师而言,掌握 CQRS 意味着能在高读写比、查询形态复杂的系统中,设计出可独立扩展、吞吐量成倍提升的读写链路;它也是架构岗位面试中绕不开的核心考点。
目录
可视化演示
展示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 的第一步永远是:先看清你的读写比。
- Qread(读请求量)时间窗口内系统收到的查询请求总数,相当于图书馆窗口前问路的读者人次。
- Qwrite(写请求量)同一窗口内到达的命令或写入请求总数,相当于采编员办理的新书入库单数量。
- R(读写比)读请求量除以写请求量,衡量负载结构。R 越大,拆出独立查询模型的收益越高。
命令模型:采编台的办事流程
采编台重新开张后,窗台上多了一叠印刷好的申请单:“新书入库单”上要填书名、ISBN、数量、入库时间。采编员只认单子不认人——单子没填全,直接退回;ISBN 重复了,直接驳回。每张单子办完,采编员在回执上盖章:“已入库”。
这些申请单就是命令(Command)。命令是“意图的载体”:它不描述怎么一步步做,只声明“我想让系统变成什么状态”。命令模型要做的只有三件事:接收命令、校验业务规则、应用状态变更。至于数据将来怎么被查询、被展示成什么样子,命令模型一概不管。
命令模型通常由命令处理器(Command Handler)负责接收。处理器先做业务校验(余额是否充足、库存是否够),再调用领域模型完成状态迁移。记当前状态为 \(S\),命令为 \(C\),应用函数 \(\text{apply}\) 负责校验并迁移状态,那么新状态 \(S'\) 可以写成:
采编台永远不会顺手帮你查书,也永远不会在登记簿之外另造一本“读者指南”——它只管把状态改对。改了状态之后的事,交给另一条柜台。这就是命令模型的本分。
- S(当前状态)命令应用前系统的状态,相当于书库里当前馆藏的集合。
- C(命令对象)携带意图与参数的命令,相当于一张填好的新书入库申请单。
- apply(应用函数)先校验规则、再迁移状态的完整动作,相当于采编员核对单子后把书摆上书架。
- S'(新状态)命令成功应用后的状态。命令侧只关心状态是否正确(馆藏多了哪些书),不关心怎么展示。
查询模型:检索卡的秘密
目录厅里那一排检索卡,是整座图书馆最聪明的设计。卡片上只写三样东西:书名、作者、架位。读者按字母翻,三秒定位。注意,卡片上没有任何“图书分类法”“借阅历史”——那些信息对读者找书毫无帮助,只会让卡片又厚又乱。
查询模型(Query Model,也叫读模型)就是这套检索卡。它不复制业务系统的完整语义,而是按界面与查询场景,把数据“重新排版”一遍。这个过程叫投影(Projection):从事件流或写侧数据中挑选字段,重组为利于查询的视图。写侧越严谨越好,读侧越“好查”越好。
投影可以形式化地写成:从一组事件 \(E_1, E_2, \ldots, E_n\) 中挑选所需字段并重组为视图 \(V\):
读者不需要理解图书分类法,就像前端不需要理解领域模型。检索卡替读者做了“翻译”,把复杂的世界折叠成一张好用的卡片——这正是查询模型存在的全部意义。
- E(事件流)写侧产生的一系列有序事件,相当于流水账本上的每一条入库通知。
- Πfields(投影操作)从事件中挑选需要的字段并重组为视图,相当于把书目信息摘成一张张检索卡。
- V(视图模型)面向查询场景的结果结构,相当于读者手里那张只写“书名、作者、架位”的卡片。
读写分离与同步:两个柜台之间的传话
分柜台后第一个麻烦来了:新书凌晨 3 点入库,可目录厅的检索卡还是昨天下午的版本。最初馆长让采编员入库后立刻跑去目录厅插卡,结果采编台又堵了——同步变成了新的瓶颈。后来馆长加了条“传话通道”:采编员入库完毕,只要写一张“新书通知单”丢进传送篮,自有专人每 10 分钟收一次篮子,送去目录厅统一更新卡片。采编台不再跑腿,目录厅也不再干等。
技术上,这张“通知单”就是领域事件(Domain Event)。命令模型完成写入后发布事件,同步组件异步地把事件投递给查询模型,查询模型重新执行投影,刷新读数据。两个存储就此物理隔离:写库用关系型数据库保证事务与一致性,读库可以用 Elasticsearch、Redis 或文档数据库专攻查询。
异步带来一个关键指标——同步延迟。设写侧提交时刻为 \(t_{\text{write}}\),读侧完成同步时刻为 \(t_{\text{read}}\),则延迟为:
传话通道让两个柜台彻底解耦,代价是检索卡永远比真实书库慢半拍。馆长对读者的答复只有一句:“查到就是查到,查不到,过十分钟再来看。”这句话在技术圈有个正式的名字——最终一致性。
- twrite(写侧提交时刻)命令模型完成写入并发布事件的时间点,相当于采编员把通知单丢进传送篮的那一刻。
- tread(读侧同步时刻)查询模型消费事件、刷新投影完成的时间点,相当于目录厅卡片更新完毕的那一刻。
- Δt(同步延迟)两个时间点相减,衡量检索卡比真实书库“慢”了多久。
事件溯源:一本流水账本
有一年洪灾泡了目录厅一半卡片,馆长却一点不慌。因为他上任第一天就要求采编台写流水账:某年某月某日 14:30 入《三体》3 本,15:05 下架 1 本……每一条都按时间顺序记。对着一本没被水泡的流水账,馆长派两个实习生从开馆第一天逐条重放,三天后,一套全新的馆藏清单完整还原。事实不分对错,账本永远是对的。
事件溯源(Event Sourcing)就是这个流水账本。传统系统保存“当前状态”,比如余额 70 元;事件溯源保存的是一串“事实”:先入账 100,再支出 30。当前状态不再是事实,而是从事件流里计算出来的投影,随时可以丢弃、随时可以重建。
形式化地写,\(t\) 时刻的状态等于对初始状态 \(S_0\) 依次应用事件 \(e_1\) 到 \(e_t\) 做归约(fold):
馆长从此不再担心卡片被烧。只要流水账在,任何时刻的馆藏都能“重放”出来。这就是事件溯源给 CQRS 的底气:状态会丢,事实不会。
- S0(初始状态)开馆时空的馆藏,对应系统的初始版本。
- e1…et(事件序列)从第 1 个到第 \(t\) 个不可变事件,顺序就是事实发生的先后。
- fold(归约函数)从左到右把每个事件“叠加”进状态的迭代动作,相当于实习生逐条誊写流水账。
- St(重放状态)按序重放完 \(t\) 个事件后得到的当前状态,随时可以丢弃重建。
最终一致性:迟到的检索卡
读者渐渐习惯了检索卡“慢半拍”,但馆长给自己定了三条规矩:第一,卡片迟到最多不能超过 10 分钟,而且 99% 的情况下要做到;第二,同一张新书通知单绝不重复插卡;第三,每周日晚闭馆后全员对账——把真实书库和检索卡逐本比对,发现差异当场修正。
这些规矩翻译成技术语言,就是最终一致性(Eventual Consistency)的三个配套机制:一致性 SLO、幂等消费、对账补偿。CQRS 把“一致性达成时间”从写入提交那一刻推迟到了同步完成那一刻,业务必须接受一个延迟窗口 \(\Delta t\)。
工程上可以给一致性定一个可量化的目标:延迟 \(\Delta t\) 不超过阈值 \(\varepsilon\) 的概率不低于 \(1-\delta\),即:
馆长的三句话,成了 CQRS 落地团队的座右铭:迟到可以,错账不行;重放安全,幂等护航;平时不管,周日对账。最终一致性不是“没有一致性”,而是把一致性做成一个有界、可度量、可修复的工程承诺。
- Δt(延迟随机变量)每次同步的滞后时间,随负载抖动而变化,相当于每次传话通道实际耗费的分钟数。
- ε(容忍阈值)业务能接受的最大延迟,相当于馆长定的“不超过 10 分钟”。
- P(Δt≤ε) ≥ 1−δ以不低于 \(1-\delta\) 的概率保证延迟达标,\(\delta\) 是允许违约的少数场景比例。
适用场景与权衡:什么时候该开分馆
消息传开后,隔壁社区小馆的馆长也来取经,照着图纸拉了一条红线。结果呢?全馆一天只有 30 次请求,两个柜台互相闲着,还要多养一个人。小馆长懊恼地拆掉红线。市馆馆长笑着说:分馆不是标准答案,是一道成本题。先算读写比,再算养两个柜台的成本,最后才动红线。
判断是否引入 CQRS,核心是权衡。经验判据有几条:读写比悬殊(R 远大于 1)、查询形态多样(列表、详情、报表、搜索)、写侧与读侧扩展方向不同、团队有精力维护两套模型和同步链路。反过来,简单 CRUD 系统、读写比接近 1、团队小或交付压力大时,CQRS 是负资产。
把权衡写成判据:设同步链路的额外维护成本为 \(C_{\text{sync}}\),单体读写模型的基础成本为 \(C_{\text{base}}\),当读写比满足以下条件时,分离的收益才值得付出:
半年后,市馆的年度报告写着:查询吞吐翻了三倍,入库效率提升两成,检索卡和书库的偏差率降到千分之一。而隔壁小馆,依然一个柜台,岁月静好。CQRS 从来不是最时髦的答案,而是最清醒的成本题。答案只属于读懂了自身读写比的人。
- R(读写比)业务的读写比值,越大说明分离的收益越明显。
- Csync(同步成本)维护两套模型与同步链路的持续成本:消息中间件、对账任务、排障工时。
- Cbase(基础成本)单体读写模型下的基础开发与维护成本。
- 判据(收益压过成本)当读写比高出“基础成本加一份同步成本”的分界时,CQRS 才划算。
AI 应用场景
电商订单中心的高读写比改造
阶段:系统架构设计阶段
解决的问题:传统单体订单表同时承担下单写入与订单列表查询,大促时写锁拖慢查询,列表查询的聚合统计又把订单表压垮。运营后台、推荐展示又需要多种形态的读视图(列表、详情、聚合报表)。CQRS 把下单命令与订单查询投影分离:命令侧保持事务强一致,查询侧走只读副本与缓存,读写互不抢占。
具体案例:某电商平台把用户订单查询切换为 CQRS:写侧 MySQL 存订单事实,读侧 Elasticsearch 投影出订单列表视图;下单后通过 MQ 异步同步,用户端 99% 的查询延迟控制在 200ms 内,大促期间订单查询接口 TPS 提升 3 倍。
银行交易流水的不可变审计
阶段:数据存储与溯源建模阶段
解决的问题:传统余额表只存当前值,无法回答“这个余额是怎么来的”;对账、审计、反欺诈又需要完整的交易轨迹。事件溯源把所有交易存为不可变事件流,CQRS 的命令侧负责追加事件,查询侧投影出余额、日账单、流水明细,AI 反欺诈模型可以直接消费同一份事件流做特征提取。
具体案例:某支付系统改造后,每笔交易作为事件追加到事件存储;查询侧投影出“账户余额”“当日流水”“月对账单”三类读模型。审计人员要对账时,从事件流重放即可还原任意时点的账本,对账时间从天级降到分钟级。
物联网设备状态上报与监控大屏
阶段:数据接入与查询解耦阶段
解决的问题:海量设备高频上报(写多),监控大屏与 AI 异常检测又要做高频聚合查询(读多)。如果读写共用一张表,写入的锁和查询的聚合会互相拖垮。CQRS 让命令侧专收设备事件,查询侧按设备、区域、时间维度投影出监控视图,两侧可以完全独立扩容。
具体案例:车联网平台每小时接入千万级点位上报事件,命令侧用消息通道削峰;读侧投影出实时轨迹、区域热度、异常告警三类视图,支撑监控大屏与 AI 检测任务并发读取,查询 P99 稳定在 500ms 内。
Python 演示
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()
class EventStore:命令侧的“事实库”:只允许追加不可变事件,是系统的唯一事实来源,就像采编台的流水账本。if cmd.kind == 'withdraw' and cmd.amount > self.balance(cmd.account_id):命令校验:余额不足直接拒绝,确保写侧业务规则不被破坏。total += e.amount if e.kind == 'deposit' else -e.amount事件溯源重放:不保存余额,从每笔事件累加还原当前值。class ReadModel:查询侧的投影:用最方便查询的字典结构保存余额与交易次数,结构与事件流完全不同。if e.seq <= self.read_model.synced_seq:幂等消费:按事件序号跳过已处理事件,防止重复投影。time.sleep(self.delay)模拟同步延迟,这正是最终一致性“慢半拍”的来源。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 的核心思想,描述最准确的是?
- A.将数据存储拆成主库与从库
- B.将读写操作分离成两套模型← 正确答案
- C.用消息队列异步处理请求
- D.将数据库替换为缓存系统
答案:B。CQRS 的本质是把“改状态”和“读数据”拆成两个模型来设计。主从复制(A)只是数据层的冗余,两个副本模型相同;消息队列(C)是 CQRS 常配合的工具,但不是定义本身;把查询换成缓存(D)也只是手段。选 B。
Q2. 在图书馆比喻中,对应 CQRS 查询模型的是?
- A.采编员按申请单办理入库
- B.馆长决策增购新书采购单
- C.读者按检索卡查询书目← 正确答案
- D.清洁工整理书架和桌椅
答案:C。图书馆比喻里,采编台对应命令模型,检索卡对应查询模型。读者按卡查书(C)是典型的查询行为。A、B 都是写侧动作,D 是物理整理,都与查询模型无关。
Q3. CQRS 中命令模型的核心职责是?
- A.校验业务规则并执行状态变更← 正确答案
- B.为前端缓存整页查询结果
- C.定时生成统计报表快照
- D.维护所有读者的浏览记录
答案:A。命令侧的职责是接收命令、校验规则、执行状态变更(A)。B、C、D 都是查询侧的活:为展示优化、生成报表、记录浏览行为,不应混入命令模型。
Q4. 下列关于最终一致性的描述,正确的是?
- A.读模型立即反映最新写入结果
- B.所有写操作必须严格串行执行
- C.查询永远返回同一个固定结果
- D.读模型稍后延迟才完成同步← 正确答案
答案:D。最终一致性的定义就是“读侧经过一小段延迟后最终达到一致”(D)。A 是强一致的表现;B 说的是写侧调度策略;C 说的是静态数据,都与最终一致性无关。
Q5. 事件溯源中,系统如何恢复当前状态?
- A.从历史备份库中恢复快照
- B.重新执行命令生成新的状态
- C.按顺序重放全部累计事件← 正确答案
- D.手工修正当前状态字段值
答案:C。事件溯源不保存最终状态,只保存事件流水;恢复状态唯一的标准做法是按事件顺序从头重放(C)。A 是备份恢复,B 重放的是命令而非事件,D 是手工修正,都不符合溯源思想。
Q6. 下列场景中,适合采用 CQRS 的有哪些?
- A.读写比接近一的简单内部应用
- B.读写比悬殊且并发量极高的系统← 正确答案
- C.只读静态页面展示的营销官网
- D.查询形态多样且需要独立扩展← 正确答案
答案:BD。适合 CQRS 的场景是读多写少、查询形态多样且需要两端独立扩展(B、D)。A 读写均衡时收益很小;C 静态页面几乎没有写侧,谈不上分离。
Q7. 关于 CQRS 与事件溯源的关系,正确的是?
- A.二者必须同时使用不可单独拆分
- B.事件溯源可以完全替代查询模型
- C.命令侧存事件流供读侧投影← 正确答案
- D.读模型必须保存全部原始事件
答案:C。CQRS 与事件溯源是正交模式,可自由组合:命令侧存事件流,读侧从事件流投影出读模型(C)。A 说必须同时使用,错;B 事件溯源只是命令侧的存储风格,不替代查询模型;D 读模型存的是投影结果而非全部历史事件。
Q8. 读侧数据滞后时,应优先采取的措施是?
- A.立即回滚取消所有刚提交的写入
- B.重放事件并做对账修正同步偏差← 正确答案
- C.让所有查询请求直接阻塞等待
- D.删除读模型改回主库直查直返
答案:B。读侧滞后是异步同步的正常现象,正确做法是保证事件不丢不乱:重放事件并对账修正(B)。A 回滚已提交写入会破坏写侧事实;C 阻塞查询会拖垮读路径;D 放弃读模型等于退回单体架构。
Q9. CQRS 带来的收益包括哪些?
- A.读写模型可以各自独立扩展← 正确答案
- B.查询能够按场景定制投影← 正确答案
- C.事务与一致性需求完全消失
- D.命令校验逻辑更加清晰聚焦← 正确答案
答案:ABD。CQRS 的收益包括读写模型独立扩展(A)、查询按场景定制投影(B)、命令校验更聚焦(D)。但 C 错——写侧的事务与一致性需求不会消失,它只是从读侧转移到了写侧。
Q10. 关于最终一致性的补偿机制,下列做法合理的有?
- A.定期对账修正两端的偏差数据← 正确答案
- B.用幂等键防止事件重复消费← 正确答案
- C.同步失败后直接静默丢弃事件
- D.保留事件版本便于按序重放← 正确答案
答案:ABD。处理最终一致性需要三件套:对账修正偏差(A)、幂等键防重复消费(B)、保留事件版本便于重放与修复(D)。C 静默丢弃事件会永久丢失事实,正如流水账被撕掉一页,是事故温床。