想象一座老式电影院:导演、演员、观众全部挤在同一间房里。导演刚喊了一句“换机位!”,观众立刻追问“为什么换?”,导演不得不回头解释半天;观众想回看刚才一帧画面,导演就得暂停整个拍摄。拍摄要按剧本严谨推进,观看要随心所欲随时回放——两种需求挤在一起,互相拖累,现场一片混乱。 现实中的电影工业早就解决了这个问题:导演在片场下达命令,演员执行,摄像机把一切记录成胶片;胶片被保存进资料库,之后由放映机把胶片投影到银幕上,供观众观看。拍电影和放电影被分成了两座建筑,各自按自己的节奏优化。CQRS(Command Query Responsibility Segregation,命令查询职责分离)正是把这种思想带进软件架构:写操作走命令通道,读操作走投影视图。而这套体系的核心枢纽,就是本文的主角——事件投影(Event Projection)。

岗位关联

对 AI 从业者而言,CQRS 与事件投影是构建高并发读多写少系统的重要架构武器。理解它,你才能设计出查询与写入互不拖累、可扩展、可回放的现代分布式系统。

目录
  1. 可视化演示
  2. 起源与定义:从混沌单间到双子影城
  3. 命令端设计:导演的拍摄指挥部
  4. 读模型构建:观众看到的银幕
  5. 事件投影机制:放映机的魔法
  6. 最终一致性:胶片需要冲洗时间
  7. 同步与重放:影院调度与断点续播
  8. 工程落地实践:从单厅影院到院线集团
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

将命令与查询分离到独立模型,经事件投影构建读模型,实现读写独立扩展与最终一致。

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

读写分离 命令 查询 模型 CMD+QRY 读写独立优化

起源与定义:从混沌单间到双子影城

老式电影院的困境,正是传统软件架构的日常。一个订单模块里,下单、改状态、校验库存这些“写操作”,和查列表、看详情、跑报表这些“读操作”共用同一个实体模型、同一张表、同一把锁。写流量大时,慢查询把数据库拖垮;读流量大时,事务提交被阻塞。就像一个剧组既要拍戏又要现场售票,两边都做不好。

传统 CRUD 系统使用同一个领域模型同时承担读写职责。但读与写有着截然不同的诉求:写关注业务规则和不变量,读关注查询速度和形态。Greg Young 在 2010 年前后将 CQRS 系统化:把系统拆成两条通道——写侧用命令(Command)表达“希望发生的意图”,读侧用查询(Query)读取投影后的视图,两者使用不同的模型、不同的存储、不同的优化策略。

导演从此专心在片场拍戏,观众在放映厅舒服观影。两条通道各自演进、各自扩容,这就是 CQRS 的起点。

Write:ΔS=f(C),Read:V=g(Q)\displaystyle \mathrm{Write}: \Delta S = f(C), \qquad \mathrm{Read}: V = g(Q)
写侧把命令 C 变成状态增量 ΔS;读侧把查询 Q 计算成视图 V。两者不再共用同一个模型。
  1. 写侧定义写侧接收命令,执行业务规则后产生状态变化。
    Write:ΔS=f(C)\mathrm{Write}: \Delta S = f(C)
  2. 状态增量ΔS 表示系统状态的变化量,而不是完整的新状态。
    ΔS=Snew−Sold\Delta S = S_{\text{new}} - S_{\text{old}}
  3. 读侧定义读侧只执行查询,不产生任何副作用。
    V=g(Q)V = g(Q)

命令端设计:导演的拍摄指挥部

导演身边有一位尽职的场记助理。助理收到“演员就位”的指令后,先核对剧本(校验规则),再喊“开拍”,演员按剧本执行,摄像机记录下真实发生的一切。这里,助理就是命令处理器,演员就是聚合对象,摄像机记录下来的素材就是领域事件。

命令端是 CQRS 的写半边。命令对象是不可变的意图描述,比如 PlaceOrderCommand(userId, items)。命令处理器(Command Handler)负责从仓库加载聚合、调用业务方法、校验业务不变量,最终让聚合产生一个领域事件。这个事件被追加到事件存储中,代表“已经发生的事实”——不是命令的希望,而是岁月的见证。

现场拍完的镜头被完整归档,场记在台本上写下“本镜已拍”。没有人需要立刻跑到放映厅去手动改银幕——那是投影器的工作。

