领域事件与架构风格

共 20 题
📑 题目列表 20 题
#
★★★

1. Domain Event 的不可变性(immutability)中事件一旦发布即不可修改

为什么 Domain Event(领域事件)必须是不可变的?一旦发布即不可修改的含义是什么?

  • 领域事件的不可变性
  • 事件作为"已发生事实"的语义
  • 事件不可变与审计/重放的配合

Domain Event 描述"已经发生的事实"(past fact),一旦发布即不可修改,因此必须是不可变对象:所有字段在构造时由 final 固化,不提供 setter,事件对象在生命周期内不可变。原因:事件是历史的记录,若可修改,则事件不再代表"当时发生的事实",无法用于审计、重放、事件溯源与对账;事件可能被多个消费者订阅,若可修改会形成共享可变状态,一个消费者修改影响其他消费者。事件发布后,内容应固化,若要表达新状态应发布新事件。不可变事件可安全并发消费、可被持久化、可被重放。

事件不可变是"事件即事实"的体现。可变事件会破坏历史记录的准确性,并造成并发安全问题。设计上事件一律 final 字段 + 构造器/工厂创建,配合事件 ID 与时间戳,保证事实的完整与不可篡改。

#
★★★

2. Domain Event(领域事件)的发布时机中聚合状态变更后立即 vs 事务提交后

领域事件应在何时发布?聚合状态变更后立即发布还是事务提交后发布?两者有何区别?

  • 事件发布早晚的差异
  • 事务提交后发布的意义
  • 事务性发件箱

领域事件发布时机有两种选择:聚合状态变更后立即发布(同步、事务内)与事务提交后发布。立即发布的问题是:若后续事务回滚,事件已发出但业务状态未落库,消费者处理了"不存在的事实",造成不一致。因此推荐"事务提交后发布":事务内收集事件(聚合变更记录事件),事务提交成功后统一发布事件;若事务回滚,事件不发布。为保证不丢失,常用事务性发件箱(Outbox):事件与业务数据同库同事务写入 outbox 表,事务提交后由 relay 读取发布,保证"业务状态与事件"原子一致。事务提交后发布避免"假事件",且配合 Outbox 保证可靠投递。

发布时机核心是"事件要描述真实落库的事实"。事务内立即发布存在"回滚后事件仍在"的假事件风险。事务提交后发布(配合 Outbox)保证事件与状态一致,是可靠事件投递的标准做法。

#
★★★

3. Event Sourcing 中 Snapshot 策略(每 N 个事件、性能 vs 存储)

Event Sourcing 中的 Snapshot 策略是什么?每 N 个事件、性能 vs 存储如何权衡?

  • Snapshot(快照)的作用
  • 快照频率策略
  • 性能与存储的权衡

Event Sourcing 中,聚合状态由事件流重放得到,事件越多重放越慢、存储越大。Snapshot(快照)是定期保存聚合在某个时间点的状态,并结合事件流重建:加载时先取最近快照,再重放快照之后的事件,从而减少重放量。快照策略通常"每 N 个事件"或"按时间/阈值"生成快照:N 较小(快照频繁)则重放快、恢复快,但快照存储开销大、生成成本高;N 较大(快照稀疏)则存储省、生成少,但重放事件多、恢复慢。权衡:在高频变更聚合上适当调小 N 以快恢复,低频聚合可调大 N 省存储。快照也需版本化,升级时与新事件处理配合。

Snapshot 是"性能与存储"的平衡点:快照减少重放开销,但增加存储与生成成本。需要按聚合的变更频率与恢复时延需求动态调整 N。快照不可变,配合事件流保证可追溯。

#
★★★

4. 领域事件的「幂等消费」中消息重投导致的事件重复处理如何用事件 ID 去重、版本号与状态机保证只生效一次?

领域事件的幂等消费如何实现?消息重投导致的事件重复处理如何用事件 ID 去重、版本号与状态机保证只生效一次?

  • 幂等消费的必要性
  • 事件 ID 去重
  • 版本号与状态机

