假设你是刚上任的档案主管,公司每天产生大量业务记录:订单成交、款项到账、库存变动……这些记录是公司的“事实底账”,必须妥善保存。你面前有三条路:建一座只为存档设计的专用保险库(EventStoreDB);在现有办公室的文件柜里开辟一块专区(Marten);或者把收发、登记、派送全外包给物流调度公司(Axon)。三条路都能“把事办了”,但投入、灵活度和适用场景天差地别。在软件世界,这套“把业务事实以事件形式永久保存、随时重放”的思路叫事件溯源(Event Sourcing),承载事件的存储层与框架统称 Event Store。这篇文章就带你从零认识这三个主流方案,学会在真实项目中做出明智的选型。

岗位关联

选对 Event Store,等于为事件驱动系统打好地基——它直接决定系统的可审计性、可扩展性与团队长期维护成本,是架构师评审微服务方案时的核心能力。

目录
  1. 可视化演示
  2. 框架定位
  3. EventStoreDB
  4. Marten框架
  5. Axon框架
  6. 架构对比
  7. 选型建议
  8. 迁移实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示EventStoreDB、Marten与Axon三大Event Sourcing框架在事件存储、投影能力与生态集成上的差异,展示事件追加写入与投影构建流程,辅助选型决策。

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

Event Sourcing 选型 EventStoreDB:事件流专用存储 Marten:.NET PostgreSQL集成 Axon:Java领域驱动框架 事件=事实 不可变追加

框架定位

上任档案主管的第一天,你就面对一道选择题:公司的业务记录怎么存?方案一是建一座专用档案大楼,防潮防火防盗,只干存档这一件事;方案二是在现有办公室的文档柜里划一块专区,和日常文件混着放;方案三是把收发、登记、派送全外包给物流调度公司。三种方案都能把事办了,但投入、灵活度、适用场景截然不同。这正是 Event Store 家族的三个生态位。

在软件架构里,“把每笔业务事实永久保存、需要时重放历史”的思路叫事件溯源(Event Sourcing)。它不保存“账户余额 = 100 元”这样的最终状态,而是保存“存入了 100 元”“取出了 30 元”这样不可变的事件。Event Store 就是这个档案库。按功能厚度,一个完整的事件驱动系统可以拆成三层:事件存储负责持久化,调度总线负责路由命令与事件,投影视图负责把事件流折叠成查询模型。

三兄弟的分工一目了然:EventStoreDB 是专职“存储”的独立产品,语言无关,任何客户端都能接入;Marten 是 .NET 库,复用 PostgreSQL 充当存储层,调度与投影由你自行组装;Axon 是 Java 全栈框架,把命令总线、事件总线、聚合、Saga 全部内置,存储实现可以随时更换。理解了分工,选型就不是“哪个好”,而是“你需要哪几层”。

所以选型的第一步不是比参数,而是先回答:你只想“租保险柜”,还是想“连物流调度一起外包”?框架定位决定了后面所有的取舍。

系统=事件存储+调度总线+投影视图\displaystyle \text{系统} = \text{事件存储} + \text{调度总线} + \text{投影视图}
一个事件驱动系统等于三块乐高:存储、路由、查询视图,三个框架各占其中一块或几块。
  1. 事件存储负责把事件按顺序追加保存,是整个系统的地基。
    事件存储\text{事件存储}
  2. 调度总线负责把命令路由给聚合、把事件分发给订阅方。
    调度总线\text{调度总线}
  3. 投影视图把事件流折叠为查询友好的只读模型。
    投影视图\text{投影视图}

EventStoreDB

如果预算充足、档案极其重要,你会选择建那座专用档案大楼。它只有一个使命:把档案按编号顺序锁进保险柜。大楼里没有办公桌、没有会议室,只有一排排保险柜。这就是 EventStoreDB 的自我定位——一款为了事件溯源而生的专用事件存储。

EventStoreDB 的核心模型有三个词:Stream(流)、Event(事件)、Position(全局位置)。一条 Stream 就像一位客户的档案袋,属于它的所有事件按版本号排列;Position 是集群内单调递增的全局编号,用来精确表达“从哪条记录之后开始读”。写入只能追加(Append-Only),历史事件不可修改,这让金融审计、数据回放变得极其自然。

