一套服务要服务多个客户(公司、团队),每个客户的数据必须隔离,串了就是安全事故。多租户有三种隔离力度,按成本和合规要求选。这一篇把三种都讲清,重点给最常用的行级方案落地代码。
1. 三种方案对比
独立库:每租户一个数据库,隔离最强、成本最高。独立 schema:同库不同 schema,折中。共享表行级:所有租户一张表,每行带 tenant_id,成本最低、最容易出错。绝大多数 SaaS 用行级。
2. 行级:中间件解析租户
请求带租户标识(子域名、header、JWT claim),中间件解析后写进 context:
func Tenant() gin.HandlerFunc { return func(c *gin.Context) { tid := c.GetHeader("X-Tenant-Id") if tid == "" { c.JSON(400, gin.H{"error": "缺少租户标识"}) c.Abort() return } ctx := context.WithValue(c.Request.Context(), "tid", tid) c.Request = c.Request.WithContext(ctx) c.Next() } }
3. GORM 作用域自动带 tenant_id
每条查询手动加 Where("tenant_id = ?") 容易漏。用 GORM scope 统一注入:
func TenantScope(ctx context.Context) func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { if tid, ok := ctx.Value("tid").(string); ok { return db.Where("tenant_id = ?", tid) } return db } } // 用法 db.Scopes(TenantScope(ctx)).Find(&orders)
新建也自动填 tenant_id,从源头杜绝串租户。
4. 独立 schema 方案
同库按租户切 schema,查询前 Set:
db.Exec("SET search_path TO ?", "tenant_"+tid)
PostgreSQL 的 schema 天然适合。切换要在连接池按租户取专用连接,否则串到别的租户。
5. 独立库方案
租户映射到独立 DSN,连接池按租户路由:
func dbForTenant(tid string) *gorm.DB { dsn, ok := tenantDSN[tid] if !ok { return nil } return connect(dsn) // 带连接池缓存,别每次新建 }
隔离最彻底,适合合规要求高的金融、医疗客户。代价是连接数多、迁移要逐库跑。
6. 上线清单
租户解析必须在最外层中间件,任何请求都过。行级方案所有读写都带 tenant_id,作用域统一注入别手写。租户标识来源要做校验,不能让用户伪造访问别人数据。跨租户统计、运维操作走独立的高权限通道,不混在普通接口。独立库/schema 方案迁移脚本要逐租户执行并校验。
下一篇讲集成测试,用真实依赖替掉 mock,把上面这些都测住。