Command→handlerAggregate→validateEvent→publishBus\displaystyle \mathrm{Command} \xrightarrow{\text{handler}} \mathrm{Aggregate} \xrightarrow{\text{validate}} \mathrm{Event} \xrightarrow{\text{publish}} \mathrm{Bus}
命令经过处理器编排、聚合校验后变成事件,事件发布到总线上等待投影侧消费。
  1. 命令进入一条命令从客户端进入命令总线,等待处理器接管。
    Command→handler\mathrm{Command} \xrightarrow{\text{handler}}
  2. 处理器编排处理器负责加载聚合、调用方法、捕获结果,不自己写业务逻辑。
    →handlerAggregate\xrightarrow{\text{handler}} \mathrm{Aggregate}
  3. 聚合校验聚合在自己的一致性边界内校验规则,保证不变量不被破坏。
    Aggregate→validateEvent\mathrm{Aggregate} \xrightarrow{\text{validate}} \mathrm{Event}
  4. 事件发布事件被持久化并发布到消息总线,供投影器订阅。
    Event→publishBus\mathrm{Event} \xrightarrow{\text{publish}} \mathrm{Bus}

读模型构建:观众看到的银幕

观众不会去片场看摄像机,他们看的是售票台、场次屏、座位图——每一个都是为特定查询精心准备的信息形态。同一部电影可以投影成海报、预告片、影评榜,它们来自同一份原片,却以各自适合的样子呈现在不同入口。

读模型是专门为查询而设计的模型,通常以反规范化(denormalized)的形式存在。查询“我的订单”页面需要用户名、订单号、商品列表、状态、金额,传统架构要 JOIN 三张表才能查全;而读模型在事件投影时就把这些字段物化成一张宽表。CQRS 允许甚至鼓励读模型与写模型使用完全不同的存储:写库用 MySQL 保证事务,读库用 Elasticsearch 或 Redis 追求性能。

影院的每个入口都挂着不同的信息牌:给观众看排片表,给财务看票房日报。读模型的“多形态”,正是 CQRS 应对复杂查询需求的优雅姿势。

R=πuserId, goods, status(Torder⋈Tuser)\displaystyle R = \pi_{\text{userId, goods, status}} (T_{\text{order}} \Join T_{\text{user}})
读模型是对多张业务表做“投影 + 连接”后的物化结果。关系代数里的这行式子,在 CQRS 中被提前计算并缓存成了视图。
  1. 投影运算π 表示按需要的列做投影,只保留查询关心的字段。
    πuserId, goods, status\pi_{\text{userId, goods, status}}
  2. 连接运算⋈ 表示把订单表和用户表按关联键连接起来。
    Torder⋈TuserT_{\text{order}} \Join T_{\text{user}}
  3. 物化结果把查询结果提前算好存入专用存储,查询时不再现场计算。
    R=物化视图R = \text{物化视图}

事件投影机制:放映机的魔法

真正的魔法藏在放映室。放映员把母带装进放映机,一帧帧投到银幕上。如果银幕布旧了要换新的,放映员只需要把母带从头再放一遍——投影可以无限重放,不必重新拍电影。CQRS 的读模型也是从事件流里“投影”出来的,那个负责投影的放映员,叫投影器(Projector)。

投影器订阅事件流,每收到一个事件(比如 OrderPlaced),就更新一次读模型。它需要维护一个进度位置(position/checkpoint),记录自己已经处理到第几个事件。当读模型需要重建时,投影器从位置 0 开始重放全部历史事件。为了提升效率,系统每隔一段时间打一个快照——相当于把胶片某几卷的“剧情摘要”存档,重建时从最近的快照开始,而不必从头逐帧放映。

放映机不会为了换幕布重拍电影,它只需要重放胶片。投影器同理——事件流就是那卷永不失真的母带。

Rt=⨁i=1tproj(ei)\displaystyle R_t = \bigoplus_{i=1}^{t} \mathrm{proj}(e_i)
读模型在时刻 t 的状态,等于从第 1 个到第 t 个事件依次经过投影函数叠加后的结果。
  1. 读模型状态R_t 表示投影完成后读模型在时刻 t 的快照。
    RtR_t
  2. 顺序叠加⊕ 表示按事件发生顺序把中间状态一个个应用、累积。
    ⨁i=1t\bigoplus_{i=1}^{t}
  3. 投影函数proj(e_i) 把单个事件 e_i 转化为对读模型的一次更新。
    proj(ei)\mathrm{proj}(e_i)