它提供从任意位置订阅事件流的能力,每次新事件落库,订阅端立刻收到通知;内置投影机制可以把事件流折叠成读模型。由于是独立部署的专用服务,它不绑定任何语言,任何客户端都可以接入。代价是多了一个需要单独运维的集群组件——养一座大楼,总要配物业。

就像档案大楼的保险柜编号永远只增不减,EventStoreDB 用“全序追加”换来了无可辩驳的审计线索。如果你的系统最看重“历史不容篡改”,它就是那座为你量身定制的档案大楼。

positionn+1=positionn+1\displaystyle \text{position}_{n+1} = \text{position}_{n} + 1
每追加一条事件,全局位置就加一:这就是“只许增、不许改”的记账方式。
  1. 当前位置position_n 表示当前已写入的最新事件全局序号。
    positionn\text{position}_{n}
  2. 追加新事件新事件只能书写在日志末尾,序号整体加一。
    positionn+1\text{position}_{n} + 1
  3. 得到新位置position_{n+1} 是追加完成后的全局位置,单调递增。
    positionn+1\text{position}_{n+1}

Marten框架

预算有限、公司已有办公楼,你会不会说“别建新楼了,在文档柜里划块区域就行”?Marten 的诞生正是这个思路:不引入任何新组件,把 PostgreSQL 变成事件存储。对 .NET 团队而言,这是成本最低的起跑方式。

Marten 是 .NET 生态的事件存储库。它在 PostgreSQL 里维护一张“事件表”,每一行就是一个事件,字段包含流标识、流内版本、事件类型、时间戳与 JSON 负载。更特别的是,Marten 让普通业务文档和事件共用一个数据库:你可以把“订单表”这种文档模型与“订单事件”放进同一套事务——要么都成功,要么都回滚,永远不会出现“事件记了、业务没改”的尴尬。

投影是 Marten 的舒适区:用 LINQ 写查询、声明投影,开发体验非常流畅。运维上它零负担,索引、备份、高可用全部继承 PostgreSQL 能力。代价是存储模型受制于关系表的写放大,极端并发写入场景下不如专用存储激进。

就像在办公室文档柜里归档——省了建楼的钱,但规则得自己定。Marten 适合“已有 PostgreSQL、以 .NET 为主、不想新增运维负担”的团队,让事件溯源以最低门槛落地。

eventt=(sid, ver, data)\displaystyle \text{event}_t = (\text{sid},\ \text{ver},\ \text{data})
一个事件在 Marten 中就是“哪条流 + 第几个版本 + 什么内容”的三元组。
  1. 流标识sid 指明事件归属的事件流,相当于档案袋编号。
    sid\text{sid}
  2. 版本号ver 是事件在流内的序号,用于乐观并发校验。
    ver\text{ver}
  3. 事件负载data 保存业务事实的 JSON 内容,是事件的“正文”。
    data\text{data}

Axon框架

如果业务复杂到不想自己操心“文件怎么送、怎么分拣、怎么登记”,你干脆把所有流程外包给一家物流调度公司。你把“申请单”递进窗口,剩下的事都交给它:Axon 就是那个全包的调度公司,深耕 Java 生态。

Axon 不只是一个存储,而是一套完整的领域驱动设计(DDD)框架。它内置 Command Bus(命令总线)与 Event Bus(事件总线):业务操作以命令形式发出,总线把它路由给对应的聚合(Aggregate),聚合执行业务规则后产出事件,事件再通过总线广播给所有订阅方。由此,命令、事件、查询三类模型各走各的道,互不干扰。

Axon 还提供 Saga 编排能力,用于跨聚合甚至跨服务的长流程事务,比如“下单后先扣库存、再支付、最后发货”。它的存储层是可插拔的:可以选 JPA 关系库、MongoDB,也可以接 EventStoreDB。代价是概念多、学习曲线陡,Axon Server 的高级集群能力属于商业版——外包合同里的“增值服务”总得额外付费。

交给物流公司省心,但要付出培训与部署成本。Axon 适合“Java 团队 + 复杂 DDD 建模 + 需要 Saga 长流程编排”的场景,让事件驱动架构开箱即用。