消息中间件可能重投事件(at-least-once),导致消费者重复处理同一事件,产生副作用(重复扣款、重复下单)。幂等消费保证事件只生效一次。实现手段:其一,事件 ID 去重——每个事件携带唯一 eventId,消费者在处理前记录已处理事件 ID(如数据库唯一约束/去重表),重复事件被拒绝;其二,版本号/业务键——用业务唯一键 + 版本号做乐观锁,重复提交因版本不匹配被拒绝;其三,状态机——把业务状态建模为状态机,只有"合法状态迁移"才接受,重复事件因状态已迁移到目标而无法再次生效。三者可组合,核心是"处理结果可重复、副作用只发生一次"。

幂等消费是最终一致架构的基石。去重表 + 唯一约束是基础手段,版本号应对并发,状态机从业务语义上保证"只走一次"。关键是让重复处理与首次处理结果一致但副作用不重复。

#
★★★

5. 事务性发件箱(Transactional Outbox)中事件与业务事务同库写入后由 relay 发布,如何保证不丢失、不重复且顺序可控?

事务性发件箱(Transactional Outbox)如何工作?事件与业务事务同库写入后由 relay 发布,如何保证不丢失、不重复且顺序可控?

  • Outbox 模式原理
  • 不丢失、不重复、顺序可控的实现
  • relay 的机制

事务性发件箱(Transactional Outbox)把"业务数据变更"与"待发布事件"写入同一个数据库事务:业务表更新 + outbox 表插入事件记录,同库同事务,保证"业务如果提交,事件记录必然存在;业务回滚,事件也不存在"。之后由独立 relay(轮询 outbox 表或订阅 binlog)把事件发布到消息中间件。保证不丢失:relay 从 outbox 读取并发布,只有确认投递成功后才删除/标记 outbox 记录,未投递的继续重试;保证不重复:发布采用 at-least-once,消费者需幂等去重;保证顺序可控:relay 按自增 ID/时间戳顺序发布,且同一聚合的事件用相同的分区键保持顺序。Outbox 把"本地事务"与"消息投递"解耦,避免分布式事务。

Outbox 的核心是"业务与事件同事务",消除"业务成功但事件丢失"或"事件发出但业务回滚"的不一致。relay 负责可靠投递,配合幂等消费与顺序控制,实现"不丢、不重、按序"。

#
★★★

6. 领域事件的 Schema 治理中事件字段的兼容性规则与版本化,消费者升级顺序如何编排避免破坏性变更?

领域事件的 Schema 治理如何做?事件字段的兼容性规则与版本化、消费者升级顺序如何编排避免破坏性变更?

  • 事件 Schema 的兼容性规则
  • 事件版本化
  • 消费者升级顺序

事件 Schema 治理是保证事件驱动系统中各消费者兼容的前提。兼容性规则:新增字段应设默认值(向后兼容),不得删除字段、不得改变字段类型/语义、不得重命名字段;破坏性变更需新增事件版本。版本化:事件携带 schemaVersion 或产生新事件类型,用 schema registry(如 Avro/JSON Schema Registry)管理事件 schema 的演进。消费者升级顺序:先升级消费者(能处理新旧版本),再发布新事件/新 schema,避免"先发新事件老消费者无法解析"的破坏性变更。即"消费者先行、生产者后发"的兼容编排,配合契约测试保证升级兼容。

事件 schema 是跨服务契约,破坏性变更会瞬间破坏所有消费者。兼容性规则(只增不改、默认值)+ 版本化 + schema registry + 消费者先行升级,共同保证事件演化安全。升级顺序的原则是"兼容性优先于发布"。

#
★★

7. Event Sourcing 中 Upcasting(事件版本升级)的演化策略

Event Sourcing 中 Upcasting(事件版本升级)的演化策略是什么?

  • Upcasting 的概念
  • 事件版本升级
  • 旧事件到新 schema 的迁移

