事件溯源(Event sourcing)要求你停止覆盖数据。在传统的 CRUD 应用中,更新用户的收货地址意味着找到该行,修改值,并丢弃之前的状态。事件溯源则采取了不同的路径。它将每一次变更都存储为一个不可变的既定事实:用户创建了账户、更新了地址、验证了电子邮件。系统当前的状态并不直接存储,而是通过按顺序重放这些事件计算得出的。
这种模式解决了实际问题。审计追踪(Audit trails)成了免费的副产品。你可以重建过去任何时刻的订单状态。你可以通过重放发生的具体过程来进行调试。权衡之处在于复杂性。你现在管理的是事实流、读模型(read models)和最终一致性(eventual consistency),而不再是简单的行。
PostgreSQL 可以充当你的事件存储(event store)。大多数团队已经在运行它了。它提供 ACID 事务、用于灵活负载的 JSONB 以及经过验证的备份工具。你不需要在第一天就引入 Kafka、Cassandra 或专门的事件存储数据库。一个标准的 Postgres 实例就能提供事件溯源所需的事务保证和审计追踪,而无需扩大你的基础设施规模。
Postgres 事件存储的形态
模式(Schema)可以简单到令人惊讶。至少,你需要一张只追加事件且从不在原位更新的表。一个实用的设计如下:
id使用bigserial或UUID,作为全局排序。stream_id用于对相关事件进行分组,例如单个用户或订单的所有变更。event_type为纯文本:UserEmailChanged、PaymentReceived、InventoryAdjusted。payload为JSONB,保存该事件发生的具体数据。occurred_at带有时区精度。- 每个流(stream)一个
version,用于强制执行乐观并发控制(optimistic concurrency)。
你可以在应用代码中或通过数据库约束来强制执行不可变规则。在 (stream_id, version) 上建立唯一索引,可以防止两个写入者追加相同的序列号。当命令进入时,你在一个事务中读取该流的当前版本,将其递增,然后插入新事件。如果另一个进程抢先一步,唯一约束就会失效,然后你可以重试或拒绝该命令。
考虑一个具体的例子。你正在运行一个库存系统。你不再使用带有 quantity 列的单个 inventory 行,而是向 inventory_events 表追加事件。ItemReceived 增加 10 个单位。ItemReserved 减少 2 个。ItemShipped 减少 3 个。要知道 SKU-42 的当前库存,你需要对相关的事件负载进行求和。要知道三天前的库存,你只需求和到那个时间戳为止。如果上周二你的发货逻辑中的一个 bug 导致了错误,你可以通过修正后的代码重放事件,以获取真实状态。使用简单的 UPDATE 语句是无法做到这一点的。
让你免于陷入困境的原则
基于 Postgres 构建并不意味着不需要自律。以下原则直接适用于事件溯源系统。
保持简单。 复杂性会扼杀可靠性。在实现一个可运行的工作流之前,抵制构建通用事件框架的冲动。一张表、一个用于追加事件的 repository 函数以及一个用于构建读模型的 projection worker 就足以证明其价值。只有在出现具体问题时才添加工具。
从小处着手。 不要重写你的整个单体应用。选择一个审计追踪带来的收益能抵消其开销的限界上下文(bounded context)。计费账本、工作流引擎或库存预留系统都是不错的选择。端到端地构建那一条流水线。让它在生产环境中运行。然后再决定是否扩展。
先定义成功。 事件溯源不是默认架构;它是针对特定需求的解决方案。如果你的需求只是跟踪最新状态,那么 CRUD 更快、更便宜。如果你需要时序查询(temporal queries)、严格的可审计性,或者按需重建读模型的能力,那么使用事件才是合理的。在投入使用之前,先搞清楚你正在解决什么问题。
先测量,后优化。 在普通的硬件上,现代 PostgreSQL 使用简单的仅追加表每秒可以摄取数千个事件。在你的监控证明你已经用尽了更简单的修复方法之前,不要对事件存储进行分片或引入复杂的划分方案。为你查询的字段建立索引。针对仅追加的工作负载调整 autovacuum。然后再次测量。
Test everything. Unit test your event handlers. Integration test the append path. Most importantly, test failure scenarios. What happens when two nodes append to the same stream simultaneously? What happens when a projection worker crashes mid-batch? Write tests that verify your optimistic concurrency and your at-least-once delivery guarantees.
Monitor in production. The events table will grow. Unlike a normalized schema where updates keep row counts flat, event sourcing is intentionally additive. Track table size, disk I/O, and the lag between your write model and your read model projections. Set alerts on projection lag before your users notice stale data.
Automate manual tasks. Manual schema changes, manual projection rebuilds, and manual event replay are ticking time bombs. Script your migration strategy. If you evolve an event schema, automate the upcasting or transformation so that old events can be replayed through new logic without human intervention at midnight.
Document your choices. Write down why specific streams exist, what each event type means, and when the team should choose CRUD over events. Event sourcing introduces cognitive load. Good documentation prevents a new engineer from guessing wrong and appending malformed events to a critical stream.
Traps That Waste Months
Event sourcing has a way of sounding elegant in a diagram and painful in production. Watch for these traps.
Underestimating complexity. Replaying events to rebuild state is conceptually simple. Managing idempotency, snapshotting for performance, and compensating transactions across aggregates is not. Break your system into small pieces. Solve one stream at a time.
Over-engineering. Do not provision a multi-node Kafka cluster because you imagine your event volume will one day require it. Postgres can carry you surprisingly far. Introduce new infrastructure only when you have a measured bottleneck that you cannot fix within your current setup.
Ignoring technical debt. Old event schemas linger forever. If you change your OrderCreated payload, you still have ten million historical events in the old shape. Track this debt. Plan backward-compatible readers or migration scripts. Do not let the burden of legacy events slow every new feature.
Choosing tools the team cannot run. The best architecture fails if only one person understands it. If your team knows Postgres and SQL, start there. If you introduce a specialized event store, make sure you have the operational expertise to debug it at two in the morning.
A Practical Starting Point
If this approach fits your problem, do not wait for a five-quarter rewrite. Start this week.
Audit your current systems. Find a place where an audit trail would solve real pain. Maybe it is an order state machine that currently maintains a single status column. Perhaps it is a financial ledger where balance corrections require manual database patches. Pick one gap where overwriting state has hurt you.
Then pick one small improvement you can make today. Create one event table. Model one stream. Write one projection that builds a read model from those events. Deploy it behind a feature flag. Watch it handle real traffic.
Event sourcing with PostgreSQL is not magic. It is a practical tool for teams that need to know not just where things are, but how they got there. Build slowly, measure honestly, and let your actual requirements guide the architecture.