最终一致性:胶片需要冲洗时间

导演喊完“卡”,观众不可能瞬间看到这个镜头——胶片还要冲洗、剪辑、拷贝、配送。观众看到的画面总是“几分钟前的事实”,但故事不会因此出错。CQRS 中的读模型也有类似的“冲洗时间”,这就是最终一致性。

命令提交成功后,事件进入日志,但投影器可能还没处理到那条事件,读模型短暂滞后。如果用户刚下单就立刻刷新“我的订单”,可能会看不到新订单——这就是“读己之写”问题。解决思路包括:写后重定向到详情页并等待、对关键路径采用同步投影、客户端做短暂重试或轮询。更重要的保障是幂等(idempotent):同一事件被重复应用时不能重复加余额,投影函数必须以事件 ID 或版本号做去重。

观众永远不会看到“穿帮的瞬间”——导演喊停后几分钟,银幕上才出现刚才那个镜头。银幕反映的是真实发生过的事,只是略微迟到,不是编造的事。

lim⁡t→∞dist(Rt,St)=0\displaystyle \lim_{t \to \infty} \mathrm{dist}(R_t, S_t) = 0
读模型与写模型之间的“距离”,随时间推移趋近于零。注意是趋近,不是瞬间相等。
  1. 距离函数dist 度量读模型 R_t 与写模型 S_t 当前状态的差距。
    dist(Rt,St)\mathrm{dist}(R_t, S_t)
  2. 时间极限t 趋向无穷时,只要事件不丢失,差距的极限为零。
    lim⁡t→∞\lim_{t \to \infty}
  3. 收敛含义系统保证读模型最终追平写模型,但不保证某一瞬间追平。
    dist(Rt,St)=0\mathrm{dist}(R_t, S_t) = 0

同步与重放:影院调度与断点续播

影院会根据影片热度安排上映策略:首映夜也许用卫星“实时直播”(同步投影),日常则靠实体拷贝配送(异步投影)。放映员还要在每本胶片上做标记,一旦停电,换好灯泡后从标记处继续放——这就是检查点(checkpoint)的妙处。

投影器的同步策略分为两类:同步投影在命令事务内同时更新读模型,读侧立即可见,但写延迟增加,适合极小规模的低延迟场景;异步投影让事件发布后由投影器在后台消费,读写彻底解耦,是主流选择。异步模式下,消息中间件的持久化订阅(Persistent Subscription)会为每个投影器记录消费位置。投影器崩溃重启后,从断点位置继续消费,而不是从头再来。

放映员在换片时会记住“放到第几本盒”。哪怕放映机烧了灯泡,换好之后接着那盒继续放——不会傻到把整卷胶片从头再放一遍。

pos=max⁡{i∣ei applied}\displaystyle pos = \max\{ i \mid e_i \ \text{applied}\}
投影器的位置 pos,就是它已经应用到的最大事件序号。这个数字是所有容错机制的锚点。
  1. 最大序号pos 取所有“已应用”事件中序号最大的那个。
    max⁡{i}\max\{ i \}
  2. 已应用条件e_i applied 表示第 i 个事件已经被投影器成功处理。
    ei appliede_i \ \text{applied}
  3. 断点续传从 pos + 1 继续处理,配合快照可避免全量重放。
    pos→pos+1pos \rightarrow pos+1

工程落地实践:从单厅影院到院线集团

从单厅影院到院线集团,并不是每个厅都需要上 IMAX。爆米花柜台用普通 POS 机就好,旗舰厅才值得上 CQRS——落地要看场景。

CQRS 适合:读多写少、查询形态复杂、报表分析拖垮交易库、需要审计回放的系统。不适合:简单 CRUD、对强一致要求苛刻且无法接受异步延迟的系统。它与 Event Sourcing(事件溯源,简称 ES)是正交概念——CQRS 可以不依赖 ES,直接命令后更新读库;ES 也可以不配 CQRS。两者搭配最顺:ES 提供不可变事件流,CQRS 把事件流投影成多样读模型。此外,监控的核心指标是投影延迟(Lag):最新事件序号减去投影器已处理位置,这个差值就是运维大屏上最该跳动的数字。