在 Event Sourcing 中,已持久化的旧版本事件无法修改,当事件 schema 演进后,加载旧事件时需用 Upcasting(上转型)把它们升级到新版本:用一个转换函数把旧版本事件对象转换为新版本事件对象(补充默认字段、转换字段),再交给聚合重放。Upcasting 是"读取时迁移"(lazy migration),不需要重写历史事件存储,只在加载时对旧版本事件做兼容转换。策略:为每个事件类型维护 fromVersion 到 toVersion 的 upcaster 链,按版本顺序依次升级;新业务逻辑只处理最新版本事件。Upcasting 保证旧事件可被新聚合逻辑正确重放。

事件不可变、历史不可改,Upcasting 在读取时把旧事件升级到新 schema,是 Event Sourcing 应对 schema 演化的标准策略。它把"迁移"延迟到读取时,避免重写历史,且可组合多个 upcaster 完成多版本升级。

#
★★

8. Event-Carried State Transfer(事件携带状态)中消费者免回查

Event-Carried State Transfer(事件携带状态)是什么?如何让消费者免回查?

  • 事件携带状态的含义
  • 消费者免回查
  • 状态冗余与一致性

Event-Carried State Transfer(事件携带状态,ECST)是事件中携带完整的业务状态(而非仅事件 ID),消费者收到事件后直接使用其中的状态更新自己的读模型/数据,无需回查生产者的数据库。这避免了消费者在处理事件时再调用生产者 API 查询,降低耦合与延迟、提高可用性(生产者故障不影响消费者用事件重建)。代价是状态冗余(事件携带的数据可能过期、需处理版本冲突)与重复存储。消费者需用版本号/时间戳判断最新状态,丢弃过期事件。ECST 常用于跨服务传播数据、构建读模型或降级回查。

ECST 让消费者免回查,通过事件携带状态自足处理,是事件驱动系统的常见优化。它解耦了消费者对生产者的实时依赖,但需处理状态冗余与陈旧事件,用版本号控制谁来更新。

#
★★

9. Eventual Consistency(最终一致性)的 SLA 与传播延迟

Eventual Consistency(最终一致性)的 SLA 与传播延迟如何定义与管理?

  • 最终一致性的时间窗口
  • SLA 与传播延迟
  • 延迟的度量与治理

最终一致性意味着系统的写操作和读操作之间有一个时间窗口,期间数据可能不一致,最终会收敛到一致。SLA 与传播延迟管理的关键是:明确"最终多久一致"(如 99% 的事件在 5 秒内传播到各消费者),定义最大可接受延迟(silence window)与可容忍的不一致窗口。传播延迟由事件发布、投递、消费处理链路组成,需监控:事件从发布到被处理的时间、各消费者处理滞后、队列积压。治理手段:优化事件处理吞吐、减少重试、增加分区、监控延迟指标、对关键场景设置超时与补偿。SLA 应因业务场景而异(核心交易严、通知类松)。

最终一致不是"无限制地不一致",而是有明确的传播延迟目标。通过 SLA 定义"多久内收敛"、监控延迟与积压、针对关键流程治理,才能把最终一致控制在可接受范围。

#
★★

10. CQRS 架构的边界中写模型与读模型分离的工程收益与代价

CQRS 架构的边界是什么?写模型与读模型分离的工程收益与代价各有哪些?

  • CQRS 的定义
  • 读写分离的收益
  • 读写分离的代价

CQRS(Command Query Responsibility Segregation)把系统拆分为写模型(Command 侧,处理命令、变更状态、通常用领域模型/聚合)与读模型(Query 侧,处理查询、返回展示数据、用专门优化的 DTO/视图/表)。收益:读写模型可独立优化(读模型可去规范化、加索引、投影、独立缓存);写模型保持领域建模纯净,读写语义清晰;可分别扩展(读侧横向扩展、读库分流);复杂查询不再拖累写路径。代价:读写模型间数据同步(写后需体现到读模型,通常用事件,引入最终一致);代码与模型数量增加;一致性与运维复杂度上升;简单场景下过度设计。适用:读写比高、查询复杂、需独立扩展的场景;简单 CRUD 不必引入 CQRS。