E=Φ(C, S)\displaystyle E = \Phi(C,\ S)
业务规则 Φ 把“用户想做什么(命令 C)”和“当前状态 S”结合,产出“已经发生了什么(事件 E)”。
  1. 命令输入C 表示用户的业务意图,比如“取消订单”。
    C=CommandC = \text{Command}
  2. 当前状态S 是聚合在命令执行前的最新状态。
    S=StateS = \text{State}
  3. 映射规则Φ 是聚合内封装的业务规则,输出不可变事件 E。
    Φ(C, S)→E\Phi(C,\ S) \rightarrow E

架构对比

三家档案方案摆在桌面,横评要看三件事:档案的存放格式、取阅的便利程度、搬运的速度。对应到技术上就是存储模型、投影能力与性能基准。

存储模型上,EventStoreDB 使用专用日志格式,追加吞吐行业标杆,但导出与深度分析需要专门工具;Marten 用普通关系表,与业务数据同库同事务,透明可查;Axon 存储可插拔,性能取决于接入的后端。投影能力上,EventStoreDB 内置投影但函数式语法有门槛;Marten 用 LINQ 声明投影,.NET 开发者最顺手;Axon 不内置投影,需要自己维护查询模型。

性能基准方面,EventStoreDB 的单节点追加写入通常优于通用数据库上的事件表;Marten 的吞吐与 PostgreSQL 配置强相关,合理调优后足以支撑多数中大型业务;Axon 的主要开销在总线与序列化,持久化往往是瓶颈点。选型时应以团队压测数据为准,而不是纸面参数——仓库快不快,搬一箱才知道。

看清三家的“存放、取阅、速度”,就像横评三个仓库:专用库最快但封闭,关系柜透明但有限,外包公司全面但厚重。没有全能选手,只有最匹配的组合。

staten=g(staten−1, en)\displaystyle \text{state}_n = g(\text{state}_{n-1},\ e_n)
投影的本质是一个折叠函数:拿着旧状态,读一条新事件,揉一揉,得到新状态。
  1. 上一步状态state_{n-1} 是处理完前 n-1 条事件后的读模型。
    staten−1\text{state}_{n-1}
  2. 当前事件e_n 是投影器正在处理的第 n 条事件。
    ene_n
  3. 折叠函数g 将旧状态与新事件合并成最新状态。
    g(staten−1, en)→stateng(\text{state}_{n-1},\ e_n) \rightarrow \text{state}_n

选型建议

选仓库不能看广告,要看“病情”:团队有多少人、懂什么技术、业务复杂到什么程度、能分出几个运维。把这些条件摆齐,答案会自己浮出来。

三条经验法则:第一,看语言生态——.NET 团队优先 Marten 或 EventStoreDB,Java 团队优先 Axon 或 EventStoreDB,EventStoreDB 是跨语言的“最大公约数”。第二,看业务复杂度——只想把事件可靠落库、做审计回放,选 EventStoreDB;需要事件与文档在同一个事务里,选 Marten;需要领域建模、命令总线、Saga 编排,选 Axon。第三,看运维成本——独立部署的组件越多,值班压力越大。

正式决策建议用加权评分法:把“团队熟悉度、存储性能、投影能力、运维成本、扩展性、商业许可”列成维度,按业务重要性打权重,再为每个框架打分,总分最高者胜出。打分时务必结合真实压测,而不是拍脑袋。

加权评分就像给三个仓库方案打体检表:重要项高权重、次要点低权重。分数不会替你做决定,但能把“我觉得”变成“数据说”,让选型在评审会上站得住脚。

Score=∑i=1nwi si\displaystyle \text{Score} = \sum_{i=1}^{n} w_i\, s_i
选型总得分等于每个维度的得分乘以权重再求和,把主观判断变成可对齐的数字。
  1. 维度评分s_i 是某个选型维度的评分(如运维成本、性能)。
    sis_i
  2. 权重系数w_i 反映该维度对业务的相对重要程度。
    wiw_i
  3. 加权求和把每个维度的得分乘权重后累加,就得到总分。
    ∑i=1nwi si\sum_{i=1}^{n} w_i\, s_i