院线集团的中控室墙上挂着所有影厅的放映状态大屏,投影延迟就是那块跳动的数字——它告诉你哪块银幕掉队了,以及整个放映体系是否健康。

Lag=Emax⁡−pos\displaystyle \mathrm{Lag} = E_{\max} - pos
投影延迟 = 最新事件序号 - 投影器位置。它直观反映了读模型落后了多少。
  1. 最新事件E_max 是事件流中最新事件的全局序号。
    Emax⁡E_{\max}
  2. 投影位置pos 是投影器已处理到的位置。
    pospos
  3. 延迟告警Lag 超过阈值时,说明读模型掉队,需要排查投影器。
    Lag=Emax⁡−pos\mathrm{Lag} = E_{\max} - pos

AI 应用场景

电商订单系统:前台下单与后厨查询分离

阶段:写模型设计 + 事件投影

解决的问题:订单查询拖垮交易库。用户下单后,订单列表、详情、运营报表都直接查 MySQL,慢查询和事务锁互相抢占资源。CQRS 把下单命令与查询视图分离,查询侧走专有读模型,事务库负载大幅下降。

具体案例:某电商平台高峰期订单写入约每秒 1000 笔,但查询 QPS 是写入的 20 倍。改造后,下单命令写入订单库并发布 OrderPlaced 事件,投影器把订单数据同步到 Elasticsearch 索引;用户订单页只查 ES,事务库查询压力下降 70% 以上。

银行账户流水与余额视图

阶段:事件存储 + 读模型重放

解决的问题:传统银行系统把余额直接存成“最终值”,一旦出错难以追溯。CQRS + ES 将所有存取款作为事件追加,余额只是一个投影出来的读模型,任何时刻都可以重放全部流水重建余额。

具体案例:银行核心系统的交易明细表记录每笔 Deposited / Withdrawn 事件,余额视图和月度账单视图分别由两个投影器订阅。某次数据事故中,运维从事件流重放重建了所有账户余额,找回了被误更新的数据——就像放映员用母带重新投映了一遍。

在线学习平台的进度报表

阶段:行为事件投影到分析视图

解决的问题:学习行为是典型的写多读杂场景,用户观看视频产生海量行为事件,而“课程完成度”“学习时长排行榜”等报表需要聚合计算。CQRS 让行为命令流式写入事件总线,投影器实时聚合出统计视图。

具体案例:某在线教育平台把 PlayStarted、VideoCompleted 等事件写入 Kafka,投影器消费事件并更新 Redis 中的课程进度和 MySQL 中的学习报表。运营大屏看到的实时数据,全部来自事件投影而非直接查业务库。

Python 演示

python81 行
# -*- coding: utf-8 -*-"""CQRS 读写分离与事件投影的最小演示"""  class BankAccount:    """写模型:聚合根"""     def __init__(self, account_id):        self.account_id = account_id        self.balance = 0        self.handled = 0     def apply(self, event):        if event["type"] == "deposited":            self.balance += event["amount"]        elif event["type"] == "withdrawn":            self.balance -= event["amount"]        self.handled += 1     def handle(self, command, event_store):        if command["op"] == "deposit" and command["amount"] > 0:            event = {"type": "deposited", "account": self.account_id,                     "amount": command["amount"]}            event_store.append(event)            self.apply(event)        elif command["op"] == "withdraw" and command["amount"] <= self.balance:            event = {"type": "withdrawn", "account": self.account_id,                     "amount": command["amount"]}            event_store.append(event)            self.apply(event)        else:            raise ValueError("invalid command")  class AccountProjection:    """投影器:把事件流变成读模型"""     def __init__(self):        self._position = 0        self.views = {}     def _load(self, account_id):        if account_id not in self.views:            self.views[account_id] = {"balance": 0, "count": 0}        return self.views[account_id]     def project(self, event):        view = self._load(event["account"])        if event["type"] == "deposited":            view["balance"] += event["amount"]        elif event["type"] == "withdrawn":            view["balance"] -= event["amount"]        view["count"] += 1        self._position += 1     def replay(self, event_store):        self.views.clear()        self._position = 0        for event in event_store:            self.project(event)     @property    def position(self):        return self._position  def main():    event_store = []    account = BankAccount("acc-001")    account.handle({"op": "deposit", "amount": 1000}, event_store)    account.handle({"op": "withdraw", "amount": 300}, event_store)     projection = AccountProjection()    print("命令已提交但尚未投影", projection.views)    projection.replay(event_store)    print("投影完成后的读模型", projection.views["acc-001"])    print("投影器位置", projection.position)  if __name__ == "__main__":    main()
目的:用 100 行不到的 Python 演示 CQRS 的核心链路——命令产生事件、事件追加到存储、投影器重放事件生成读模型。
  1. event_store.append(event)把事件追加到事件存储,而不是直接覆盖余额字段,这就是“记录事实”。
  2. self.apply(event)聚合通过 apply 事件恢复自身状态,这是 Event Sourcing 的“从事件重建状态”。
  3. projection.replay(event_store)投影器全量重放事件流,重建读模型视图。
  4. view["balance"] += event["amount"]投影函数把单个事件转成对读模型的一次更新。
  5. self._position += 1投影器记录已处理位置,支撑断点续传与延迟监控。

