跳到正文

安全

事件演进

随时间修改 go-sequent 事件的兼容性策略:upcaster 规则、版本化策略、兼容性矩阵、禁止事项与测试要求。

事件演进

事件历史会存很多年。本页说明:在不改写历史的前提下,如何安全地改事件形状。

原则

  1. **事件是不可变的事实。**优先用新的事件类型或 upcaster,而不是改写历史。
  2. **JSON payload 是默认编码。**信封字段放在存储列里。可行时,事件体应省略冗余的信封键。
  3. **过去式、小而扁平的事件。**重命名类型需要 registry 条目加 upcaster,或者做数据迁移。
  4. **永不导入或迁移 Ruby Sequent 的数据库。**Sequent 只是概念上的灵感,没有 dump 或线协议兼容。

Upcaster 规则

Upcaster 位于 event/upcast,在加载事件时运行。

规则 说明
按已存储类型注册 每个已存储类型名一个 upcaster。Chain.Register 在 FromType 重复时 panic。
链式执行直到稳定 默认上限 8 跳,超出会返回错误,所以要避免环。
指向当前类型 最终类型必须已在 event.Registry 注册。
纯转换 不做 I/O,确定性的 JSON 进、JSON 出。
加载时接入 在 LoadStream 之前调用 store.SetUpcasters(chain)。

示例:把字段 amt 改名为 amount,把类型 bank.MoneyDeposited.v0 改名为 bank.MoneyDeposited。

app.RegisterUpcaster(upcast.FuncUpcaster{
    From: "bank.MoneyDeposited.v0",
    Fn: func(p json.RawMessage) (string, json.RawMessage, error) {
        var m map[string]json.RawMessage
        if err := json.Unmarshal(p, &m); err != nil {
            return "", nil, err
        }
        if v, ok := m["amt"]; ok {
            m["amount"] = v
            delete(m, "amt")
        }
        out, err := json.Marshal(m)
        return "bank.MoneyDeposited", out, err
    },
})

store.SetUpcasters(app.Upcasters)

金额全程保持为 decimal 字符串。upcaster 只搬运这个值,不解析它。

版本化策略

  1. **新增字段。**使用可选的 JSON 键,并在 applier 里给默认值。不需要 upcaster。
  2. **重命名或重塑。**写一个从旧类型名出发的 upcaster。或者保留类型名,只转换 payload。
  3. **语义破坏。**以补偿的方式新增一个事件类型,例如 PaymentReversed,旧事件保持原样。
  4. **快照。**当聚合状态形状发生不兼容变化时,提升 SnapshotVersion,删除或忽略旧快照。见 快照。

兼容性矩阵

变更 事件 快照 Projector
新增可选字段 可以 通常可以 处理缺失值
重命名字段 Upcaster 提升版本 Upcaster 或双读
重命名事件类型 Upcaster 加 registry 由事件重建 追上之前两种都处理
删除字段 旧事件中保留,applier 忽略 必要时提升版本 读模型里不再写入该字段
金额表示 必须保持 decimal 字符串,绝不用 float64 同左 同左

禁止事项

  • 为了业务更正而静默地 UPDATE 或 DELETE 事件行。请使用补偿事件。
  • 把事件签名作为框架功能。框架实现的是 hash chain(按聚合串联事件哈希,用来发现篡改)。
  • 把加密销毁作为 GDPR 路径。删除只通过 Tombstone 完成:不删历史事件,而是追加一条标记事件,让读模型不再暴露个人数据。
  • 把 Ruby Sequent 的 SQL dump 加载到 go-sequent 的 schema 里。

修改已存储的事件行同样会破坏 hash chain,LoadStream 会返回 ErrHashChainTamper。见 EventStore。

测试要求

  • 为每个生产环境的 upcaster 写单元测试。
  • 注册 upcaster 后,用 LoadStream 重放一个夹具事件流。
  • 类型变更后重建 Projector(读模型),见 读模型重建。

相关

文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。