go多租户必须全程显式绑定tenant_id或schema:postgresql需用sync.map缓存租户专属*sql.db并set search_path,gorm preload/count须手动注入tenant_id条件,redis键名必须带tenant前缀,casbin需改四元组模型并传四参数。

Go 微服务里做多租户,别指望框架自动兜底——租户隔离必须从连接池、SQL 生成、缓存键到权限校验全部显式绑定 tenant_id 或对应 schema,漏一处就是跨租户数据泄露。
PostgreSQL schema 隔离怎么避免连接池失效
很多人试 db.Exec("SET search_path TO tenant_abc"),结果下一次查询又回到 public。根本原因是 *sql.DB 连接池不保证复用同一物理连接,session 级命令无法跨请求生效。
- 必须为每个租户缓存独立的
*sql.DB实例,用sync.Map存tenant_id → *sql.DB - 初始化时用标准 DSN 打开连接,立刻执行
db.Exec("SET search_path TO " + schemaName),后续所有操作自动落在该 schema - 务必调
db.SetMaxOpenConns(5)和db.SetConnMaxLifetime(5 * time.Minute),否则 100 个租户 × 默认 100 连接 = PostgreSQL 直接被打崩 - 别手写
"tenant_abc.users"拼表名——破坏 ORM 抽象、难测、GORM 钩子(如BeforeCreate)全失效
GORM Preload 和 Count 为什么总绕过 tenant_id 过滤
Preload("Orders") 和 Count() 不继承主查询的 Scopes,GORM v2+ 也不透传 tenant_id 到关联查询或聚合语句里。漏一次,就可能查出全量订单。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 禁止裸调
db.Preload("Orders").Find(&u);改用封装方法:tenantDB.User().WithOrders(tenantID).Find(&u),内部对Orders关联也手动加WHERE tenant_id = ? -
Count()必须走同级过滤:db.Where("tenant_id = ? AND status = ?", tenantID, "pending").Model(&Order{}).Count(&count),不能依赖 scope 透传 - 软删除字段
deleted_at必须和tenant_id同级约束:Where("tenant_id = ? AND deleted_at IS NULL", tenantID),否则Unscoped()直接绕过 - 每张表的
tenant_id字段必须加联合索引,比如(tenant_id, created_at),否则分页查询慢成秒级延迟
Redis 缓存键漏 tenant_id 是高频 P0 故障点
缓存键不带 tenant_id,多个租户查同一资源名(比如都叫 config),Redis 里存的就变成“谁先写谁赢”。这不是并发问题,是设计缺陷。
- 所有缓存 key 必须形如
tenant:{tenant_id}:user:{id}或tenant:{tenant_id}:feature_flag:paywall - 硬编码拼接比用 struct hash 更可控,避免序列化差异导致键不一致
- 别在
redis-go的Get返回nil就去查 DB,却没在缓存 miss 后把带tenant_id的结果回填——下个租户会直接命中脏数据
Casbin 四元组模型必须显式带 tenant
Casbin 默认 rbac_model.conf 只定义 r = sub, obj, act,所有租户共享策略空间。Alice 在租户 A 被赋予权限,可能凭同一角色访问租户 B 的资源。
- 必须改请求定义为四元组:
r = sub, tenant, obj, act,并在[matchers]中强制校验r.tenant == p.tenant - 调用时不能用
e.Enforce("alice", "/api/users", "read"),而必须传四参数:e.Enforce("alice", "tenant-abc", "/api/users", "read") - 策略行要显式带租户:
p, admin, tenant-abc, /api/users, read;角色继承也得带租户:g, alice, admin, tenant-abc - 别为每个租户启一个
Enforcer实例——内存爆炸、策略同步难;用单实例 + 四元组 +FilteredAdapter加载租户策略子集更可控
最易被忽略的不是技术选型,而是所有层面对 tenant_id 或 schema 名的传递是否全程不可绕过:HTTP 中间件注入后,DAO 层必须显式接收参数,不能靠 context.Value;连接池缓存必须带租户粒度;缓存键、权限策略、索引设计,全部要对齐同一租户上下文——少一处,安全边界就塌一块。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