常见误区

误区“我看到的教程都是 CQRS 和 Event Sourcing 一起讲的,那我不用 ES 是不是就不算 CQRS?”

正解CQRS 与 Event Sourcing 是两个正交概念。CQRS 只规定读写两侧模型分离;ES 规定用事件流作为持久化方式。你完全可以命令直接更新业务表、然后同步更新读模型,这也是 CQRS。就像电影院可以拍完胶片再投影,也可以“现场直播”——两者不强制捆绑。

为何常见:Greg Young 当年常把二者放在一起讲,社区教程也习惯捆绑演示,导致“CQRS 离不开 ES”的错觉。

误区“数据库主从分离不就是读写分离吗?我配一个主库写、从库读,就是 CQRS 了吧?”

正解主从复制是把同一份数据复制到多台数据库,读的依然是同一个逻辑模型。CQRS 在应用层把命令模型和查询模型拆成不同的类、不同的接口、不同的存储形态。读模型是对事件流的投影物化结果,而不是主库的物理副本。

为何常见:两者都叫“读写分离”,名字相近但层级完全不同,初学者极易混淆。

误区“反正系统是最终一致的,那读模型查到旧值也没关系,过一会儿它就自己好了。”

正解最终一致承诺的是“延迟窗口内读到旧数据,而不是错误数据”。银幕上晚几分钟出现的是“刚才真实发生的事”,不是篡改的剧情。投影仍然需要幂等、去重、对账;对支付结果等关键业务,还要额外做读己之写补偿。

为何常见:把“异步”与“不可靠”画了等号,忽略了 CQRS 背后严格的容错设计。

误区“聚合只是一个类而已,我可以在一个命令里同时改订单聚合和用户聚合,反正都是一次事务。”

正解聚合的意义正是“一致性边界”——一次命令事务只允许更新一个聚合,保证聚合内部的不变量。跨聚合的状态协调应该通过事件驱动的最终一致完成,而不是靠一个大事务强行耦合。否则聚合边界失去意义,CQRS 的扩展性也荡然无存。

为何常见:写惯了传统事务型代码的人,习惯把多个实体塞进一个大事务里,忽略了领域建模的边界约束。

面试题

Q1. CQRS 全称中 C 与 Q 分别是什么词?

单选题 · easy

  • A.Cache 与 Queue
  • B.Command 与 Query← 正确答案
  • C.Create 与 Quality
  • D.Connect 与 Quantity

答案:B。CQRS 是 Command Query Responsibility Segregation 的缩写。C 代表命令(写意图),Q 代表查询(读数据)。Cache 与 Queue 张冠李戴,Create 和 Connect 都不属于该缩写。

Q2. CQS 与 CQRS 的本质差异是什么?

单选题 · easy

  • A.CQS 在方法级别规范读写← 正确答案
  • B.CQS 在架构级别强制分离
  • C.CQRS 禁止任何查询操作
  • D.CQRS 只适用于单体应用

答案:A。CQS 由 Bertrand Meyer 提出,要求一个方法要么是命令(改状态)要么是查询(读状态),不能两者兼得。CQRS 把这一思想从方法级扩展到架构级,让读写使用完全不同的模型。CQRS 并不禁止查询,也不限于单体。

