跳到正文

参考

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 行,末尾没有换行:

  1. event_type
  2. aggregate_id(UUID 字符串)
  3. sequence_number(十进制)
  4. created_at(UTC,截断到微秒,RFC3339Nano)
  5. event_json(实际存储的载荷字节)
  6. 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 不一致,以源码为准。