多租户系统数据库应该如何设计?共享库、独立库和 tenant_id 怎么选?
简化版
多租户数据库设计要在隔离性、成本、运维复杂度之间取舍。小中型 SaaS 常用共享库共享表加 tenant_id,大客户或强隔离场景用独立库/独立 schema;无论哪种方案,都要把租户隔离做到查询条件、唯一约束、索引、权限和备份恢复里。
详细版
常见多租户方案:
- 共享库共享表:所有租户在同一张表,用
tenant_id隔离,成本低,但隔离弱。 - 共享库独立 schema:数据库实例共享,schema 隔离,隔离性中等。
- 独立库:每个租户独立数据库,隔离强,但成本和运维复杂度高。
共享表设计时,几乎所有业务表都要带 tenant_id,联合唯一约束也要带租户维度,比如 (tenant_id, user_name)。查询必须默认注入租户条件,否则容易串数据。
面试回答重点不是只说“加 tenant_id”,而是讲清楚隔离模型、索引约束、权限控制、数据迁移和大租户拆分。
完整版教学
一、多租户设计是在隔离和成本之间取舍
多租户系统的核心问题是:多个客户共用一套系统,但彼此数据不能混。隔离越强,安全性和定制能力越好,成本和运维复杂度也越高。
比如 1000 个小客户,每个客户只有几百条数据,如果每个客户都建一个数据库,连接数、备份、迁移、监控都会很重。反过来,一个金融大客户可能要求独立数据库、独立密钥、独立备份策略。
所以多租户没有唯一标准答案,必须结合租户数量、数据规模、合规要求和运维能力选择。
记忆钩子:多租户不是“加 tenant_id”四个字,而是一整套隔离边界设计。
二、三种主流模型要能对比清楚
面试里最常被问的是共享表、独立 schema、独立库怎么选。可以用一张表讲清楚。
| 方案 | 隔离性 | 成本 | 适合场景 |
|---|---|---|---|
| 共享库共享表 | 低到中 | 低 | 标准 SaaS、小租户多 |
| 共享库独立 schema | 中 | 中 | 中等隔离、租户数可控 |
| 独立库 | 高 | 高 | 大客户、合规、强定制 |
共享表最常见,因为成本低、开发简单、统计方便。独立库最安全,但要解决版本升级、批量迁移、连接管理和跨租户分析问题。
一个成熟系统也可能混合使用:普通租户共享表,大客户独立库,这就需要租户路由层。
三、共享表方案的关键是 tenant_id 全链路兜住
共享表方案下,tenant_id 是隔离边界。大多数业务表都要有 tenant_id,并且查询、更新、删除都必须带租户条件。
select *
from project
where tenant_id = ?
and id = ?;
只按 id 查询是危险的。即使主键全局唯一,也会让代码习惯绕过租户条件,后续一旦出现导入数据、迁移数据或权限漏洞,就可能串租户。
很多团队会在 ORM、数据访问层或 SQL 拦截器里自动注入 tenant_id,但关键接口仍要做测试,不能只靠约定。
四、唯一约束和索引也要带租户维度
多租户下,“唯一”通常是租户内唯一,而不是全平台唯一。比如用户名、角色名、项目编码,在 A 租户和 B 租户里可以重复。
错误设计:
unique(user_name)
正确设计通常是:
unique(tenant_id, user_name)
index(tenant_id, created_at)
index(tenant_id, status, updated_at)
这样既保证租户内唯一,也让按租户过滤的查询能走索引。否则所有查询都带 tenant_id,但索引没有把它放进去,性能会越来越差。
五、租户隔离不只在数据库字段里
很多事故不是因为没设计 tenant_id,而是某个入口忘了加条件。比如导出接口、后台管理接口、异步任务、搜索索引、缓存 key、消息消费都可能绕过租户隔离。
隔离要覆盖这些层:
HTTP 请求 -> 租户解析 -> 权限校验 -> SQL tenant_id 条件
缓存 key -> tenant:{tenant_id}:resource:{id}
消息 -> payload 携带 tenant_id
搜索索引 -> tenant_id 过滤
日志审计 -> 记录 tenant_id
如果缓存 key 不带租户,A 租户查到的数据可能被 B 租户复用,这和数据库串租户一样严重。
六、大租户和迁移要提前留出口
共享表方案早期很香,但某个大租户数据量变大后,会影响其他租户。比如一个租户占全表 60% 数据,慢查询、备份、归档都会被它拖慢。
所以设计时要考虑“大租户拆分”。租户元数据表可以记录路由信息:
tenant_id | storage_mode | db_group
1001 | shared | cluster_a
9001 | dedicated | cluster_vip_1
应用层通过租户路由决定访问共享库还是独立库。即使初期不做独立库,也要避免把租户信息写死在代码里。
七、常见误区与追问
- 误区:多租户就是每张表加 tenant_id。
tenant_id只是共享表方案的一部分,还要覆盖索引、约束、缓存、搜索、消息和权限。 - 误区:独立库一定最好。 独立库隔离强,但升级、备份、监控、连接和成本都会上升。
- 误区:主键全局唯一就不用 tenant_id 条件。 租户条件是安全边界,不只是定位数据的手段。
- 追问:租户内唯一怎么设计? 用联合唯一约束,如
(tenant_id, user_name)或(tenant_id, project_code)。 - 追问:缓存怎么避免串租户? key 中必须带租户维度,缓存读写和失效都按同一规则。
- 追问:大租户怎么拆? 通过租户路由表把大租户迁到独立库或独立分片,应用层按路由访问。
八、面试中可以这样给方案
如果是普通 SaaS,可以说初期采用共享库共享表,所有业务表带 tenant_id,联合唯一约束和索引都以 tenant_id 开头,数据访问层统一注入租户条件。
create table project (
id bigint primary key,
tenant_id bigint not null,
code varchar(64) not null,
name varchar(128) not null,
created_at timestamp not null,
unique (tenant_id, code)
);
同时补充:缓存、消息、搜索索引都携带 tenant_id;后续通过租户路由支持大客户独立库。这个答案既有当下实现,也有演进路径。
九、加强记忆
多租户设计记住“模型选择、字段隔离、约束索引、全链路防串、迁移出口”。共享表低成本但要严守 tenant_id,独立库强隔离但运维成本高。面试时不要停在“加字段”,要把查询、缓存、消息、搜索和大租户拆分一起讲出来。