Q3. 写命令成功的事务标志是什么?

单选题 · mid

  • A.读模型立即更新完毕
  • B.业务校验通过且事件持久化← 正确答案
  • C.所有查询全部返回新值
  • D.界面提示用户操作成功

答案:B。命令端的成功标志是业务规则通过校验、事件被持久化写入事件存储。读模型是否立即更新不属于命令端的职责范围,界面提示也只是表象。CQRS 中命令与查询的时延被有意解耦。

Q4. 下列哪些属于事件投影器的职责?

多选题 · mid

  • A.订阅事件流并更新读模型← 正确答案
  • B.维护已投影的进度位置← 正确答案
  • C.校验命令的业务规则
  • D.支持从快照重放重建视图← 正确答案

答案:ABD。投影器负责消费事件、更新读模型、记录位置、重放重建。业务规则校验属于命令端和聚合的职责,投影器不应该也不需要有业务决策能力——它只是忠实的放映员。

Q5. 读模型采用反规范化的主要目的是什么?

单选题 · mid

  • A.减少查询时的 JOIN 提高性能← 正确答案
  • B.降低数据库的存储成本
  • C.增强数据的保密性
  • D.消除所有冗余数据

答案:A。反规范化是刻意把关联数据冗余到一张宽表或一个文档里,让查询不必现场 JOIN 多张表,从而显著降低读延迟。它不省存储、不加密,也恰恰是“保留冗余”而非消除冗余。

Q6. 关于 CQRS 的最终一致性,哪种说法正确?

单选题 · mid

  • A.读模型将永远落后于写模型
  • B.延迟窗口内读到的是旧数据而非错误数据← 正确答案
  • C.读写两侧数据完全互不相关
  • D.只要最终一致就不需要幂等

答案:B。最终一致性承诺读模型会收敛到写模型状态。延迟窗口内读到的是“刚刚发生过的事实”(旧数据),不是编造的错误数据。投影依然需要幂等保证,避免重放重复计数。

Q7. 投影器重启后需要具备哪些能力才能保证恢复?

多选题 · hard

  • A.从 checkpoint 位置继续投影← 正确答案
  • B.对重复事件做幂等去重← 正确答案
  • C.每次重放都重新定义业务规则
  • D.必要时从快照重放历史事件← 正确答案

答案:ABD。投影器崩溃恢复依赖三件事:持久化的检查点位置、事件的幂等处理、快照加速重建。业务规则属于命令端,重放事件是“重新投影”而非“重新定义规则”。

Q8. 关于聚合边界,下列理解正确的是?

单选题 · hard

  • A.一个命令事务可以同时更新多个聚合
  • B.聚合是保证业务不变量的一致性边界← 正确答案
  • C.聚合内部不允许存在任何状态
  • D.读模型必须与聚合结构完全一致

答案:B。聚合是领域驱动设计中的一致性边界,一次事务只更新一个聚合,保证边界内的不变量。跨聚合的协作应通过事件实现最终一致。读模型是投影视图,不必与聚合结构一致。

Q9. CQRS 与 Event Sourcing(事件溯源,ES)的关系是?

单选题 · hard

  • A.CQRS 必须依赖 ES 才能使用
  • B.两者正交,可以独立或组合使用← 正确答案
  • C.ES 必须搭配 CQRS 才有效果
  • D.CQRS 和 ES 是同义词

答案:B。CQRS 关心读写模型分离,ES 关心如何持久化状态变化。两者可以独立使用,也可以组合——组合时 ES 为 CQRS 提供优质事件流,CQRS 为 ES 提供投影出口,但互相不强制。

Q10. 以下哪些场景不适合直接引入 CQRS?

多选题 · extreme

  • A.简单 CRUD 业务系统← 正确答案
  • B.强一致要求拒绝异步延迟← 正确答案
  • C.读多写少且查询形态复杂
  • D.团队缺乏事件建模经验← 正确答案

答案:ABD。简单 CRUD 引入 CQRS 是过度设计;强一致场景无法接受读侧延迟;团队对事件建模不熟时冒然引入会陷入复杂度泥潭。读多写少、查询复杂恰恰是 CQRS 的典型适用场景,不应排除。