城东有家百年老店“聚宝斋”,掌柜换了三代,账房先生却始终守着一本流水账。规矩很特别:绝不涂改。记错了,就补一条“冲正”;每笔收支,都要写明日期、事由、经手人。月底对账时,先生把流水账从头翻到尾,算出当天余额。有一回伙计碰翻墨汁污了两页,先生不慌不忙,拿出上月底的“小汇总本”,从那里继续往后翻,很快恢复了账目。 老店的记账方式,正是本文主角——Event Sourcing(事件溯源)。核心思想朴素:不保存“现在余额是多少”,而是保存“每一步发生了什么”。 当前状态只是历史事件的累计结果,事件本身才是唯一事实。为了加速恢复,先生备了“小汇总本”(快照);为了给掌柜看不同角度的账,先生又整理了“利润表”“欠款表”这类专门账册(投影)。这套思路应用于高并发消息与异步系统,能构建可审计、可回溯、高可用的核心业务。
掌握 Event Sourcing,就能在高并发订单、金融结算、AI 行为日志等场景中,设计出可随时回溯任意时点状态的系统,获得审计级可信度与故障自愈能力,是资深架构师的核心武器。
可视化演示
观察不可变事件按序追加到事件存储,通过重放事件重建状态,以及快照优化降低重放开销的过程,可切换三种模式。
本节配有交互动效(进入视口后自动加载,约 3KB)。
事件追加:append-only 每次状态变更记录为不可变事件,无法修改 事件按时间顺序追加到事件存储 状态 = Σ(事件序列) 天然支持审计与时间旅行。
核心概念
学徒第一天进账房,师傅教他的第一句话是:“记错不要紧,要紧的是不许改账。” 学徒想不通,明明擦掉重写更快,何必多写一条?师傅指着门外说:“要是哪天掌柜问起上个月的哪笔账,你擦掉了,拿什么给他看?” 学徒这才知道,这家老店的账本,每一页都只能往后面添,从不能往回改。
这正对应两种数据思维。传统 CRUD(增删改查,即 Create/Read/Update/Delete)像黑板,每次更新都擦掉旧值写新值,查询时只有一个“最新结果”;Event Sourcing(事件溯源)则像流水账,把每次变化(事件)追加到账本末尾,永不抹去。事件就是“已经发生的事实”,一旦写入便不可变。系统不断追加事件,形成一条长长的事件流(Event Stream),当前状态不单独保存,而是随时可以由事件流推导——这个过程叫状态重建(Rebuild)。
在工程上,事件是一个结构化记录,描述“什么时间、哪笔交易、发生了哪种变化”。把从初始状态到当前状态的全部事件依次累加,就能得到当前状态,这可以用一个简洁的式子表达:
所以学徒终于明白:擦掉重写,其实是在和时间对抗;而流水账,是和事实同行。每一条记录都不撒谎,每一个状态都有来路。哪怕掌柜问起半年前的一笔旧账,他也能翻出来,一笔笔讲清楚来龙去脉。这就是事件溯源最朴素的底气——事实一旦被记住,就永远不会消失。
- 当前状态 S_now等号左边表示业务当下的累计结果,比如账户余额。
- 初始状态 S_0业务开始时的基线,比如开户存入的第一笔本金。
- 求和符号 Σ把每个事件带来的变化量逐一累加起来,相当于把流水账逐页相加。
- 事件增量 ΔE_i第 i 个事件带来的变化量,比如“存入 100 元”就是 +100。
事件存储设计
师傅的第二课是记账格式。“光记数字不行,还得记清楚这笔钱是哪来的。日期、事由、经手人、凭证号,一个都不能少。” 学徒发现,正是这些要素,让每一笔账在几十年后依然说得清来龙去脉。如果只记一个“收到 100 元”,第二年再翻,谁都说不清这钱来自哪里。
事件也需要类似的结构化字段。一个事件通常包含:事件 ID、事件类型(如“存款”或“取款”)、载荷(金额、账户等业务数据)、版本号(事件结构的模式版本)和时间戳。事件类型就是账目的“事由”标签,载荷相当于“金额和经手人”。这样设计,每个事件都自包含、可独立解读。
存储设计上,事件要落到专门的“事件存储”(Event Store)中,本质是 append-only 的日志结构:只允许顺序追加,不允许随机修改。为了支撑高并发写,底层常选择追加性能优异的存储引擎;同时要让不同时期的代码都能读懂老事件,需要做好序列化兼容与模式演进(Schema Evolution),这就像账房规定“数字一律用大写、日期一律用干支”,后人才能看懂旧账。
师傅合上账本时会说:“格式定好,十年后翻开都认得。” 事件存储的版本管理,就是让系统在十次重构之后,依然读得懂最早的那条记录。对高并发系统来说,多花一点心思设计事件格式,能省下未来无数排查问题的夜晚。
- id 事件编号全局唯一标识,对应账本上的“凭证号”。
- type 事件类型说明“发生了什么”,比如存款、取款、转账。
- payload 载荷携带业务细节,比如金额、账户、对手方。
- version 版本号标识事件结构的模式版本,用于兼容旧数据。
- timestamp 时间戳记录事件发生时刻,让事件排序可复现。
溯源与重放
账房里有个口头规矩:掌柜说“记一笔”,伙计先喊一嗓子,师傅才落笔。喊声不等于账目——喊声可以被拒绝(比如掌柜说记错了、余额不足),落笔的才算数。师傅敲敲账本:“喊了不算,写了才算。” 这个微小的区别,正是事件溯源的心脏。
在系统里,“喊一嗓子”就是命令(Command)——表示“想要发生的事”,携带用户意图,可能被业务规则校验拒绝;“落笔”就是事件(Event)——表示“已经发生的事”,是不可否认的事实。命令通过校验后,被转成对应的事件写入事件流。写入之后,系统把事件“应用”(apply)到内存中的状态对象上,得到一个更新的状态。整个过程天然支持重放(Replay):从初始状态出发,把事件流再次左折叠(fold)一遍,就能重建任意时刻的状态。
因为事件是事实,重放不会改变事件本身,这个性质叫“无副作用”:无论重放十次还是一百次,只要事件相同,得到的状态就相同。用公式表达,第 t 个时刻的状态就是从初始状态开始,对事件序列做一次折叠:
这就像月底对账:账房先生从去年年初的“结余”开始,一页一页往后加,把每个动作重演一遍。重放不会让已经发生的交易消失,只会让账目越来越清楚。对系统而言,“重放”不是为了怀旧,而是为了在需要时,找回任意时刻的真相。
- fold 折叠把序列中的元素逐个累积成一个结果,相当于“逐页翻账本”。
- apply 应用函数把一个事件作用到当前状态上,比如把“取款 50 元”作用到余额上。
- S_0 起始状态折叠的初始累积值,也就是开户时的余额。
- 事件序列 [E1..Et]从第 1 个到第 t 个事件的完整列表,按时间排序。
快照优化
流水账记了十几年,对一次账要翻半天。师傅想了个办法:每个月月底,把截至当天的余额抄到一张“小汇总页”上,夹在账本里。下回对账,先看汇总页,再从那里继续往后翻。学徒恍然大悟:“原来不用每次都从第一页开始。” 师傅笑了:“账越厚,越要靠汇总页指路。”
这就是快照(Snapshot)的朴素原型。快照是“某个时点的状态备份”,记录了事件流在第 k 个事件之后的状态。有了快照,重建状态时就不必从 S₀ 重放全部事件,只需加载最近一次快照,再重放快照之后的新事件。快照本身也是不可变的,通常周期性生成,可以存储在独立的存储介质中,以便快速加载。
快照的关键是触发策略:可以按事件数量(每 1000 条)、按时间(每小时)、按业务周期(每日结算)触发。快照越新,恢复越快,但生成成本也越高;快照越旧,恢复越慢,但生成频率低、开销小。架构师需要在这两者之间做出权衡。恢复流程也一目了然:先加载快照状态 S_k,再顺序应用 k 之后的事件:
师傅给学徒总结:“汇总页是路上的驿站,不用从城门口重新出发。” 快照就是事件溯源世界里的驿站,让最遥远的起点变得不再可怕。在高并发系统里,快照策略设计得好不好,直接决定了故障恢复是“秒级”还是“小时级”。
- S_k 快照状态第 k 个事件之后的已保存余额,相当于“小汇总页”。
- E_{k+1} 第一个后续事件快照之后的下一笔交易,恢复工作从这里开始重放。
- 重放区间 [E_{k+1}, E_t]只需处理快照之后的事件,而不是全部历史。
投影与查询
掌柜经常要看不同的账:今天看“本月利润”,明天看“哪些客户还欠钱”。每月都翻一遍流水账太慢,师傅便提前把流水账整理成一本本专门账册:利润表、应收款表。这些账册不改变事实,只是换个角度组织事实。学徒问:“这不就是重新抄一遍吗?” 师傅说:“抄,但要抄得让掌柜一眼看得懂。”
在事件溯源中,这种“从事件流派生出的查询友好的视图”叫作投影(Projection)。投影消费事件流,按业务需要构造出专门的读模型(Read Model),比如“用户账户汇总”“订单状态表”“本月收入统计”。查询时直接读投影,不用重放所有事件,读性能大幅提升。投影既可以同步订阅事件(事件写入后立即更新),也可以异步批处理(定时批量刷新)。
当投影数据损坏,或者产品需要新增一个查询视角时,可以从头(或从某个快照)重新投影(Rebuild)。由于异步刷新,投影与事件源之间是最终一致性(Eventual Consistency)——事件刚写入时,投影可能还没更新,但经过极短时间后最终一致。用公式表示,投影就是一个把事件流转换为视图的函数:
师傅管这些专门账册叫“掌柜的眼镜”——账没变,但看账的人有了不同的视角。投影让事件流既能写得好,也能读得快。理解投影,就理解了 Event Sourcing 系统中“写一份事实,服务多种查询”的优雅之处。
- V 投影结果一个查询友好的读模型,比如“本月收入汇总表”。
- project 投影函数把事件流转换成一个或多个视图,必要时可由事件流重建。
- 事件流输入投影的唯一输入源,事件不变则投影可重复生成。
架构优势
有一年,聚宝斋遭了贼,柜上登记簿被翻得乱七八糟。掌柜慌了,师傅却从流水账里一笔笔算出了所有账目,还顺藤摸瓜找出了账款去向。掌柜问:“这账本怎么这么厉害?” 师傅答:“因为它记的是每一步,而不是最后一眼。” 这正是事件溯源最迷人的地方。
事件溯源的第一个优势是审计追踪(Audit Trail):每一件事都有完整凭证,谁能改、何时改、改了什么,全部有据可查,天然满足金融与合规要求。第二个优势是时点恢复(Point-in-time Recovery):想回到任何一天,只需把事件流重放到那一天。第三个优势是领域建模(Domain Modeling):状态机的每一步转移都有明确事件,特别适合落地领域驱动设计(DDD),聚合根可以把业务规则表达得清晰自然。
在高并发场景下,事件流以追加写为主,写入可以横向扩展,避开“同一行数据多次更新”的锁竞争;重放与投影还可以并行分摊到多台机器。这种读写分离、追加优先的架构,让事件溯源在高吞吐写密集场景中拥有独特的弹性。从成本角度看,全量重放与快照加增量恢复的代价差异可以用复杂度表示:
师傅的流水账之所以值钱,不是因为它写得漂亮,而是因为它“记得住每一个事实”。事件溯源让系统拥有同样的记忆力和抗毁性——这对追求高可用、强审计的架构师来说,是无法拒绝的理由。
- T_replay 全量重放耗时与事件总数 n 成正比,n 越大越慢。
- c 单事件应用代价处理一个事件所需的平均时间,比如一次余额累加。
- T_snapshot 快照恢复耗时加载快照耗时 s 加上快照后 k 个事件的重放耗时。
- 对比结论当 k 远小于 n 时,快照恢复显著胜出。
挑战与对策
流水账也有麻烦:记账格式变过三次,早年“大洋”和今天的“元”怎么换算?遇到坏账是划掉还是补记?师傅的经验是:格式可以变,但旧账不能废,要有一套“换算表”。学徒把这些换算规则抄成小册子,每次看旧账前都要翻一翻。
事件溯源最大的挑战之一是事件演化(Event Evolution)。业务规则变了,旧事件的结构和新代码不一致。对策包括:事件带版本号,应用层按版本做兼容读取;用 upcast(读取时转换)机制,在新代码读取旧事件时将其升级为新结构;保留历史版本的解析逻辑。最重要的是:绝不能改写历史事件。另一个挑战是最终一致性与运维复杂度:投影有短暂延迟,不适配强实时聚合查询;事件存储需要额外的生命周期管理,包括保留策略、归档、压缩和清理。
团队还需要从“覆盖式思维”切换到“追加式思维”,这是最隐蔽的学习曲线。很多工程师刚接触时,总想把错误事件改掉,这与事件溯源的根基相悖。事件兼容读取可以用一个分段函数表达:
师傅常说:“规矩改了,老账的算法不能丢。” 事件演化管理的核心,就是让系统在演进中始终读得懂历史。能在十年后还说得清每一笔账的来龙去脉,这才是“可溯源”的真正含义。
- upcast_v 升级函数根据目标版本 v 将旧事件转换为新结构。
- E_t 原始事件历史中真实发生过的事件,始终保持不可变。
- 版本匹配判断若事件版本已符合要求,直接使用;否则触发转换。
- transform 转换器负责字段映射,比如把“大洋”换算成“元”。
实战案例
聚宝斋的故事还没完。有一年新来的伙计差点把“应付款”当成“应收款”入账,账目全乱了。师傅靠流水账和汇总页,半天内恢复得一清二楚。这个场景,正是今天很多线上系统的日常——数据出错了,能不能快速找回真相?
以电商订单为例:订单状态从“已创建”到“已支付”到“已发货”,每一步都是一个事件。如果使用覆盖式存储,状态被误更新后很难追责;改用事件溯源后,每次状态迁移都落为事件。客服定位“订单为什么卡住”时,直接回放该订单的事件流即可。金融系统的转账、支付更是如此——交易流水本身就是事件流,审计、对账、冲正都依托事件展开。
在高可用设计上,事件流天然适合多副本复制。主副本接受追加写,从副本同步事件并异步构建投影;主库故障时,从副本加载最近快照并重放事件,即可快速恢复服务。这套以事件溯源为基础的高可用与故障复盘框架,让恢复时间变得可预估、可控。故障恢复时间(RTO,即 Recovery Time Objective)可以粗略估算为:
回到账房,师傅把账本递给新伙计:“记住,这笔账今天写下来,明天、明年、几十年后,都还能说得清。” 事件溯源给系统的,正是这种跨越时间的确定性。无论是金融账务、电商订单还是 AI 日志,只要事实被完整记录,系统就永远有路可退。
- T_load 快照加载时间从存储读取最近快照的耗时。
- N_since 快照后事件数故障发生时,快照之后新增的事件数量。
- T_apply 单事件应用耗时重建状态时处理单个事件的平均耗时。
- 总恢复耗时加载快照的时间加上增量重放所有未处理事件的时间。
AI 应用场景
金融反欺诈:从事件流重建资金网络
阶段:特征工程与数据管道
解决的问题:反欺诈模型需要“某笔资金在 3 分钟内经过多少账户”这类时序证据。传统覆盖式存储只留下最终余额,无法回答“钱刚从哪来”。事件溯源保留下每一笔转账事件,使模型可任意回放时间窗口,重建资金流转图谱,就像账房先生能随时翻出任意一天的流水来还原资金去向。
具体案例:某支付平台把所有转账写成 TransferInitiated、TransferCompleted 事件,风控系统定时投影出“多层转账圈”特征,输入图神经网络识别洗钱团伙,准确率提升约 21%。
推荐系统:用户行为流的投影画像
阶段:数据处理与模型训练
解决的问题:推荐模型依赖用户完整行为序列,但行为流水线经常需要重跑。事件溯源把点击、曝光、播放、购买都存为不可变事件,用户画像变成可随时重建的投影,训练与线上服务共用一致的读模型。
具体案例:某音乐 App 将每次播放、快进、收藏写成事件,每日凌晨从事件流投影出“用户偏好矩阵”,召回模型直接读取。既省去解析多份日志的麻烦,又能在画像异常后一键重建。
LLM Agent 会话审计:重放每一轮对话
阶段:模型服务与合规审计
解决的问题:大模型客服出错时,需要精确重放“用户说了什么、模型答了什么、调用了哪些工具”。传统日志零散易丢,事件溯源把每轮交互建模为 UserMessageReceived、ToolInvoked、ReplyGenerated 等事件,完整记录决策链路。
具体案例:某银行客服 Agent 上线后,一次误答复引发客诉。工程师通过事件流重放该会话,定位到工具返回字段解析错误,并基于事件流补充了回归用例,全程 40 分钟完成复盘。
Python 演示
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""Event Sourcing 极简演示: 银行账户流水账"""from dataclasses import dataclassfrom typing import List, Dict @dataclassclass Event: kind: str amount: float version: int = 1 class Account: def __init__(self) -> None: self.events: List[Event] = [] def apply(self, event: Event) -> None: self.events.append(event) def _apply_to(self, e: Event, total: float) -> float: if e.kind == "deposit": total += e.amount elif e.kind == "withdraw": total -= e.amount return total def balance(self) -> float: """重放全部事件, 重建当前余额""" total = 0.0 for e in self.events: total = self._apply_to(e, total) return total def replay_from_snapshot(self, snapshot_balance: float, start_index: int) -> float: """从快照余额 + 后续事件重建状态""" total = snapshot_balance for e in self.events[start_index:]: total = self._apply_to(e, total) return total def projection_by_kind(self) -> Dict[str, float]: """投影: 按事件类型汇总, 构建读模型""" proj: Dict[str, float] = {} for e in self.events: proj[e.kind] = proj.get(e.kind, 0.0) + e.amount return proj def main() -> None: events = [ Event("deposit", 100.0), Event("withdraw", 30.0), Event("deposit", 200.0), Event("withdraw", 50.0), Event("deposit", 50.0), ] account = Account() for e in events: account.apply(e) total = account.balance() print("重放全部事件后余额 =", total) snapshot_index = 2 snapshot_balance = account.replay_from_snapshot(0.0, 0) restored = account.replay_from_snapshot(snapshot_balance, snapshot_index) print("从第", snapshot_index, "个事件后的快照恢复, 余额 =", restored) proj = account.projection_by_kind() print("投影(按事件类型汇总) =", proj) if __name__ == "__main__": main()
self.events: List[Event] = []事件存储的本质就是一个只追加的列表,没有删除与修改入口。account.apply(e)写入事件的唯一方式,每一步都记录事实,而不是覆盖余额。total = self._apply_to(e, total)这就是重放:把所有事件逐个“应用”到状态上,折叠出当前余额。snapshot_balance = account.replay_from_snapshot(0.0, 0)先生成快照基准,模拟账房先生的“小汇总页”。proj[e.kind] = proj.get(e.kind, 0.0) + e.amount投影:从事件流派生按类型汇总的读模型,查询时无需重放。
常见误区
误区我觉得事件溯源就是多打日志,把每次操作 print 出来保存,以后排查问题还能翻出来看看,和 log 没本质区别。
正解日志是给排查用的辅助信息,事件流是业务的事实源。事件是结构化、不可变、可回放的领域事实,有类型、载荷、版本约束;而普通日志格式随意、可能截断、不保证顺序和完整性。就像流水账是正式账册,伙计随手写的便条只是便条,两者地位完全不同。
为何常见:二者都有“追加写”的外形,非常容易混淆,很多入门文章也喜欢拿日志打比方。
误区发现某个事件金额写错了,直接 UPDATE 改掉或者 DELETE 删除,让账目“看起来正确”不就行了?
正解事件不可变是根基,修改历史等于伪造事实。要修复错误,应新增“补偿事件”,比如记一笔冲正。新事件进入事件流尾部,保留因果与审计链。否则已有的投影、下游订阅、合规审计全部错乱,系统的可信度瞬间归零。
为何常见:CRUD 的覆盖写心智根深蒂固,习惯“改了就是对了”,很难接受“错了也要保留错误痕迹”。
误区为了查询快,我把对象的完整最新状态也放到事件里,每条事件都带一份全量快照,这样重放一次就能拿结果。
正解事件表达“发生了什么变化”,而不是“结果是什么”。若把最新状态塞进事件,就丢失了每次变化的因果链,无法回答“这一刻到底发生了什么”。结果应由重放推导;全量状态应放到快照或投影中,不该占用事件语义。
为何常见:“事件”和“状态”两个词概念重叠,直觉上容易把事件当成“保存状态的另一种格式”。
误区每次查询都要把所有事件重放一遍,事件多了肯定卡死,生产环境根本没法用,性能不如传统数据库。
正解写路径是顺序追加,吞吐往往很高;读路径用快照、投影、索引优化,重放不必全量。未加优化就全量重放当然慢,但那是工程方案问题,不是模式缺陷。账房先生也不会每次对账都从建店第一天翻起,他手里有“小汇总页”。
为何常见:只用最朴素的全量重放来估算性能,忽略了快照与投影这两大优化手段。
误区听教程说事件溯源通常和 CQRS 一起用,于是我决定先买一套 Event Store 中间件,把 CQRS 拆好再动手。
正解ES 是建模与存储策略,CQRS 是读写分离的架构风格,二者可以独立演进。初期用一张追加表加消息订阅也能跑通 ES;是否引入专用 Event Store 产品取决于规模与团队能力。先理解事实源,再谈框架。
为何常见:教程常把 ES、CQRS、Event Store 捆绑讲解,造成“缺一不可”的错觉。
面试题
Q1. Event Sourcing 的核心心法是什么?
- A.定期备份业务数据
- B.用事件序列表达状态变迁← 正确答案
- C.仅把最终状态存入内存
- D.依赖分布式事务保证
答案:B。事件溯源的核心心法是用不可变事件序列表达状态变迁,就像账房先生只记流水不动余额。定期备份是快照思路,只存最终状态是 CRUD 思路,分布式事务又是另一个领域,都不是事件溯源的出发点。
Q2. 关于事件的不可变性,下列哪项正确?
- A.出错时允许删除事件
- B.事件可以覆盖和修改
- C.事件只能追加且永不修改← 正确答案
- D.事件需要定期压缩清理
答案:C。事件一旦写入便是历史事实,只能追加、不能修改删除。删除事件好比撕掉账页,会让后续重放和审计失真;“定期压缩”可以淘汰旧快照,但不能动事件本身。
Q3. 快照的价值体现在哪些方面?(多选)
- A.减少事件重放的工作量← 正确答案
- B.加速系统状态的快速恢复← 正确答案
- C.取代事件流成为唯一事实源
- D.丢弃历史事件与审计信息
答案:AB。快照是“小汇总页”,作用是减少重放量、加速恢复;它只是中间产物,不能取代事件流这一事实源,也不能借快照丢弃审计所需的历史事件。
Q4. 关于“命令”与“事件”的关系,正确的是?
- A.命令是意图,事件是事实← 正确答案
- B.命令与事件完全相同
- C.事件先于命令产生
- D.命令用于查询读模型
答案:A。命令代表“想要发生的事”,会被校验拒绝;事件代表“已经发生的事”。就像伙计喊“记一笔”是命令,师傅落笔才是事实;命令不可能先于事件产生,也不用于查询读模型。
Q5. 投影(Projection)主要负责什么?
- A.持久化原始事件
- B.生成查询友好的读模型← 正确答案
- C.修改历史事件内容
- D.负责执行命令处理
答案:B。投影从事件流派生查询友好的读模型,相当于从流水账整理出利润表。保存原始事件是事件存储的工作,投影不修改历史,也不处理命令,职责非常单一。
Q6. 事件存储设计应关注哪些方面?(多选)
- A.序列化格式的兼容演进← 正确答案
- B.事件类型的版本化管理← 正确答案
- C.把最终状态冗余进事件
- D.支持追加式高并发写入← 正确答案
答案:ABD。事件存储要保证新老代码都能读旧事件,所以序列化兼容和版本管理必须设计好;事件本质是 append-only,需支持高并发追加写。把最终状态冗余进事件会掩盖事件语义,是典型的不良设计。
Q7. 从快照 S_k 重建状态,时间开销近似为?
- A.重放全部历史事件
- B.加载快照加重放后续事件← 正确答案
- C.快照数量乘总事件数
- D.仅需从磁盘加载快照
答案:B。从快照恢复时,只需要读入快照状态再重放 k 之后的少量事件,时间约为“加载快照 + 后续事件数 × 单事件耗时”。这正是快照优化的核心价值:不用重放全部历史。
Q8. 以下哪种场景最不适合使用事件溯源?
- A.需要完整审计轨迹的场景
- B.领域模型复杂的业务场景
- C.需要强实时聚合且极少审计← 正确答案
- D.需要高并发消息回放的场景
答案:C。事件溯源天然适合审计、复杂领域建模、消息回放;但如果业务要求极强实时聚合、几乎不需要审计,覆盖式存储加投影反而更简单,事件溯源的重放成本与最终一致性会成为负担。
Q9. 应对事件演化(Event Evolution)的合理策略有哪些?(多选)
- A.提升事件版本并兼容读取← 正确答案
- B.通过 upcast 实现读取时转换← 正确答案
- C.直接改写已发生的旧事件
- D.记录 schema 迁移脚本← 正确答案
答案:ABD。事件演化讲究“旧账不能废,算法不能丢”:用版本号标记、在读取时 upcast 转换、准备迁移脚本都是正路;改写历史事件会破坏事件流的时间线一致性与审计价值。
Q10. 事件溯源与 CQRS 的关系是?
- A.必须与应用强绑定使用
- B.可独立使用且常搭配← 正确答案
- C.两者完全相互排斥
- D.CQRS 可替代事件溯源
答案:B。事件溯源是写入侧的建模方式,CQRS 是读写分离的架构风格,二者经常搭配但可以独立演进。它们既不是绑定关系,也不互相排斥或替代,选型应看业务需要。