如何实现一个多租户 SaaS 系统:从字段级到实例级隔离
约 4454 字大约 15 分钟
2026-07-01
引言:为什么多租户隔离不只是加一个字段
很多教程在讲多租户时,会告诉你"给每张表加一个 tenant_code 字段,查询时带上条件"。这确实是最低成本的起点,但离一个可长期维护的 SaaS 系统还差得很远。
在真实业务里,只靠业务代码手动加租户条件,至少会遇到四类问题:
- 遗漏风险:新人写接口时漏写
where tenant_code = ?,导致 A 租户看到 B 租户的数据。 - 边界丢失:后台跑批任务、数据导出、定时报表没有 HTTP 请求上下文,不知道该用哪个租户。
- 共享语义混乱:平台级字典、公共模板、运营配置和租户私有数据混在一起,每个接口都要判断"谁能看到什么"。
- 扩展性瓶颈:当大客户要求独立部署、独立备份,或强合规要求物理隔离时,系统无法平滑升级,只能推倒重做。
所以,好的多租户设计要同时解决五个问题:
- 存储隔离:不同租户的数据在物理或逻辑上如何分开。
- 查询注入:租户过滤条件在哪里、由谁来加。
- 上下文传递:租户信息如何在服务间、任务间不丢失。
- 共享语义:平台公共数据和租户私有数据如何共存。
- 可扩展性:从小客户共享到头部客户独享,能否渐进式演进。
这篇文章从架构层面讲清楚三种主流隔离方案——字段级、Schema 级、实例级——分别解决什么问题、适合什么场景、落地时要注意什么,以及它们如何在一个系统里共存。
第一部分:字段级隔离
字段级隔离是所有 SaaS 平台最先采用的方案。所有租户共享同一个数据库、同一套连接池,数据通过 tenant_code 字段区分归属。
为什么字段级隔离是主流起点
不是因为它是最好的方案,而是因为它是成本最低、扩展性最好的方案。对初创 SaaS 来说,真正的痛点不是"隔离够不够强",而是"能不能低成本地服务大量小客户"。字段级隔离恰好满足这一点。
它适合的场景包括:
- 租户数量多,但单租户数据量不大。
- 成本敏感,希望用一套数据库支撑所有租户。
- 跨租户统计、运营分析需求频繁,希望直接在同一张表上做聚合。
- 运维团队小,不希望为每个租户维护独立 Schema 或实例。
字段级隔离的架构要点
字段级隔离能规模化,关键在于"自动注入"四个字。如果靠业务代码在每个查询里手写租户条件,系统越大越危险。因此,必须在 ORM 层或数据访问层统一处理。
核心设计通常是三层:
- 统一入口注入上下文:在 HTTP 中间件或 RPC 拦截器里,从请求头读取租户标识,构造用户上下文并写入
context.Context。 - 业务层按上下文取连接:Repo 层统一调用类似
GetTenantConn(ctx)的接口,保证拿到的连接已经绑定了当前租户上下文。 - ORM 层自动注入过滤条件:模型中的租户字段使用自定义字段类型,在 GORM 的
QueryClauses、CreateClauses等回调里自动追加tenant_code = ?或填充字段。
这样,业务代码写 db.Where("id = ?", 100).First(&device),实际执行的 SQL 会自动变成 SELECT ... WHERE id = 100 AND tenant_code = 'tenant_a'。
字段级隔离的优势
- 成本最低:一套数据库、一套连接池,新增租户几乎零成本。
- 扩展性最好:理论上租户数量只受限于单库容量。
- 开发效率高:业务代码无感知,权限判断下沉到 ORM。
- 跨租户分析容易:同一张表可以直接按
tenant_code聚合、对比、统计。
字段级隔离的局限与应对
它的核心问题是隔离性弱。当租户数量或单租户数据量增长到一定规模时,会出现三类问题:
| 问题 | 表现 | 应对思路 |
|---|---|---|
| 数据泄露风险 | 漏写 where 或后台任务用错上下文 | ORM 层强制注入 + 审计日志 + 查询 review |
| 单表过大 | 查询变慢、索引膨胀、备份耗时 | 分表/分区/归档 + 历史数据冷备 |
| 无法单租户备份 | 大客户的合规诉求无法满足 | 逻辑导出按 tenant_code 过滤,或升级到 Schema/实例级 |
字段级隔离不是银弹,但它是最适合验证商业模式初期的方案。当业务跑通、客户分层清晰后,再引入 Schema 级或实例级隔离来服务不同层级的客户。
第二部分:Schema 级隔离
Schema 级隔离是字段级隔离的进阶方案。所有租户仍然共享同一个数据库实例和连接池,但每个租户拥有独立的 Schema(命名空间),表结构相同,数据完全分开。
为什么需要 Schema 级隔离
字段级隔离的隔离强度对一些中等规模的客户来说不够。这些客户通常有以下诉求:
- 数据逻辑上与其他租户分开,便于审计和合规解释。
- 可以单独备份、恢复自己的数据。
- 希望将来能够平滑迁移到独立实例。
直接给每个租户一个独立数据库实例,成本又太高。Schema 级隔离正好卡在中间:比字段级更安全,比实例级更便宜。
它适合的场景包括:
- 中大型客户占比高,对隔离性有明确要求。
- 使用 PostgreSQL 等原生支持 Schema 的数据库。
- 租户数量适中,不需要几千几万个独立 Schema。
- 愿意投入自动化迁移和结构同步工具的运维成本。
Schema 级隔离的架构要点
Schema 级隔离的关键是"共享连接池,动态切换命名空间"。同一个数据库连接可以通过限定表名访问不同 Schema,不需要为每个 Schema 单独建物理连接。
架构上通常是:
- 连接池复用:底层仍然复用一套
commonConn连接池。 - 动态表前缀:按租户代码创建对应的
*gorm.DB实例,设置TablePrefix = tenantCode + "."。 - Schema 自动同步:新增租户或系统升级时,自动在所有 Schema 里创建/更新表结构。
- Repo 层切换:业务代码本身不变,只需在取连接时决定用字段级连接还是 Schema 级连接。
业务代码写 db.Where("id = ?", 100).First(&device),实际执行的 SQL 会变成 SELECT * FROM tenant_a.device WHERE id = 100。这里没有 tenant_code 条件,因为数据本身就在 tenant_a Schema 里。
Schema 级隔离的优势
- 隔离性较好:数据逻辑上完全分开,审计和合规解释更容易。
- 成本可控:共享数据库实例和连接池,资源利用率高于实例级。
- 备份粒度细:可以按 Schema 导出,满足中等客户的备份诉求。
- 迁移路径清晰:是字段级到实例级的自然过渡。
Schema 级隔离的局限与应对
| 问题 | 表现 | 应对思路 |
|---|---|---|
| Schema 管理复杂 | 升级时要同步所有 Schema 的表结构 | 自动化迁移工具 + 分批执行 |
| 跨租户查询困难 | 数据分散在不同 Schema | 建立汇总表/数据仓库/定时 ETL |
| 数据库兼容性问题 | MySQL 的 Schema 语义较弱 | MySQL 场景改用独立数据库方案 |
Schema 级隔离是字段级和实例级之间的桥梁。当客户愿意为更好的隔离性多付一些成本,但又不需要物理隔离时,这是最平衡的选择。
第三部分:实例级隔离
实例级隔离是最强隔离方案。每个租户拥有独立的数据库实例,甚至独立的数据库主机,租户之间在物理层面完全隔离。
为什么需要实例级隔离
当客户出现以下特征时,字段级和 Schema 级都无法满足:
- 金融、医疗、政务等强监管行业,要求物理隔离和独立审计。
- 头部大客户数据量极大,共享实例会影响性能。
- 客户有数据主权要求,数据必须部署在指定区域或指定主机。
- 客户愿意为隔离性支付高溢价。
实例级隔离的架构要点
实例级隔离的核心是"DSN 模板化 + 连接池按租户管理"。系统不再使用统一的 DSN,而是根据租户代码动态生成连接配置。
架构上通常是:
- DSN 模板化:配置中使用占位符,例如
{{.tenantUser}}:{{.tenantPass}}@tcp({{.tenantHost}}:{{.tenantPort}})/{{.tenantCode}}?charset=utf8mb4。 - 租户到连接池映射:维护一个租户到
*gorm.DB的缓存。首次访问时按租户 DSN 创建独立连接池。 - 连接池生命周期管理:租户开通时初始化,解约或实例下线时清理,避免连接泄漏。
- 平台级统计需要额外机制:跨租户数据聚合需要单独的数据仓库或同步链路。
实例级隔离的优势
- 隔离性最强:物理层面分开,合规审计最容易通过。
- 性能最可控:不受其他租户影响,可以独立扩缩容。
- 备份恢复最灵活:可以按实例做完整备份、按时间点恢复。
实例级隔离的局限与应对
| 问题 | 表现 | 应对思路 |
|---|---|---|
| 成本高 | 每个实例都要独立资源 | 只卖给头部大客户,按实例收费 |
| 运维复杂 | 监控、补丁、扩缩容按实例处理 | 自动化运维平台 + 实例分组管理 |
| 资源利用率低 | 小租户也占用完整实例 | 小租户仍用字段级/Schema 级 |
实例级隔离适合"少而贵"的头部客户。真正成熟的 SaaS 平台不会让所有租户都用实例级,而是把它作为最高端选项。
第四部分:三种隔离方案对比与选型
横向对比
| 维度 | 字段级 | Schema 级 | 实例级 |
|---|---|---|---|
| 隔离强度 | 弱 | 中 | 强 |
| 成本 | 低 | 中 | 高 |
| 运维复杂度 | 低 | 中 | 高 |
| 扩展性 | 好 | 中 | 差 |
| 跨租户查询 | 容易 | 较难 | 很难 |
| 单租户备份 | 不支持 | 支持 | 支持 |
| 适用租户数 | 多 | 中 | 少 |
选型决策树
渐进式演进路径
一个 SaaS 平台很少从第一天就选择最强隔离。更常见的演进路径是:
- 初创期:字段级隔离,验证商业模式,低成本获客。
- 成长期:引入 Schema 级隔离,服务对隔离性有要求的中型客户。
- 成熟期:为头部客户提供实例级隔离,形成高客单价产品线。
- 最终形态:三种方案共存,按客户层级自动分配。
第五部分:租户语义细化
字段隔离解决的是"数据属于谁",但真实业务还需要解决"谁能看到什么"。一个 SaaS 系统里通常同时存在三类数据:
- 租户私有数据:设备、项目、订单等业务数据,只能当前租户看到。
- 平台共享数据:字典、协议模板、公共配置,所有租户可以读取,但写入受限。
- 平台专属数据:运营配置、系统级参数、平台菜单,只有平台管理员能看到。
为什么需要语义细化
如果没有语义细化,这三种数据的区别只能写在业务代码的 if/else 里。接口越多,判断越分散,最终变成"每个接口都要重新发明一次权限逻辑"。
正确的做法是把语义下沉到模型字段类型或 ORM 层:
- 普通租户私有字段:查询和写入都限定当前租户。
- 公共可读字段:查询时允许读取平台共享数据,但写入只能写自己租户。
- 公共可写字段:普通租户也可以向平台共享数据写入。
- 平台专属字段:非平台管理员查询时自动过滤。
这样,新增业务模型时,开发者只需要选择合适的字段语义类型,就能自动获得对应的权限行为,而不是在每个接口里重复判断。
第六部分:跨服务上下文透传
多租户系统往往不是单服务架构。一个请求可能经过:网关 → 用户服务 → 设备服务 → 时序数据库。如果租户上下文只在入口设置,后面就会丢失。
为什么上下文透传是基础设施
跨服务调用时,context 不会自动传递。必须显式把 UserCtx 序列化后放到 RPC metadata 里,接收方再反序列化注入本地 context。
上下文透传的三层
- HTTP 层:中间件从 Header 读取租户和应用信息,构造
UserCtx写入 context。 - RPC 层:调用方将
UserCtx序列化并编码后写入 gRPC metadata;接收方解析后注入本地 context。 - 内部任务层:定时任务、后台任务没有 HTTP 请求,需要显式绑定租户上下文,或提升为 root 上下文执行跨租户操作。
没有透传机制会怎样
- 服务 A 查询的是租户 A 的数据,调用服务 B 时变成默认租户或空租户。
- 后台报表把所有租户数据混在一起统计。
- 平台级操作误删租户数据,或租户操作看到平台数据。
上下文透传看起来是个小机制,但它决定了多租户系统能不能在微服务架构下正确工作。
结语:多租户设计的真正难点
多租户数据隔离不是"选一个方案"那么简单。真正成熟的设计要满足三个条件:
- 隔离与共享的平衡:既能保护租户私有数据,又能共享平台公共数据。
- 可渐进演进:从字段级到 Schema 级再到实例级,不需要推倒重来。
- 基础设施化:租户过滤、上下文透传、语义细化都下沉到框架层,业务代码无感知。
字段级隔离成本低、扩展性好,适合大多数 SaaS 场景;Schema 级隔离提供了更好的隔离性和备份粒度,适合中等规模客户;实例级隔离最强,但成本最高,适合头部大客户和强合规场景。
联犀的设计正是为了让这三种方案能够共存:连接层统一抽象、ORM 层自动注入租户过滤、上下文层保证跨服务透传。业务代码不需要关心底层是哪种隔离级别,只需要按正确的方式使用 context 和连接,就能获得稳定、安全的多租户能力。
如果你想了解多租户之上的多应用体系如何设计,可以参考下一篇文章:如何实现一个多应用的 SaaS 平台:通用应用、平台应用与定制应用。
更新日志
2026/7/4 21:43
查看所有更新日志
8044c-docs: 同步未提交文档变更于
