参考
Idempotency
租户级 Idempotency(同一业务键重复提交只生效一次):(TenantID, IdempotencyKey) 键空间、memory 与 Postgres 存储、默认空操作重放、Strict 模式的 ErrReplay,以及接线顺序。
Idempotency
包:idempotency
Idempotency 让客户端可以安全地重试命令:同一业务键重复提交只生效一次。带某个键的命令已经成功过,再用相同键重试时不会再次执行 Handler。
键空间
键是 (TenantID, IdempotencyKey) 这一对。两个租户可以用相同的键字符串而不冲突。设置了 IdempotencyKey 却没有 TenantID 会报错:idempotency: TenantID required when IdempotencyKey is set。
ctx = tenancy.WithTenant(ctx, tenantID)
cmd.Meta().IdempotencyKey = "merchant-order:42"
err := app.Service.Execute(ctx, cmd)
没有 IdempotencyKey 的命令完全跳过检查。
行为
对每条带键的命令,中间件的处理如下:
- 在存储里查找
(TenantID, IdempotencyKey)。 - 如果记录已存在:
- 默认:返回
nil,重试静默成功(空操作)。 Strict:返回包装了idempotency.ErrReplay的错误,其中包含原命令的 id 与类型。
- 默认:返回
- 如果不存在,执行链的其余部分,包括 Handler 与
Repo.Commit。 - 成功之后保存一条记录:命令 id、类型、状态
succeeded与时间。
命令失败时不会写入记录,重试会正常执行。
err := app.Service.Execute(ctx, cmd)
if idempotency.IsReplay(err) {
// 仅 Strict 模式:这个键已处理过
}
存储
type Store interface {
Get(ctx context.Context, tenantID uuid.UUID, key string) (*Record, error)
Put(ctx context.Context, rec Record) error
}
| 存储 | 用途 |
|---|---|
idempotency.NewMemory() |
测试与演示 |
idempotency.NewPostgres(pool) |
生产。使用表 idempotency_keys,主键为 (tenant_id, key)。 |
在 Postgres 上,记录与命令在同一个事务里写入。idempotency.Postgres 复用 WithinTx 的连接,所以指向 command_id 的外键能看到尚未提交的命令行。事务回滚时,键也一起回滚。
接线
app := sequent.New(sequent.Config{},
sequent.WithIdempotency(idempotency.NewPostgres(pool), false), // 第二个参数:strict
)
app.Service.Middleware = append(
[]command.Middleware{tenancy.Middleware},
app.Service.Middleware...,
)
tenancy 必须先运行,这样 Idempotency 查询时 Meta.TenantID 已经设好。中间件外层先行。也可以直接构造:idempotency.Middleware(idempotency.Config{Store: s, Strict: false})。
与 AuthZ 的关系
sequent.WithAuthorizer 安装的是过滤器,而过滤器在中间件链之后运行。因此重放的键会在过滤器运行之前返回。第一次尝试执行时已经通过了授权。如果希望每次尝试都做授权,把 security.Middleware(auth) 放在 Idempotency 中间件之前。见 AuthZ。
如何选键
- 键应当来自业务意图,而不是每次尝试生成的随机值。商户订单号、支付流水号,或在重试中保持不变的客户端请求 id 都可以。
- 键在重试之间必须稳定,在不同意图之间必须不同。如果客户端每次重试都生成新键,Idempotency 就帮不上忙。
- 键按租户隔离,不要把租户编码进键里。
- 把同一个键用于不同类型的命令是客户端的 bug。store 保留的是第一条命令的 id 与类型。
它不做什么
- 不能替代 OCC。两个键不同的请求仍可能在同一聚合上竞争,见 唯一键与 OCC。
- 不保护没有键的请求。
- 不返回原始结果。默认的重放返回
nil;客户端需要结果时,请读取当前状态或读模型(Projector 维护的查询表)。
Handler 仍应保持防御性,使用自然键和保护条件——Idempotency 是传输层面的安全网,不是领域规则的替代品。
相关
文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。