真正可靠的多租户方案只有两种:字段隔离必须强制参数化(dao方法显式传tenantid),schema隔离必须一租户一*sql.db实例(用sync.map缓存映射);其他折中方案均在压测或审计中暴雷。

别用 context.Value 存租户 ID 做全局共享变量——它不是设计来干这个的,漏传、类型断言失败、异步任务丢失上下文,全是 P0 级隐患。
DAO 方法签名必须显式带 tenantID 参数
所有数据访问函数(GetByID、Create、Count、Preload)的第一参数或必填参数必须是 tenantID string,而不是从 ctx.Value("tenant_id") 里取。
-
ctx.Value返回interface{},下游必须做类型断言,断言失败直接panic - 定时任务、消息队列消费者、
time.AfterFunc等场景根本没有 HTTP context,ctx.Value永远是nil - 单元测试时容易 mock 错 context,导致漏掉租户过滤的 SQL 在测试里完全不暴露
- GORM 的
Count、Raw、Joins、Unscoped全部绕过 Scopes,靠中间件注入tenant_id等于没防
缓存键必须硬编码拼接 tenant_id
Redis 缓存 key 漏掉 tenant_id 是最隐蔽的数据越界源头,尤其当多个租户查同名资源(如 config、feature_flag)时,后写入者覆盖前者的值,下个租户直接命中脏数据。
- 正确格式:
tenant:{tenant_id}:user:{id}或tenant:{tenant_id}:feature_flag:paywall - 禁止用 struct hash 或 JSON 序列化生成 key,字段顺序、空格、嵌套结构稍有变化就 miss
- 缓存 miss 后查 DB,必须把带
tenant_id过滤的结果回填,否则下次请求仍可能读到其他租户旧数据
Schema 隔离必须一租户一 *sql.DB 实例
想靠单个 *sql.DB + SET search_path TO tenant_xxx 切换 schema 是伪方案,连接池不保证连接复用,上一个请求设的 schema 可能被下一个租户继承。
- 用
sync.Map缓存tenantID → *sql.DB映射,key 推荐用子域名(如acme),不是 UUID - 每个
*sql.DB初始化时执行db.Exec("SET search_path TO tenant_acme"),且必须白名单校验 schema 名 - 调用
db.SetMaxOpenConns(5)和db.SetConnMaxLifetime(5 * time.Minute),否则 1000 租户 × 默认 100 连接会压垮 PostgreSQL - 禁止手写
"tenant_acme.users"这类拼接表名,破坏 ORM 抽象,也引入 SQL 注入风险
真正难的不是加字段或切 schema,而是让每个 DAO 函数、每条缓存 key、每个 DB 实例初始化路径都强制绑定租户边界——漏掉任意一环,隔离就形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











