数据隔离策略应优先于框架选型,字段隔离(tenant_id)仅适用于租户数少、安全要求低的场景,需根据规模与安全需求选择独立数据库、共享数据库独立Schema或字段级隔离。

Go 语言做多租户 SaaS,**别从框架选型开始,先定数据隔离策略**。字段隔离(tenant_id)只适合租户数 schema 隔离——它靠数据库原生 search_path 兜底,漏写条件也不会越权。
怎么安全提取租户 ID(子域名场景)
子域名(如 acme.example.com)是生产最稳的租户标识方式,但直接 strings.Split(r.Host, ".")[0] 会崩在带端口的开发环境(localhost:3000)或反向代理重写后的 Host 头上。
- 用
net.SplitHostPort(r.Host)先剥离端口,再对干净主机名做strings.SplitN(host, ".", 2) - 主域名(
example.com)必须定义为常量,不能硬编码进正则或字符串里 - 本地开发时,
localhost或127.0.0.1应跳过解析,或走白名单配置,避免测试失败 - 别依赖
X-Tenant-ID请求头作为唯一依据——它可被伪造,仅适合内部调试或 BFF 层透传
为什么 tenant_id 字段隔离容易出 P0 事故
所有表加 tenant_id 字段、每个查询手动加 WHERE tenant_id = ?,看似简单,实则极难兜底。GORM 的 Preload、Count、Raw 查询都可能绕过你写的 Scopes,一次漏加就是全量数据泄露。
- DAO 层所有方法必须显式接收
tenantID string参数,禁止裸调db.Where().Find() -
soft delete字段(如deleted_at)要和tenant_id同级约束,否则Unscoped().Where()可绕过租户检查 - GORM
Scopes对Session和关联预加载不自动生效,Preload必须显式传入 scope - 任何直连 DB、用第三方库查表、甚至日志埋点写入 DB 的路径,都必须显式携带
tenant_id——漏掉任意一个点,边界就塌了
PostgreSQL schema 隔离怎么落地(不爆连接池)
每个租户一个 schema(如 tenant_acme),物理隔离强度高,但 Go 生态没有“运行时动态切换 search_path 并复用连接”的标准方案。硬拼 SET search_path TO ... 到每个查询前,或用全局 map 缓存 *sql.DB,都容易踩坑。
立即学习“go语言免费学习笔记(深入)”;
- 不要用
context.Value存tenant_id再一路透传到 DAO 拼 SQL——类型不安全、易漏传、异步任务(cron/消息消费)根本拿不到 - 推荐用
sync.Map缓存tenant_id → *sql.DB实例,key 是租户 ID,value 是已执行过SET search_path TO tenant_acme的连接池 - 每个
*sql.DB实例必须限制连接数:db.SetMaxOpenConns(5),否则 1000 租户 × 默认 100 连接 = 10 万连接,直接打崩 PostgreSQL - 迁移工具(如
golang-migrate)不支持按 schema 批量执行,得自己遍历租户列表,挨个执行migrate -database "postgres://?options=-c+search_path%3Dtenant_abc"
真正难的不是写代码切 schema,而是让整个团队意识到:租户边界不在中间件里,而在每一条 SQL 的执行路径上。哪怕一个后台定时任务忘了传 tenant_id,它跑出来的数据就可能是跨租户污染的。


