迁移实践

老档案库搬新库,最忌讳锁门搬家——公司还要营业。成熟的搬迁是“边营业边搬”:白天照常营业,晚上悄悄搬运,最后选个深夜完成切换。技术上的名字叫双写迁移。

第一步是双写:新老系统同时处理业务,同一笔事件既写旧库也写新库,通过对账任务定期比对差异,保证两边一致。第二步是历史回放:把存量历史事件批量灌入新存储,构建完整的投影状态。第三步是割接:先做一次最终快照,追平增量事件,校验通过后把读写流量切换到新库,并保留一段时间的回退窗口。

回放耗时是可以预估的:总事件数除以每秒处理吞吐,就是理论回放时间。若时间过长,可以用并行分区回放与快照截断加速。迁移期间建议打开新旧两端的审计日志,任何不一致都应视为阻断问题,直到对账连续为零差异再放行——搬库房的每一步都要留痕。

就像深夜搬库房,步骤越细、预演越足,风险越低。双写、回放、割接三步走完,新档案大楼正式营业,老库房封存留档——事件溯源系统的迁移,讲究的是“零事故换防”。

T=NR\displaystyle T = \frac{N}{R}
回放时间是事件总量除以吞吐量:事件越多、处理越慢,迁移窗口越长。
  1. 事件总量N 是需要回放到新库的历史事件总数。
    NN
  2. 回放吞吐R 是新存储每秒能处理的事件数。
    RR
  3. 回放耗时T 是理论回放总耗时,用于规划割接窗口。
    NR\frac{N}{R}

AI 应用场景

金融核心账务系统

阶段:数据架构与存储选型

解决的问题:传统账务系统直接覆盖余额字段,历史一旦出错无法追溯。用事件溯源后,每笔存取款都是不可变事件,审计与对账只需回放事件流。EventStoreDB 的 Append-Only 与全局位置正好满足监管要求。

具体案例:某银行新核心系统采用 EventStoreDB 作为账务流水的事件存储,每年承载数亿笔交易,审计部门可精确查询任意时点的账户状态,回放耗时从旧系统的“整库导出”缩短到分钟级。

.NET 电商订单中心

阶段:领域建模与持久化

解决的问题:订单状态频繁变更,传统 ORM 更新行记录会产生大量 UPDATE 与锁竞争。Marten 让订单业务表和订单事件表共用 PostgreSQL 事务,文档与事件混合存储,库存扣减与订单状态变更原子完成。

具体案例:某 .NET 电商团队用 Marten 重写订单中心,订单状态机以事件流驱动,配合 LINQ 投影生成“我的订单”查询视图,结算对账不再依赖定时任务扫描,上线后报表一致率达到 100%。

Java 供应链履约调度

阶段:事件驱动微服务编排

解决的问题:跨仓库、跨服务的履约流程涉及扣库存、支付、发货多步骤,硬编码状态机难以维护。Axon 的命令总线与 Saga 把长流程拆成可追溯的事件序列,任何一步失败都能补偿回滚。

具体案例:某物流平台用 Axon 实现履约编排,聚合封装库存与订单规则,Saga 协调支付与配送;接入 EventStoreDB 作为持久化后,全链路事件可回放,双十一高峰压测吞吐满足峰值要求。

Python 演示