CQRS 是"读写分离"的架构风格,收益与代价并存。读写完全分离带来独立优化与扩展,但引入最终一致与模型双份。判断是否引入取决于查询复杂度与扩展需求,避免对简单应用过度设计。

#
★★

11. EDA(Event-Driven Architecture)的事件总线与发布/订阅模型

EDA(Event-Driven Architecture)的事件总线与发布/订阅模型是什么?

  • 事件总线与发布/订阅
  • 事件驱动架构
  • 生产者与消费者解耦

EDA(事件驱动架构)以事件作为系统间通信的核心,组件通过"发布/订阅"(Publish/Subscribe)模型交互:生产者发布事件到事件总线(event bus),消费者订阅感兴趣的事件类型,总线负责把事件投递给订阅者。发布/订阅模型让生产者和消费者完全解耦:生产者不知道谁在消费,消费者不知道谁发布,事件类型是唯一契约。事件总线可以是进程内总线(内存事件)、消息中间件(Kafka/RabbitMQ)或事件网格。EDA 带来高解耦、异步、可扩展、弹性,代价是最终一致、调试与追踪复杂。事件总线涉及投递保证、顺序、过滤与路由。

发布/订阅是 EDA 的核心,事件总线作为"中间媒介"实现解耦。事件类型是契约,生产者与消费者通过事件类型关联而不互相依赖。总线选择与投递语义决定系统可靠性。

#
★★

12. 事件总线 vs 消息中间件(Kafka/RabbitMQ)的选型中事务性发件箱、消息顺序保证、分区键设计在事件驱动架构中的工程取舍?

事件总线 vs 消息中间件(Kafka/RabbitMQ)如何选型?事务性发件箱、消息顺序保证、分区键设计在事件驱动架构中的工程取舍是什么?

  • 事件总线与消息中间件的差异
  • 发件箱与消息中间件配合
  • 顺序保证与分区键

事件总线(进程内或轻量)适合单进程内、服务内解耦,简单低延迟;消息中间件(Kafka/RabbitMQ)适合跨服务、跨进程、需要持久化、可靠投递、高吞吐的场景。选型权衡:需要可靠投递、持久化、回溯、多消费者群组选 Kafka;需要灵活路由、优先级、工作队列选 RabbitMQ;单进程解耦用事件总线。事务性发件箱的 relay 把事件投递到消息中间件,由中间件保证跨服务投递。消息顺序保证:同一主题/队列内顺序通常有保证,跨分区需用分区键(partition key)——把同一业务实体(如同一订单)的所有事件路由到同一分区,保证其顺序。分区键设计决定顺序与并行度:按业务 ID 分区保序但并行受限,按其他键分区并行高但可能乱序。

选型依赖可靠性、跨进程与吞吐需求。顺序保证是事件驱动难点:靠分区键把相关事件聚到同一分区实现"按实体保序"。发件箱 + 中间件 + 分区键 + 幂等消费构成事务性事件投递的完整方案。

#
★★

13. 领域事件与集成事件的边界中内部订阅用领域事件、对外发布用集成事件,事件翻译与契约独立如何设计?

领域事件与集成事件的边界如何划分?内部订阅用领域事件、对外发布用集成事件,事件翻译与契约独立如何设计?

  • 领域事件与集成事件的区别
  • 内部订阅与对外发布
  • 事件翻译与契约独立

领域事件(Domain Event)是聚合在领域内发布的、表达领域事实的事件,用于聚合间/上下文内部的解耦,使用领域语言与内部模型。集成事件(Integration Event)是上下文对外发布、跨上下文/跨服务通信的事件,使用外部契约(Published Language),不暴露内部模型。边界:领域事件在聚合内部使用(领域层),不对外发布;跨上下文通信时,把领域事件翻译成集成事件(通过 Translator/ACL)再发布,契约独立设计。设计要点:集成事件契约与内部模型解耦,字段独立、版本化、只含对外需要的数据;领域事件与集成事件分离,避免把内部模型泄漏到外部。翻译逻辑集中在集成层(事件翻译器)。

