十、面试追问
以下问题从面试官视角设计,考察对事件溯源本质的理解和工程权衡能力。
Q1:事件溯源中,如果发现某个历史事件的数据是错误的(比如订单金额写错了),你怎么处理?
参考答案:事件一旦写入就是不可变的,不能直接修改。正确的做法是追加一个补偿事件(Correction Event),例如 OrderAmountCorrected,其中包含修正后的金额和修正原因。读取时会按时间顺序应用所有事件,补偿事件覆盖之前错误事件的效果。这保留了完整的审计轨迹——可以看到"谁、什么时候、为什么"修改了数据。如果错误事件影响到下游系统(如已发送的发票),还需要额外追加补偿操作。关键原则:从不删除或修改历史事件,从不重写历史,只追加真相。
Q2:Event Sourcing 和 Change Data Capture(CDC)都能捕获数据变更,它们有什么本质区别?什么时候用哪个?
参考答案:核心区别在于意图驱动(Intent)vs 状态驱动(State)。Event Sourcing 捕获的是业务意图——"用户下单了"、"订单已支付",事件携带业务语义,是领域模型的一部分。CDC 捕获的是数据库状态变更——"orders 表 status 列从 pending 变成 paid",它只是数据层面的副作用记录,没有业务语义。CDC 适合:已有 CRUD 系统的数据同步、审计日志补充、数据湖 ETL 管道。Event Sourcing 适合:需要完整审计、需要时间旅行查询、需要从事件重建任意历史状态、核心业务领域(如金融、电商订单)。两者可以组合使用:Event Sourcing 作为写入源,CDC 将投影表同步到数据仓库。
Q3:一个运行了三年的系统,某个聚合有 5 万个事件,全量回放需要 30 秒。你如何优化?
参考答案:优化策略按优先级排列:① 快照(Snapshot)——定期保存聚合的完整状态快照,重建时从最新快照加载,只回放快照之后的增量事件,30 秒降到毫秒级。② 按需重建——不是每次查询都重建,读模型通过 Projector 持续更新,只在异常恢复时才回放。③ 聚合拆分——如果单个聚合有 5 万个事件,很可能聚合边界设计过大,考虑拆分为更小的聚合(如订单拆分为 Order、OrderLine、Payment)。④ 事件归档——将超过一定时间的历史事件归档到冷存储,热存储只保留近期事件,快照覆盖旧数据。⑤ 并行回放——对于独立子聚合并行回放,但要注意因果一致性。⑥ 事件压缩——对于同类事件(如多次库存调整)合并为净效果事件,但需权衡审计完整性的损失。
Q4:事件溯源中的"事件"和消息队列中的"消息"是同一个概念吗?为什么?
参考答案:不是同一个概念,虽然它们经常在架构中配合使用。领域事件是业务事实的不可变记录,是 Event Store 中的数据——它回答"发生了什么",是真理的来源。消息队列中的消息是传输机制——它回答"如何通知其他系统",关注投递保证、顺序、重试。核心区别:① 事件存储是持久化的、不可变的、可查询的;消息队列通常是消费后删除的(Kafka 可配置保留),② 事件是事实陈述,消息是通知指令,③ 事件存储中的事件可以被任意多次回放,消息队列中的消息消费后通常不再可用。两者的关系:事件存储写入事件后,通过 Event Bus(通常是消息队列)发布给下游 Projector 或其他服务,这是存储优先、发布在后的模式,而非"先发消息再存"。
Q5:Event Sourcing 系统中,如何保证事件写入和外部系统(如发送短信通知)的一致性?
参考答案:这是事件溯源最棘手的工程问题之一。核心方案:① 最终一致性 + 补偿——事件写入成功后,异步发送短信,如果短信发送失败,记录失败事件并触发补偿(如人工介入、重试队列)。② Outbox 模式——将事件写入和"待发送通知"写入放在同一个数据库事务中,一个独立的 Relay 进程从 Outbox 表读取通知并发送,保证至少一次投递。③ 事件驱动 + 幂等消费——下游短信服务消费事件,发送前检查是否已发送过(幂等键),失败时重试或进入死信队列。④ Saga 编排——如果短信发送是业务事务的一部分,用 Saga 协调:发送命令 → 短信服务尝试发送 → 成功后发回确认事件,失败则触发补偿事件。⑤ 关键原则:接受最终一致性,用对账和补偿兜底,而不是强求两阶段提交式的强一致。
Q6:如果你的团队决定从 CRUD 架构迁移到 Event Sourcing,你会怎么规划迁移路径?
参考答案:① 选试点——在非核心、低风险领域先试(如用户操作日志、审计日志),积累经验后再推广。② 双写过渡——新事件写入 Event Store 的同时,保留对旧 CRUD 表的写入,两个系统并行运行一段时间。③ 数据迁移——对历史数据,从旧表推断事件流(如 status 变更记录、update_time 差异),但无法 100% 还原业务意图,标注"迁移事件"与"原生事件"区分。④ 读模型切换——先让新的读模型(Projection)与旧查询并行,对比验证无误后,逐步切流量。⑤ 团队培训——Event Sourcing 改变了团队的心智模型,需要培训事件建模、最终一致性思维、CQRS 模式。⑥ 基础设施准备——评估 Event Store 选型(专用 EventStoreDB vs PostgreSQL vs Kafka),建立快照、监控、重放工具链。⑦ 渐进式迁移——按聚合根逐个迁移,不要一次性全部切换。整个迁移周期通常是 6-12 个月,切忌一步到位。
Q7:在微服务架构中,一个 Saga 事务跨越多个聚合,事件溯源如何支持 Saga 的补偿操作?
参考答案:Event Sourcing 天然适配 Saga 模式。① 事件作为 Saga 的驱动——每个 Saga 步骤完成后产生一个事件,事件驱动下一步或触发补偿。② 补偿事件——当某个步骤失败,Saga 协调器追加补偿事件(如 OrderCancelled、PaymentRefunded),每个服务通过回放补偿事件来撤销自己的操作。③ 不可变回溯——因为事件不可变,Saga 可以回溯整个业务流程,精准定位失败点,只补偿已完成的操作,避免重复补偿。④ 事件溯源 + Saga 的具体实现:Saga 协调器监听事件流,维护 Saga 状态(用另一个事件流或状态机),当检测到失败事件时,按逆序追加补偿命令(Command)到对应聚合,聚合处理补偿命令并产生新的补偿事件。⑤ 关键设计点:补偿必须是幂等的(多次补偿事件不会重复扣款),需要 Saga 超时机制防止悬挂,补偿失败时进入人工处理队列。相比传统数据库 Saga,事件溯源的优势在于:完整的操作历史让补偿逻辑更清晰,不必担心"中间状态丢失",可以随时重放验证补偿是否正确执行。