1、SaaS数据模型设计:多租户架构概述、三种主流租户隔离方案与数据路由
大家好,我是你们的老朋友。今天咱们来聊聊SaaS系统里最核心、也最让人头疼的问题——数据怎么存。
你想想看,一个SaaS平台要服务成百上千个客户(也就是租户),每个客户的数据都得安全隔离,不能串。但你又不能给每个客户单独部署一套数据库,那成本谁也扛不住。怎么办?
嗯,这就是多租户数据架构要解决的核心矛盾。我个人习惯把这个问题拆成两个层面来看:隔离粒度和路由效率。今天咱们就把这三个主流方案掰开揉碎了讲清楚。
1.1 多租户架构:到底在解决什么问题?
说白了,多租户架构就是「一套系统,服务N个客户」。每个客户看到的都是自己的专属系统,但底层其实是共享的。
我在项目中遇到过最典型的场景:一个客户突然说「我们公司数据特别敏感,能不能物理隔离?」另一个客户却说「我们预算有限,能省就省吧」。你看,需求完全相反。
所以,没有银弹。你得根据客户画像、合规要求、预算水平,选择不同的隔离方案。
1.2 三种主流租户隔离方案
业界公认的三种方案,我按隔离强度从高到低给你排个序:
| 方案 | 隔离级别 | 共享程度 | 运维成本 | 典型场景 |
|---|---|---|---|---|
| Database per Tenant | 最高 | 几乎不共享 | 高 | 金融、医疗、大客户 |
| Schema per Tenant | 中等 | 共享实例,隔离Schema | 中 | 中型企业、行业SaaS |
| Shared Table | 最低 | 完全共享 | 低 | 小微企业、C端工具 |
方案一:Database per Tenant(独立数据库)
这个方案最暴力,也最安全。每个租户一个独立的数据库实例。数据完全物理隔离,备份恢复互不影响。
优点:
- 数据隔离性最好,一个租户出问题不影响别人
- 可以针对大客户单独做性能调优、备份策略
- 迁移、扩容非常灵活
缺点:
- 连接数爆炸。1000个租户就是1000个数据库连接池
- 运维成本高。升级一次数据库版本,要跑1000次脚本
- 资源利用率低。小租户可能只用了1%的容量
方案二:Schema per Tenant(共享实例,隔离Schema)
这个方案是我个人比较推荐的折中方案。所有租户共享同一个数据库实例,但每个租户拥有独立的Schema(在MySQL里就是独立的Database)。
你想想看,这样既保留了逻辑隔离,又避免了连接数爆炸。一个实例可以轻松承载几百个租户。
优点:
- 连接数可控,一个实例一个连接池
- 数据逻辑隔离,查询时天然带Schema前缀
- 备份恢复可以按租户粒度操作
缺点:
- 实例级别的故障会影响所有租户
- 跨租户统计查询比较麻烦
- Schema数量过多时,元数据管理会变慢
tenant_1001、tenant_1002。这样一眼就能看出是哪个租户,排查问题特别方便。
方案三:Shared Table(共享表)
这个方案最省事,也最省钱。所有租户的数据放在同一张表里,通过一个 tenant_id 字段来区分。
说白了,就是一张表里既有A公司的数据,也有B公司的数据。嗯,这里要注意,所有查询都必须带上 tenant_id 条件,否则数据就串了。
优点:
- 资源利用率最高,几乎没有浪费
- 开发最简单,不需要动态建库建表
- 扩容方便,加机器就行
缺点:
- 数据隔离最弱,一个SQL写错就可能泄露数据
- 索引压力大,一张表可能存几亿行数据
- 租户数据删除困难,物理删除影响性能
tenant_id 索引,结果一个租户的查询把整个库拖垮了。记住:共享表方案,索引设计是第一优先级。
1.3 租户上下文与数据路由
不管选哪种方案,你都得解决一个问题:怎么知道当前请求是哪个租户?
这就是租户上下文(Tenant Context)要做的事。我习惯在请求入口处(比如Filter或Interceptor)解析租户信息,然后塞到ThreadLocal里。这样整个请求链路都能拿到。
数据路由就更直接了。根据租户ID,决定数据往哪个库、哪个表、哪个Schema里写。
来看一个简单的路由示例:
// 伪代码:根据租户ID选择数据源
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从ThreadLocal获取当前租户ID
String tenantId = TenantContext.getCurrentTenant();
// 根据租户ID映射到对应的数据源Key
return "datasource_" + tenantId;
}
}
这段代码的核心逻辑就一句话:请求来了,先认人,再认路。
核心要点总结:
- 隔离方案没有绝对的好坏,只有合不合适
- 大客户用独立库,中型客户用独立Schema,小客户用共享表
- 租户上下文一定要在请求入口处统一处理,别散落在业务代码里
- 数据路由的Key(tenant_id)必须全局唯一且稳定
好了,这一章的内容就到这里。下一章咱们会深入聊聊「分库分表」的具体实现,包括怎么选分片键、怎么处理跨分片查询。到时候我会分享一个我踩过的坑——分片键选错了,导致全表扫描,差点被老板骂死。敬请期待!