领域事件是"内部语言",集成事件是"对外契约"。隔离二者避免内部模型泄漏,翻译层负责转换。契约独立(Published Language)保证对外稳定,内部可自由演化。

#
★★

14. 事件消费失败的重试与死信中指数退避、重试上限、死信队列与人工介入流程如何设计?

事件消费失败的重试与死信如何设计?指数退避、重试上限、死信队列与人工介入流程如何设计?

  • 消费失败的重试策略
  • 指数退避与重试上限
  • 死信队列与人工介入

事件消费失败需要可靠的重试与失败的兜底机制。重试策略:对可重试的失败(临时性错误,如超时、锁)采用指数退避(exponential backoff)重试,间隔递增(如 1s、2s、4s...),避免冲击系统;设置重试上限(如 3-5 次),超过上限不再重试。死信队列(DLQ):重试耗尽后把事件移入死信队列,标记为失败,避免阻塞主队列。人工介入流程:死信事件需有监控告警,由运维/开发检查失败原因,修复后可从死信队列重放或补偿处理;对永久性错误(数据损坏、schema 不兼容)记录详细日志与上下文,便于人工调查。重试需幂等(事件只生效一次),区分"可重试"与"永久失败"。

重试 + 死信是事件消费可靠性的兜底。指数退避防止重试风暴,重试上限防止无限重试,死信队列隔离失败事件,人工介入处理永久性错误。幂等保证重试不产生重复副作用。

#

15. Domain Event 的元数据(metadata)中发生时间、触发者、traceId

Domain Event 的元数据(metadata)包含哪些?发生时间、触发者、traceId 的作用是什么?

  • 事件元数据的组成
  • 发生时间、触发者、traceId 的作用
  • 元数据与业务数据的分离

Domain Event 除业务数据外还携带元数据(metadata),常见包括:发生时间(occurredOn,事件发生的时刻)、触发者(actor,谁发起了该事件)、事件 ID(唯一标识)、traceId(关联一次请求/业务流程的追踪 ID)、版本号等。这些元数据的作用:发生时间用于审计与事件排序、触发者用于审计与权限、traceId 用于跨服务/跨聚合的链路追踪(把一次业务请求的事件串起来,便于排查与监控)、事件 ID 用于幂等与去重。元数据与业务数据分离(业务数据放事件主体,元数据放 header),便于统一处理与进链路追踪。

元数据是事件"可追溯、可追踪、可审计"的基础。traceId 让分布式事件流可关联到同一次请求,是排查问题的关键。元数据标准化(统一 header)便于中间件自动附加。

#

16. Domain Event 的命名(过去时动词中 OrderCreated、PaymentCompleted)

Domain Event 如何命名?为什么用过去时动词(OrderCreated、PaymentCompleted)?

  • 事件命名规则
  • 过去时动词的原因
  • 事件表达"已发生事实"

Domain Event 命名采用"过去时动词"(如 OrderCreated、PaymentCompleted、OrderCancelled),形式为"实体 + 过去时动作"或"动作 + 过去时"。原因是事件表达"已经发生的事实"(past fact),过去时动词准确表达"该事件发生在过去、状态已改变"的语义,避免与命令(命令是未来意图,如 CreateOrder)混淆。命名要清晰表达业务语义、用统一语言、避免技术化命名。好的事件命名让订阅者一眼理解事件含义,便于事件驱动建模与沟通。

过去时命名强调事件是"已完成的事实",与命令(意图)区分。清晰的领域命名(OrderCreated)让事件成为可读的领域事实,是事件驱动建模与团队沟通的基础。

#

17. 事件驱动 vs 请求驱动中何时用事件?

事件驱动 vs 请求驱动(同步调用)如何选择?何时用事件?

  • 事件驱动与请求驱动的差异
  • 适用场景判断
  • 一致性与耦合的权衡

