多租户实现
约 1993 字大约 7 分钟
2025-03-03
多租户这件事,最容易被讲成“数据库该怎么分”。
但真正做 SaaS 平台时会发现,光选一种存储隔离模式远远不够。
系统最终要回答的不只是:
- 这条数据属于哪个租户?
还要回答:
- 这个租户里的哪个用户能访问它?
- 公共资源和私有资源怎么共存?
- 查询服务如何在不重写业务逻辑的前提下完成权限过滤?
联犀当前的多租户实现,就是围绕这些问题逐层收敛出来的。
多租户不是只有三种数据库方案
从数据库角度看,多租户确实常被分成三类:
- 独立数据库
- 共享数据库、独立 Schema
- 共享数据库、共享 Schema、共享表
联犀当前采用的是第三种,也就是共享表模型,通过 tenant_code 区分租户数据。
选择这条路并不是因为它“最先进”,而是因为它更适合当前 SaaS + IoT 场景的几个现实约束:
- 租户数量可能很多
- 关系型数据并不一定大到必须独库
- 跨租户统计和统一能力运营是刚需
- 增加新租户的成本要尽可能低
如果只从数据库教科书角度看,共享表的隔离级别确实不是最高。
但工程里真正重要的不是某个维度绝对最强,而是整体成本、复杂度和业务目标的平衡。
仅靠 tenant_code 远远不够
很多人一提共享表多租户,第一反应就是:
“所有表加一个 tenant_code 字段,然后查询时都带上 where 条件。”
这当然是起点,但如果系统只停在这里,后面几乎一定会失控。
原因很简单:多租户系统里的权限问题通常分成三层。
第一层:租户隔离
先判断这条数据是不是当前租户的数据。
第二层:业务范围控制
即便在同一租户内部,也还可能要按项目、区域、角色继续细分权限。
第三层:查询级裁剪
有些数据不是普通 CRUD,而是统计查询、时序查询、跨表查询。
这类查询如果仍靠业务逻辑手工拼权限条件,最终会非常难维护。
联犀当前就是把这三层分开处理,而不是把所有事情都堆到每个业务接口里。
第一层:底层租户语义
联犀当前在 share 层把常见租户语义做成了类型,而不是让每个表和每段代码自己解释。
比较常见的语义包括:
- 当前租户私有数据
- 当前租户可读 + 公共资源可读
- 平台专属资源
这类设计看起来像细节,实际上很重要。
因为它解决的是“公共能力如何与租户私有能力共存”的问题。
举个简单例子:
- 技能模板、通用配置、基础能力可能属于公共资源
- 用户自己的 Agent、设备、项目数据则属于租户私有资源
如果没有清晰的租户语义,代码里就会不断冒出:
- 这里要不要顺便查
common - 那里平台管理员能不能看到
- 这个资源是否允许被普通租户复用
时间一长,就会变成一堆很难统一的例外逻辑。
第二层:用户上下文传递
光知道当前租户是谁还不够。
系统还要知道当前用户在这个租户里拥有什么权限。
联犀当前会在请求进入 API 网关后构建统一的用户上下文,里面除了 TenantCode,还包含:
UserIDRoleCodesProjectIDProjectAuth- 管理员标记
- 内部上下文控制字段
这套上下文会继续在 HTTP、RPC、消息等链路中透传。
这样后续服务不需要反复解析 token,也不用重新拼接“这个人属于哪个租户、哪个项目、哪个区域”。
它带来的价值很直接:
- 多个服务看到的是同一份权限上下文
- 权限判断可以下沉到基础组件和中间层
- 业务代码只关心“拿上下文做判断”,而不是到处复制认证逻辑
第三层:查询服务里的权限裁剪
如果系统只做普通表查询,前两层已经能解决很多问题。
但联犀里还有一类很关键的能力:数据查询服务。
这类服务面对的不是单一业务接口,而是可配置查询、统计查询、时序查询。
如果这时还要求每个查询逻辑自己手写租户、项目、区域过滤,复杂度会迅速失控。
所以联犀当前把查询权限进一步下沉到了 datasvr 的配置模型中。
查询配置里会显式声明:
- 是否按租户过滤
- 固定追加哪些预过滤条件
- 哪些角色才允许使用
- 是否需要走动态权限 Hook
这就意味着,系统不再把所有权限逻辑写死在业务代码中,而是允许查询本身带着自己的权限语义。
为什么还需要 dataFilter Hook
即便做了查询配置,仍然会遇到一些纯配置难以覆盖的场景。
比如:
- 按项目范围过滤
- 按区域树过滤
- 某些设备需要结合上下文做更复杂的权限判断
联犀当前的做法是给 datasvr 提供 dataFilter 这种标准 Hook 点。
当查询执行时,系统会把当前查询码、请求头和上下文参数交给 Hook,再由 Hook 返回要追加的过滤条件。
这样做的好处是:
1. 数据权限和业务语义可以继续解耦
查询服务只负责“何时调 Hook、如何拼条件”,不需要内置每种业务范围的细节。
2. 可以同时覆盖关系库与时序库
因为 Hook 返回的是统一的过滤语义,后续无论走普通 ORM 查询还是 TDengine 查询,都可以继续转成实际 where 条件。
3. 新场景不必复制旧查询逻辑
新增某一类业务查询时,往往更多是复用已有权限模型,而不是再写一套新的授权判断。
公共资源与平台资源为什么要单独对待
多租户系统里还有一个很容易被忽略的问题:
并不是所有数据都严格只属于某个租户。
现实里常常存在两类特殊资源:
公共资源
比如所有租户都能读取的模板、默认配置、公共技能。
平台资源
比如只有平台管理员能读写的全局能力。
如果系统里没有明确区分这两类语义,最终会很容易把“所有人都能看”与“只有平台能看”混在一起。
联犀当前正是通过不同租户类型和上下文语义,把这两类边界拉开。
总结
联犀当前的多租户实现,并不是简单的“表里加个 tenant_code”。
它更像是一套分层模型:
- 用共享表模型解决存储与成本问题。
- 用统一用户上下文解决跨服务权限传递问题。
- 用 datasvr 配置与
dataFilterHook 解决查询级权限裁剪问题。
这套设计的关键不是某一层特别复杂,而是每一层都只做自己该做的事:
- 底层负责隔离语义
- 中间层负责权限上下文
- 查询层负责把权限翻译成真正可执行的过滤条件
当这三层配合起来以后,多租户才不只是“能查对数据”,而是真正能在复杂业务里长期维护下去。
更新日志
2026/5/18 10:48
查看所有更新日志
43ef3-docs(blog): 新增后端与架构系列技术博客 18 篇,更新原有 7 篇于d4fa0-doc: 更换皮肤到最新版于f6e1e-doc: 完善文档于f4017-doc: 完善文档于41e66-doc: 完善文档于f6cd0-feat: 完善帮助与反馈于
