先说结论
多租户有两条路:DB-per-tenant(一户一个库)和共享库行级隔离(一张表加 tenant_id)。前者隔离彻底但运维重,后者轻但每一行查询都要正确。
城市站选了后者:一套库,所有业务表带 tenant_id。
为什么不用一户一库
| 方案 | 优点 | 代价 |
|---|---|---|
| DB-per-tenant | 隔离彻底、可单独备份/迁移 | 几十个租户就是几十个库,迁移与升级成本翻倍 |
| 共享库 + tenant_id | 一套迁移、一套备份、成本低 | 所有查询必须带租户条件,漏一处就串数据 |
对一个租户量不大、站点量极大的系统,共享库更合适:站点是数据,不是实例。
隔离的六道闸
- 查询统一入口:所有业务查询走
tenantTable(),自动追加where tenant_id = ? - 写操作双条件:更新/删除必须
id + tenant_id,防止猜 id 改别家数据 - 归属校验:保存文章时校验
site_id、category_id属于当前租户 - 上下文缺失即拒绝:
TenantContext未设置时直接抛异常,绝不"没选租户就全表查" - 媒体目录隔离:文件存在
static/upload/{tenant_id}/,删除时校验路径 - 平台功能单独鉴权:租户/域名这类平台表只有超管可写
最危险的写法
// 错:只按 id 更新,租户 A 能改租户 B 的文章
UPDATE city_articles SET title = ? WHERE id = ?
// 对:id + tenant_id
UPDATE city_articles SET title = ? WHERE id = ? AND tenant_id = ?
这类漏洞不会报错,只会在某天客户说"我后台出现了别人的数据"。
什么时候该拆库
- 单租户数据量超过总库 50%
- 客户要求数据物理隔离(合同/合规)
- 写入并发成为瓶颈
扩展点在 TenantResolver:解析出租户后切换数据库连接,业务查询代码不用改。