参考
Tenancy(多租户)
共享 schema + tenant_id:tenancy.WithTenant 与 Middleware、UniqueScope、分区键,以及 Circle 多商户映射示例。
Tenancy(多租户)
包:tenancy
go-sequent 用共享 schema + tenant_id:只有一套表;Command、事件、Idempotency 键(同一业务键重复提交只生效一次)和读模型都带租户,查询按租户过滤。没有内置的 schema-per-tenant 或 database-per-tenant。
设置租户
网关或应用层根据 API key、子域名或已验证的 token 解析出租户,再放进 context:
ctx = tenancy.WithTenant(ctx, tenantID)
err := app.Service.Execute(ctx, cmd)
tenancy.Middleware 把它抄到 Meta.TenantID:
Meta.TenantID为空时,用 context 中的租户填充。- 两者都已设置但不一致时,命令被拒绝:
tenancy: command tenant ... != context tenant ...。 - context 里没有租户时,命令原样通过。
把它放在链的最前面,后面的中间件才能看到租户:
app.Service.Middleware = append(
[]command.Middleware{tenancy.Middleware},
app.Service.Middleware...,
)
其他辅助函数:tenancy.FromContext(ctx) 与 tenancy.Require(ctx),后者在没有租户时返回错误。
辅助函数
| 函数 | 用途 |
|---|---|
PartitionKey(tenantID) |
租户 id 字符串;nil UUID 时为空。 |
PartitionKeyHashed(tenantID, shard) |
tenantID:shard,用于更细的分区,仍是同一个逻辑租户。 |
UniqueScope(tenantID, scope) |
tenant:{uuid}:scope,用于 Unique keys。 |
MatchAggregateTenant(cmdTenant, partitionKey) |
聚合的分区不属于命令租户时返回错误。 |
tenancy.UniqueScope(tenantID, "email")
tenancy.PartitionKeyHashed(tenantID, "shard-a")
tenancy.MatchAggregateTenant(cmdTenant, agg.PartitionKey())
MatchAggregateTenant 是纵深防御。当聚合必须属于调用方租户时,在 Handler 里 Load 之后调用它。
哪些东西带着租户
- Command。
Meta.TenantID,随命令记录存储。 - Aggregate。
PartitionKey(),通常是tenancy.PartitionKey(tenantID),在创建聚合时设置。 - Unique keys。 用
UniqueScope把租户带进 scope。 - Idempotency。 键空间是
(TenantID, IdempotencyKey)。 - Projection。 加
tenant_id列,用WHERE tenant_id = $1过滤。 - Outbox。 消息携带
TenantID。
分区
events 表存有 partition_key。表变得很大时,可以按租户或租户加哈希分区,仍在共享 schema 内。见 分区。
行级安全
基于 tenant_id 的行级安全(RLS)是可选的,库本身不提供。如果你自己加上,仍应在 Repository 与 Handler 里保留租户检查。
业务示例:Circle 多商户
当你为多商户支付或钱包产品集成 Circle API 时,租户边界是商户,而不是终端买家。
| 业务概念 | go-sequent 映射 |
|---|---|
| 商户 | Tenant:Meta.TenantID、行上的 tenant_id |
| 买家、运营人员、后台操作者 | User:Meta.UserID(先认证,再做 AuthZ) |
| Payment、Wallet、Settlement | Aggregate,每个事件流各自 OCC |
| 商户余额、对账、可提现金额 | 账本读模型,经 SyncInTxn(在写事务里同步更新读模型),按 tenant_id 过滤 |
不要让每个终端用户成为一个租户。终端用户可以作为事件载荷里的 buyer_id,或在发起命令时作为 UserID 出现;隔离与计费仍以商户为单位。
应用层映射:
| 列 | 作用 |
|---|---|
merchant_id |
外部业务键 |
tenant_id(UUID) |
写入 Meta.TenantID,用于读模型过滤 |
| Circle 侧 id | 取决于下面的配置模式 |
支付主流程
- 网关根据 API key、子域名或 JWT 中的
merchant_id解析租户,调用tenancy.WithTenant(ctx, tenantID)。已认证主体写入Meta.UserID。永远不要只凭客户端提供的 user id 授权。 CommandService.Execute执行例如CapturePayment。在写事务内:先做租户级 AuthZ;Handler 在 Payment 与 Wallet 聚合上应用事件(载荷里可含 Circle 的payment_id或transfer_id);EventStore 追加时带上 Idempotency(tenant_id, key)和tenancy.UniqueScope(tenantID, "merchant_order")之类的 Unique keys;账本读模型经 SyncInTxn 以... WHERE tenant_id = $1更新。调用方满足 read-your-writes:提交返回后立刻能读到自己刚写的数据。- 对 Circle 的 HTTP 调用,优先在提交之后通过带 AfterCommit 钩子的 Workflow 或 Outbox 完成,让外部延迟与重试远离账本事务。如果 Handler 必须调用 Circle,只做「创建意图、获取外部 id」这类短调用,余额事实仍交给 SyncInTxn Projector。
反模式:每个买家一个租户
给每个买家一个 TenantID,会把 Idempotency、Unique keys、AuthZ 和读模型过滤切得很碎,也不符合已入驻商户的计费边界。
两种 Circle 凭据模式
- 单个平台 API key。 宿主持有一个 Circle 凭据,每次调用在 Circle 的 metadata 或客户端引用里标注
merchant_id或TenantID。适合平台结算、代收。 - 每商户独立 Circle 凭据。 密钥按
TenantID索引,存放在 KMS 或密钥表里,对外调用前根据命令的TenantID查出凭据。适合拥有自己 Circle 账户的商户。
两种模式都不改变「租户 = 商户」,变的只是凭据归属。
相关
文档随 go-sequent 源码更新;若与 API 不一致,以源码为准。