python45 行
class InMemoryEventStore:    def __init__(self):        self._events = []  # 事件日志     def _next_seq(self, account_id):        return sum(1 for e in self._events if e["account"] == account_id) + 1     def append(self, account_id, event_type, amount):        event = {            "account": account_id,            "seq": self._next_seq(account_id),            "type": event_type,            "amount": amount,        }        self._events.append(event)        return event["seq"]     def replay(self, account_id):        balance = 0        for e in self._events:            if e["account"] != account_id:                continue            if e["type"] == "deposit":                balance += e["amount"]            elif e["type"] == "withdraw":                balance -= e["amount"]        return balance     def count(self):        return len(self._events)  def main():    store = InMemoryEventStore()    store.append("alice", "deposit", 100)    store.append("alice", "withdraw", 30)    store.append("bob", "deposit", 200)    store.append("alice", "deposit", 50)    print("alice balance:", store.replay("alice"))    print("bob balance:", store.replay("bob"))    print("total events:", store.count())  if __name__ == "__main__":    main()
目的:用内存列表模拟追加式事件存储,演示“事件追加—状态回放”的核心机制,与正文投影公式 state_n = g(state_{n-1}, e_n) 一一对应。
  1. self._events = []用 Python 列表充当“追加式事件日志”,对应 Append-Only 语义。
  2. def _next_seq(self, account_id)统计该账户已有事件数并加一,模拟流版本号递增。
  3. self._events.append(event)事件只追加、不修改,对应 EventStoreDB 的不可变日志。
  4. def replay(self, account_id)重放该账户全部事件,折叠出当前余额,等价于投影。
  5. if e["account"] != account_id: continue只处理目标事件流,回放按流过滤。
  6. print("alice balance:", store.replay("alice"))验证回放结果:100 - 30 + 50 = 120。

常见误区

误区我听说事件存储会把事件推给订阅方,那它跟 RabbitMQ、Kafka 这类消息队列有什么区别?是不是发完了就删除?

正解两者定位完全不同。消息队列负责“传输与解耦”,消息被消费后通常被移除;事件存储负责“保存与重放”,事件追加后永久保留,可以随时从任意位置重新读取。打个比方:消息队列是快递站,包裹送出后站里不留底;事件存储是档案库,借阅多少次都还在。

为何常见:事件驱动架构里两者经常搭配使用,订阅推流的行为与消息订阅很像,初学者容易把“推送”当作全部功能。

误区EventStoreDB 是专用产品,Marten 不过是往 PostgreSQL 里插几行 JSON,性能肯定不行,选它是不是“低配将就”?

正解技术选型没有绝对的高配低配。Marten 的“普通数据库表”恰恰带来同库事务、SQL 可查、运维零新增等独特优势;性能上 PG 调优后足以支撑大量中大型业务。就像办公室文档柜虽然没有保险库气派,但胜在顺手、便宜、好维护。

为何常见:人们容易把“专用”等同于“更好”,忽略了运维成本、团队熟悉度等更实际的维度。

误区既然文章把 Axon 和 EventStoreDB 放在一起比,那它应该也是一款能独立部署的事件数据库吧?

正解Axon 是“事件驱动应用框架”,提供命令总线、事件总线、聚合与 Saga;它的持久化是可插拔的,可以接 JPA、MongoDB 或 EventStoreDB。它更像物流调度公司,本身不生产保险柜,而是帮你把调度全包了。

为何常见:选型文章常把“存储”与“框架”并列讨论,容易让读者误以为它们属于同一类产品。

误区事件是唯一事实来源,那我还留什么业务表?把 ORDER 表删了,需要时从事件重建就行。

正解事件溯源解决“事实如何被记录”,投影解决“状态如何被查询”,两者分工不同。每次查询都回放全部事件,性能不可接受,必须通过投影维护读模型;且聚合的运行仍需要内存态。档案是底稿,底稿不能替代日常使用的报表。

为何常见:理解了“事件是真相”后,容易放大事件库的作用,忘了查询性能与读模型的必要性。

面试题

Q1. 以下哪项最能描述 EventStoreDB 的定位?

单选题 · easy

  • A.专门的事件溯源存储服务← 正确答案
  • B.通用的关系型数据库
  • C.异步消息队列中间件
  • D.嵌入式文档数据库

答案:A。EventStoreDB 是专为事件溯源设计的独立存储服务,追加式日志保证历史不可篡改。Marten 是 .NET 库、Axon 是 Java 框架,都不是“存储产品本身”;消息队列消费后即被移除,无法承担审计与回放职责。

Q2. Marten 框架将事件持久化在哪个基础软件中?

单选题 · easy

  • A.使用 MongoDB 作为存储底座
  • B.使用 PostgreSQL 作为存储底座← 正确答案
  • C.使用 Cassandra 作为存储底座
  • D.使用 HBase 作为存储底座

答案:B。Marten 的定位是“把 PostgreSQL 变成事件存储”,底层不引入新组件,事件以 JSON 行保存在 PG 的事件表中。MongoDB、Cassandra、HBase 都不是 Marten 的实现底座。

