2. 多租户模型设计
多租户模型设计是SaaS运营后台的地基。它要回答两个问题:第一,租户、用户、组织、角色、权限这五类核心对象之间是什么关系;第二,不同租户的数据在同一套系统里如何做到互不可见、互不干扰。前者决定数据模型能否支撑业务演进,后者决定隔离策略能否在成本、安全与运维复杂度之间取得平衡。
五类核心对象的关系
先厘清对象语义。租户(Tenant)是SaaS中独立的商业主体,是计费、配额、数据归属的最高边界。用户(User)是自然人身份,一个自然人可能同时属于多个租户,因此用户与租户之间通常是多对多关系,通过“租户成员(Membership)”这张关联表落地。组织(Organization)是租户内部的部门树,用于表达汇报线与数据归属范围。角色(Role)是权限的集合,是“一组能做什么”的命名封装。权限(Permission)是最小授权单元,通常以“资源:动作”形式表达,例如 order:read、order:refund。
它们的关系可以这样理解:租户是容器,组织是容器内的层级骨架,用户通过成员关系进入容器并被挂到某个组织节点上,角色把权限打包后授予成员,最终形成“某人在某租户的某组织节点上拥有某组权限”的完整授权链。这条链上任何一环缺失,鉴权都会失败。
隔离策略的三种典型选型
独立数据库:每个租户一套库。隔离性最强,单租户故障不影响他人,适合金融、医疗等强合规场景。代价是连接池数量随租户线性增长,跨租户统计需要额外汇聚层,运维成本高。
共享库、独立Schema:同一数据库实例内,每个租户一个Schema。隔离性中等,备份恢复可按Schema粒度操作,连接池压力可控。适合租户数量在数百到数千、且对隔离有明确要求的场景。
共享表、tenant_id行级隔离:所有租户数据在同一张表,靠 tenant_id 列区分。成本最低、扩展性最好,但隔离完全依赖应用层与数据库策略,一旦漏加条件就会造成越权。适合租户量大、单租户数据量小、以标准化SaaS为主的场景。
实际工程中常采用混合策略:核心敏感数据独立库,普通业务数据共享表。选型时重点评估四个维度——合规要求、租户规模、单租户数据量、团队运维能力。
数据模型落地要点
以共享表方案为例,关键表结构如下:
-- 租户表
CREATE TABLE tenant (
id BIGINT PRIMARY KEY,
name VARCHAR(128) NOT NULL,
plan VARCHAR(32) NOT NULL DEFAULT 'basic',
status TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL
);
-- 用户表(全局,不绑定租户)
CREATE TABLE user (
id BIGINT PRIMARY KEY,
email VARCHAR(128) NOT NULL UNIQUE,
nickname VARCHAR(64),
created_at DATETIME NOT NULL
);
-- 租户成员表:用户进入租户的入口
CREATE TABLE membership (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
org_id BIGINT,
status TINYINT NOT NULL DEFAULT 1,
UNIQUE KEY uk_tenant_user (tenant_id, user_id)
);
-- 组织表:租户内部门树
CREATE TABLE organization (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
parent_id BIGINT DEFAULT 0,
name VARCHAR(128) NOT NULL,
path VARCHAR(512) NOT NULL
);
-- 角色表
CREATE TABLE role (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
code VARCHAR(64) NOT NULL,
name VARCHAR(64) NOT NULL,
UNIQUE KEY uk_tenant_code (tenant_id, code)
);
-- 权限表(全局定义)
CREATE TABLE permission (
id BIGINT PRIMARY KEY,
code VARCHAR(128) NOT NULL UNIQUE,
resource VARCHAR(64) NOT NULL,
action VARCHAR(32) NOT NULL
);
-- 角色权限关联
CREATE TABLE role_permission (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id)
);
-- 成员角色关联,scope_org_id 支持数据范围授权
CREATE TABLE member_role (
member_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
scope_org_id BIGINT DEFAULT 0,
PRIMARY KEY (member_id, role_id)
);
这里有两个设计决策值得注意。第一,user 表不携带 tenant_id,因为一个自然人可能同时是多个租户的成员,把租户信息放在成员表里才能正确表达多对多。第二,member_role 中的 scope_org_id 用于表达数据范围:当它为0时表示全租户范围,为某个组织ID时表示仅该组织及其子树可见。这样同一角色在不同组织节点上可以产生不同的数据可见范围。
path 字段建议存成 /1/5/12/ 这样的物化路径,查询某节点及其所有子孙时用 WHERE path LIKE '/1/5/%' 即可,避免递归查询。鉴权链与隔离校验
一次请求的鉴权应沿着“用户 → 成员 → 角色 → 权限 → 数据范围”这条链逐级校验。典型流程是:先由登录态解析出 user_id,再根据请求头或路由中的 tenant_id 查出对应的 membership,确认成员状态有效;然后加载该成员的角色集合,合并出权限码集合;最后判断目标资源是否在 scope_org_id 覆盖的数据范围内。
隔离校验的关键在于:所有业务查询都必须带上 tenant_id 条件,且这个条件不能由前端传入,而应由服务端从已认证的成员上下文中强制注入。常见做法是在数据访问层统一拦截,把 tenant_id 作为隐式过滤条件拼进SQL,避免开发者遗漏。
选型决策参考
| 维度 | 独立数据库 | 独立Schema | 共享表 tenant_id |
|---|---|---|---|
| 隔离强度 | 最高 | 较高 | 依赖应用层 |
| 单租户成本 | 高 | 中 | 低 |
| 扩展租户数 | 受限 | 数百至数千 | 数万以上 |
| 跨租户统计 | 困难 | 较困难 | 容易 |
| 适用场景 | 强合规大客户 | 中型SaaS | 标准化SaaS |
模型设计完成后,建议用一组最小验证用例确认隔离有效:创建租户A和租户B,各自建一个用户并授予相同角色;用A的用户登录后尝试读取B的订单,预期返回空或403;再尝试用A的成员身份访问B的组织节点,预期被拒绝。只有这两类交叉访问都被正确拦截,隔离策略才算真正落地。