多数场景应优先选用共享数据库+tenant_id字段隔离,仅当租户数稳定在百级以内且有dba全程支持时才考虑schema隔离;前者更易落地、运维成本低、安全性依赖数据库强制约束而非人工编码。

租户隔离该用数据库还是 Schema?
多数人一上来就想用 PostgreSQL 的 schema 实现租户隔离,但实际落地时容易卡在迁移、权限、连接池复用三处。真正稳定的做法是「共享数据库 + 租户 ID 字段隔离」,除非你有 DBA 全程配合且租户数长期稳定在百级以内。
Schema 方案的问题很具体:golang-migrate 默认不支持动态 schema 名;pgx 连接池无法自动切换 schema;每个租户建独立 schema 后,pg_stat_activity 会暴增,监控和慢查定位变难。
- 新项目起步阶段,直接在所有核心表加
tenant_id字段(类型建议UUID或int64),并在所有SELECT/UPDATE/DELETE中强制带上WHERE tenant_id = ? - 用
sqlc或ent生成代码时,在WHERE条件里注入tenant_id,避免手写漏掉 - 如果必须用多 schema,改用
database/sql的Conn按需获取连接,执行SET search_path TO tenant_abc,但要自己管理连接生命周期,别让中间件无感透传
HTTP 请求怎么安全带入 tenant_id?
靠前端传 tenant_id 字段或 header 是最危险的起点——它等于把租户边界交给了不可信输入。正确路径是:从认证上下文推导租户身份,再绑定到请求生命周期。
典型错误是把 X-Tenant-ID 当作唯一依据,结果被恶意请求绕过 RBAC 直接读取他人数据。真正能信任的只有登录态中已验证的归属关系。
- 用户登录后,JWT payload 里存
tenant_id和tenant_role,签名校验通过才解出,别在 middleware 里重新解析或转换 - 使用
gorilla/mux或gin.Context的Set()方法把tenant_id注入 context,后续 handler 全部从ctx.Value()取,别从 query/header 二次读取 - 数据库查询层(如
ent.Client)封装一个WithTenant(ctx)方法,自动注入tenant_id过滤条件,避免每个 repo 方法都手动拼Where
缓存键要不要包含 tenant_id?
缓存键漏掉 tenant_id 是导致数据错乱的高频原因,尤其当多个租户查同一资源名(比如都叫 config)时,Redis 里存的就变成“谁先写谁赢”。这不是并发问题,是设计缺陷。
更隐蔽的问题是:有些团队用 redis-go 的 Get 返回 nil 就去查 DB,却没在缓存 miss 后把带 tenant_id 的结果回填,导致下个租户命中脏数据。
- 所有缓存 key 必须形如
tenant:{tenant_id}:user:{id}或tenant:{tenant_id}:feature_flag:paywall,硬编码拼接比用 struct hash 更可控 - 使用
groupcache或ristretto时,确保tenant_id是 cache key 的第一级字段,否则 LRU 驱逐策略会跨租户污染 - 对全局配置类数据(如系统版本号),单独建
global:前缀的 key,明确区分租户级与平台级缓存域
迁移和灰度发布怎么不炸租户?
给表加字段或改索引时,如果只跑一次 ALTER TABLE,十有八九会让某个租户的请求卡住几秒甚至超时——不是因为 SQL 慢,而是 PostgreSQL 在 DDL 期间会对整表加锁,而你的应用可能正在高并发读写。
更麻烦的是,不同租户的数据量差异极大,小租户表秒级完成,大租户表可能锁表五分钟,运维根本不敢半夜发版。
- 用
gh-ost或pt-online-schema-change替代原生ALTER,它们能在线做变更,但要注意:这些工具默认不识别tenant_id分片逻辑,得关掉并行 chunk 扫描,避免跨租户误操作 - 灰度发布时,按
tenant_id的哈希值分批,比如hash(tenant_id) % 100 先切 5% 租户,观察日志里的 <code>tenant_id错误率,别按时间窗口或机器维度切 - 任何迁移脚本里禁止出现
UPDATE table SET x=y这种全表更新,必须带WHERE tenant_id IN (..)分批执行,每次最多 1k 行,用FOR UPDATE SKIP LOCKED避免死锁
租户 ID 的传递链路越长,漏掉过滤的地方就越多。从 HTTP header 到 DB 查询中间经过的每一层中间件、ORM、缓存、消息队列,都要显式声明对 tenant_id 的依赖和校验责任,而不是指望某一层兜底。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