请求驱动(同步调用)是调用方发请求、等待响应,适合需要即时结果、强一致、实时响应的场景(如查询、必须同步确认的操作)。事件驱动是异步发布事件、不等待响应,适合以下场景:解耦——一个动作需要触发多个响应方,且响应方独立演化;异步化——不需要即时结果、可延迟处理;最终一致可接受——跨服务/跨聚合的状态同步;扩展性与弹性——事件可批量处理、削峰。判断标准:是否需要立即响应?是否需要强一致?是否只有单个响应方?若需要即时结果与强一致用请求驱动,若可异步、多响应方、可最终一致用事件驱动。很多系统两者结合。

事件驱动与请求驱动是互补而非互斥。请求驱动保实时与强一致,事件驱动保解耦与弹性。选择依据是"是否需要即时响应、强一致、单一响应方"。无响应方/可异步/多响应方用事件。

#

18. 事件溯源与 CQRS 中读写模型分离?

事件溯源(Event Sourcing)与 CQRS 的关系?读写模型为何分离?

  • 事件溯源与 CQRS 的关系
  • 读写模型分离
  • 事件溯源作为写模型

事件溯源(Event Sourcing)把聚合状态存储为事件流(而非当前状态),聚合状态由事件重放得到;CQRS 把读写模型分离。两者常结合:事件溯源作为写模型(命令产生事件),读模型由事件投影生成(从事件流构建查询用的读模型/表)。读写模型分离的原因:写模型(事件流)表达"发生了什么",精确、可审计、可重放;读模型面向查询优化(去规范、投影、索引),读写压力物理分离。事件溯源提供丰富的写模型,CQRS 提供优化的读模型,读模型从事件流投影,实现"一次写入、多视图读取"。但事件溯源也可独立于 CQRS,CQRS 也可用普通状态写模型。

事件溯源 + CQRS 是经典组合:事件流是写模型,投影出的读模型是读模型,读写分离最大化。但两者并非必须绑定——事件溯源可单用,CQRS 也不必用事件溯源。组合时读写分离自然形成。

#

19. 架构风格选型中分层、六边形与事件驱动?

分层架构、六边形架构与事件驱动架构如何选型?

  • 三种架构风格
  • 各自的适用场景
  • 选型权衡

分层架构(Layered)按依赖分层(表现层、应用层、领域层、基础设施层),依赖单向,简单直观,适合中小型、演进平稳的系统,但层次间易耦合、易退化为"跨层依赖"或"贫血模型"。六边形架构(Hexagonal / Ports & Adapters)用端口与适配器隔离核心与外部,依赖方向向内,核心领域不依赖技术,适合需要清晰边界、可测试、可替换基础设施的领域驱动系统。事件驱动架构(EDA)以事件异步解耦,适合高并发、弹性、跨服务协作、可最终一致的系统。选型权衡:简单系统用分层;核心领域复杂、需要边界与可测性用六边形;分布式、高并发、解耦用事件驱动。三者可组合(六边形核心 + 事件驱动的集成)。

架构选型取决于系统复杂度、边界需求与扩展性。分层是基础,六边形强化边界与可测性,事件驱动强化解耦与弹性。现代系统常以"六边形/清晰架构 + 事件驱动集成"结合,避免单一风格的局限。

#

20. 事件的发布与订阅中可靠投递?

事件的发布与订阅如何保证可靠投递?

  • 可靠投递的含义
  • 发布与订阅的可靠性
  • 幂等与补偿

事件可靠投递指"事件不丢失、不重复、及时到达消费者"。保证手段:发布侧用事务性发件箱(Outbox)保证事件与业务状态原子一致(不丢);投递侧用消息中间件(Kafka/RabbitMQ)的持久化与确认机制(ack)保证至少一次投递;消费侧用幂等消费(事件 ID 去重)处理重复投递(不重);监控消费滞后与积压保证及时。可靠投递通常采用"at-least-once + 幂等"组合:允许重复投递,但消费者通过幂等保证只生效一次,从而在不丢的基础上实现语义上的"不重复"。此外可用重试、死信队列兜底。

可靠投递是"at-least-once + 幂等"的经典方案:生产侧保证不丢(Outbox + 持久化),投递侧保证至少一次,消费侧幂等去重。完全"恰好一次"(exactly-once)在分布式中代价高,通常以"幂等 + at-least-once"近似实现。