多租户的两种思路
做 SaaS 或集团型后台,多租户是绕不开的坎。市面上最常见的方案是在每张业务表里加一个 tenant_id 字段,查询时永远带 WHERE tenant_id = ?。这种"行级隔离"写起来简单,但隐患不少——哪天有人 JOIN 漏了条件,数据就串了。
XYGo Admin 选了一条不同的路:租户专属表族。平台管理员体系用 xy_admin_* 表,租户体系用 xy_tenant_* 表,两套表结构物理分离。
表级隔离到底长什么样
直接看表结构就能感受到差异:
平台表(管理员体系) 租户表(租户体系)
xy_admin_user xy_tenant_admin
xy_admin_role xy_tenant_role
xy_admin_menu xy_tenant_menu
xy_admin_dept xy_tenant_dept
xy_admin_post xy_tenant_post
共享资源表(如附件表)则通过 tenant_id = 0 标记平台级数据,大于 0 归属对应租户。整个设计在数据库层面就一目了然:哪些是平台数据、哪些是租户数据,运维时不用猜。
五个维度看表级隔离的优势
1. 数据安全边界更清晰。 租户管理员、角色、菜单等核心数据跑在独立表里,从根本上杜绝了"忘加 WHERE 条件导致跨租户泄漏"这类事故。物理隔离的兜底能力不是代码评审能替代的。
2. 索引效率更高。 独立表的自增主键就是天然聚簇索引,不需要每张表都维护 (tenant_id, id) 联合索引。租户数据量越大,这个差异越明显。
3. 字段与功能独立演进。 租户菜单可以和平台菜单拥有不同的字段结构。比如平台端加一个"审计级别"字段,租户端的表完全不受影响,反之亦然。
4. 数据迁移与备份粒度更细。 导出 xy_tenant_* 相关表就能完整迁移一个租户,不需要从混合表中按条件抽取,运维效率高一个量级。
5. 诊断排错更直观。 线上出了问题,看表名就知道该查租户侧还是平台侧。
租户识别是怎么串起来的
请求进来后,TenantResolve 中间件按优先级解析租户身份:
- Header
X-Tenant-Id(前端显式传,优先级最高) - 域名匹配(查
xy_tenant.domain,适合独立域名场景) - JWT 中的 TenantId(已登录用户的 Token 里自带)
解析结果为 0 表示平台级请求,不做租户过滤。整个中间件链是:
CORS → ResponseHandler → TenantResolve → TenantAdminAuth → DemoGuard → Controller
租户管理员使用独立的 JWT 签发逻辑(token.GenerateTenant / token.ParseTenant),与平台管理员 Token 互不冲突。租户登录走 /tenant/auth/login,查 xy_tenant_admin 表校验身份。
什么时候该用哪种方案
| 方案 | 隔离粒度 | 数据安全 | 运维成本 |
|---|---|---|---|
| 独立数据库 | 物理级 | 最高 | 高 |
| 独立 Schema | 逻辑级 | 高 | 中 |
| 租户专属表族 | 表级 | 高 | 低 |
tenant_id 列 | 行级 | 中 | 低 |
如果你的项目租户数在几十到几百之间,且对数据隔离有硬性要求(金融、医疗、政务),租户专属表族是性价比最高的选择。租户数上万时,可能需要考虑独立数据库或 Schema 方案。
小结
多租户不是"加个字段就完事"的工程决策。XYGo Admin 的租户专属表族方案在安全性、性能和运维友好度之间找到了一个很好的平衡点。如果你也在基于 GoFrame + Vue3 做后台项目,这套设计值得参考。
更多细节可查阅 XYGo Admin 官方文档,项目已基于 MIT 协议开源。