Q3. Axon 框架主要面向哪个开发语言生态?

单选题 · easy

  • A.基于 Java 的生态体系← 正确答案
  • B.基于 C++ 的生态体系
  • C.基于 Go 的生态体系
  • D.基于 Rust 的生态体系

答案:A。Axon 出生于 Java 生态,围绕 DDD 提供命令总线、事件总线与 Saga,绝大多数使用者是 Java 团队。Go、Rust、C++ 都没有 Axon 的官方一等公民地位。

Q4. 事件溯源中“投影”(Projection)的核心作用是?

单选题 · mid

  • A.将命令压缩成单一事件
  • B.将事件流折叠为查询视图← 正确答案
  • C.删除已经过期的历史事件
  • D.加密全部事件的敏感字段

答案:B。投影的本质是折叠:把旧状态与最新事件合并成新的查询视图,正是 CQRS 中读模型的构建方式。把命令压缩、删除历史、加密字段都与投影的语义无关。

Q5. EventStoreDB 采用追加式(Append-Only)日志带来的直接收益是?

单选题 · mid

  • A.支持直接修改历史事件
  • B.自动压缩减少磁盘占用
  • C.保留完整可审计的历史记录← 正确答案
  • D.免除数据库备份运维

答案:C。Append-Only 意味着事件只能追加、不能修改删除,天然形成完整审计线索。直接改历史、自动压缩、免除备份都与追加式日志的设计初衷相悖。

Q6. EventStoreDB 中“期望流版本号”主要用于解决什么问题?

单选题 · hard

  • A.提高事件的序列化压缩比
  • B.并发追加时的乐观并发控制← 正确答案
  • C.决定事件的持久化存储位置
  • D.指定投影结果表的主键

答案:B。期望流版本号用于乐观并发控制:写入时带上“我以为的最新版本”,若不匹配则拒绝写入,避免两个命令同时改同一条流造成状态分裂。压缩比、存储位置、投影主键都与并发安全无关。

Q7. 一个 .NET 团队选择 Marten 的常见理由包括?

多选题 · mid

  • A.延续现有的关系库运维经验← 正确答案
  • B.文档与事件可混合存储← 正确答案
  • C.深度集成 ASP 应用生态← 正确答案
  • D.提供独立的消息队列引擎

答案:ABC。选择 Marten 的常见理由有三:延续 PostgreSQL 运维经验、文档与事件混合存储、深度融入 .NET/ASP.NET 生态。Marten 不提供消息队列引擎,消息中间件需要另行引入。

Q8. 关于事件存储与消息队列的本质区别,正确的是?

单选题 · hard

  • A.队列保存全部历史数据而事件不可恢复
  • B.事件存储吞吐一定高于消息队列
  • C.消息队列可代替事件存储做审计
  • D.事件可回放而队列消费即删除← 正确答案

答案:D。事件存储与消息队列的本质差异在于“回放”:事件永久保存可按序重放,队列消息通常被消费即删除。队列吞吐未必低于事件存储,也不能替代事件存储做审计,因此 B、C 都为错。

Q9. 从自研事件表迁移到 EventStoreDB,合理的实施步骤有哪些?

多选题 · extreme

  • A.双写新旧两层并校验差异← 正确答案
  • B.按时间窗口批量回放事件← 正确答案
  • C.比对投影结果与旧库一致← 正确答案
  • D.迁移期间立即停用写接口

答案:ABC。安全迁移讲究“新旧双写 → 回放历史 → 投影比对 → 切换”四步,任何一步不一致都禁止割接。“迁移期间立即停用写接口”会让业务中断,不合理。

Q10. Axon 框架内置的核心能力包括?

多选题 · mid

  • A.命令总线与事件总线← 正确答案
  • B.聚合与 Saga 编排机制← 正确答案
  • C.可插拔的存储实现层← 正确答案
  • D.内建前端 UI 组件库

答案:ABC。Axon 框架内置命令总线、事件总线、聚合与 Saga 编排,存储层通过 SPI 可插拔。内建前端 UI 组件库不是 Axon 的能力,它专注后端领域建模与事件驱动。