参考
EventStore
EventStore:Store 接口、memory 与 Postgres 后端、错误类型,以及按聚合维护的 SHA-256 hash chain,含规范输入与校验。
EventStore
包:eventstore
EventStore 持久化命令与聚合事件流。追加时强制 OCC(用 ExpectedVersion 挡住并发写冲突)与唯一键,并对已存事件维护 hash chain(按聚合串联事件哈希,用来发现篡改)。应用通常通过 Repository 间接使用它,而不是直接调用。
接口
type Store interface {
Append(ctx context.Context, cmd CommandRecord, streams []StreamAppend) error
LoadStream(ctx context.Context, aggregateID uuid.UUID) ([]event.Event, error)
LoadStreams(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID][]event.Event, error)
}
type TxManager interface {
WithinTx(ctx context.Context, fn func(ctx context.Context) error) error
}
两种后端还实现了 VerifyChain(ctx, aggregateID) error 与快照存储接口。
Append 接收一个 CommandRecord(id、类型、租户、用户、Idempotency 键、时间、JSON 载荷),以及每个聚合一个 StreamAppend:
type StreamAppend struct {
AggregateID uuid.UUID
AggregateType string
PartitionKey string
ExpectedVersion int64 // 最后已提交的序号;新流为 0
Events []event.Event
UniqueKeys map[string]any
}
命令行与所有事件流原子写入。第一个新事件的序号必须是 ExpectedVersion + 1。
后端
| 后端 | 用途 |
|---|---|
eventstore/memory |
单元测试与集成测试,memory.New(registry)。 |
eventstore/postgres |
生产。postgres.Open(ctx, dsn, registry),然后 Migrate。同时提供 TxManager 与 SnapshotStore。 |
PostgreSQL 是目前唯一的生产后端。接口为其他后端留了空间,但尚未提供。
store, err := postgres.Open(ctx, dsn, app.Events)
if err != nil { /* ... */ }
if err := store.Migrate(ctx); err != nil { /* ... */ }
app.Repo.Store = store
app.Repo.Snapshots = store
app.Service.Tx = store
Migrate 可重复执行(幂等):它获取 advisory lock,在 go_sequent_schema_migrations 中记录已应用版本,并依次执行 SQL 步骤 001 到 006。见 Migrations 与 CLI。
Postgres store 锁定聚合行(SELECT ... FOR NO KEY UPDATE),读取最后序号,要求第一个新事件为最后序号加一,然后插入。主键冲突即为并发冲突。完整性失败会映射为 ErrOptimisticConcurrency 或 ErrUniqueKeyViolation。
错误
| 错误 | 含义 |
|---|---|
ErrOptimisticConcurrency |
流末尾与 ExpectedVersion 不符 |
ErrUniqueKeyViolation |
某个唯一键 scope 与值已被占用 |
ErrStreamNotFound |
该聚合 id 没有事件 |
ErrHashChainTamper |
hash chain 校验失败 |
ErrHashChainIncomplete |
缺少哈希列 |
用 errors.Is 判断。
Hash chain(哈希链)
完整性靠每个聚合一条 SHA-256 hash chain。每个已存事件保存 prev_hash 与 event_hash。第一个事件(序号 1)的 prev_hash 是 genesis:32 个零字节。
规范输入是用 \n 连接的 UTF-8 行,末尾没有换行:
event_typeaggregate_id(UUID 字符串)sequence_number(十进制)created_at(UTC,截断到微秒,RFC3339Nano)event_json(实际存储的载荷字节)prev_hash,64 位小写十六进制
event_hash = SHA-256(input)
公式在 eventstore/hashchain 中。Postgres 里载荷列是 BYTEA 而不是 JSONB,因此存下来的字节就是参与哈希的字节。
LoadStream 读取时会校验 hash chain;也可以调用 VerifyChain(ctx, aggregateID) 显式检查。链断裂时返回 ErrHashChainTamper。
Hash chain 能给你什么
它能发现事件流内部的修改、删除与重排。改动任何已存字段,都会让之后所有的哈希失效。
它不能给你什么
- 没有事件签名。 Hash chain 证明的是内部一致性,而不是谁写了某个事件。拥有数据库完整写权限的人可以重算整条流。请配合数据库 ACL、加密备份与可选的 WORM 存储,见 威胁模型。
- 只按聚合。 跨事件流没有全局链。
- 有成本。 追加要算哈希,加载要校验哈希。
载荷字段加密
crypto.PayloadProtector(Protect 与 Unprotect,附带 Noop 实现)是可选接口,用于加密事件 JSON 的部分字段。需要你在自己的 store 路径里围绕事件 JSON 接入,默认 store 不会自动应用。它不是 crypto-shredding,也不意味着事件真实性。
相关
文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。