很多团队第一次做 SaaS,多租户设计往往是从一句话开始的:
"不就是每张表加个 tenant_id 吗?"
这句话不能说错,但非常危险。
因为 tenant_id 确实是多租户系统里最常见、最基础的字段。用户创建订单时,写入租户 ID;查询客户列表时,带上租户条件;统计报表时,按照租户聚合数据。对于一个早期 SaaS 产品来说,这套方案看起来简单、直接、成本低,也确实能快速跑起来。
但问题在于,tenant_id 只是多租户的入口,不是多租户的全部。
真正的 SaaS 多租户,解决的不是"表里有没有租户字段",而是整个系统能不能持续、稳定、安全地服务多个客户。它牵涉数据隔离、权限判断、配置差异、订阅计费、缓存设计、消息路由、文件归属、审计追踪、客户迁移、独立部署等一系列问题。
很多 SaaS 项目前期跑得很快,后期却越来越痛苦,根源往往不是业务功能太复杂,而是租户边界一开始就没有设计清楚。
早期一个查询忘记加 tenant_id,可能只是测试环境里的小 bug。到了生产环境,它可能就是严重的数据串租事故。
早期一个缓存 key 没带租户 ID,可能只是偶发权限异常。到了客户规模变大之后,它可能导致 A 客户读到 B 客户的配置。
早期配置写死在代码里,可能只是为了快速交付。等客户越来越多,每个客户都要一点差异化时,系统就会变成一堆 if else。
早期没有租户级计费模型,可能只是因为还没开始商业化。等要做套餐、试用、续费、升级、用量限制时,才发现业务逻辑到处都要改。
所以,多租户不是 SaaS 的一个小功能,而是 SaaS 的底层秩序。
这篇文章就围绕一个核心问题展开:一个看似简单的 tenant_id 背后,到底藏着怎样的系统设计战争?
一、多租户不是加个 tenant_id,而是 SaaS 的底层边界
多租户,简单说,就是一套系统同时服务多个客户,每个客户在系统中是一个相对独立的租户。
这个租户可以是一家公司、一所学校、一个门店、一个加盟商、一个项目空间,也可以是平台里的一个独立业务主体。不同 SaaS 产品对租户的定义不完全一样,但它们有一个共同点:多个客户共享同一套系统能力,同时彼此保持边界。
这里有两个关键词:共享和隔离。
SaaS 之所以能够成立,很大程度上依赖共享。共享代码、共享部署、共享基础设施、共享升级机制、共享运维体系。只有这样,服务商才能把研发和运维成本摊薄,让每新增一个客户的边际成本逐渐下降。
但 SaaS 又不能无限共享。
客户可以共享系统,但不能共享彼此的数据。A 公司不能看到 B 公司的客户资料,B 学校不能访问 C 学校的学生信息,普通套餐不能使用高级套餐功能,试用租户不能无限创建用户,大客户的特殊流程不能影响其他客户。
所以,多租户架构本质上是在解决一个矛盾:
底层尽可能共享,边界必须足够清晰。
很多人把这个边界理解成数据库里的 tenant_id,这当然是重要的一部分,但远远不够。因为租户边界不是只存在于数据库层,它会贯穿整个请求链路。
用户登录时,系统要知道他属于哪个租户。
做权限判断时,系统要知道他在当前租户下是什么角色。
查询业务数据时,系统要自动限制在当前租户范围内。
加载配置时,系统要读取当前租户的功能开关和参数。
判断套餐时,系统要知道当前租户购买了什么版本。
写审计日志时,系统要记录操作发生在哪个租户下。
使用缓存时,缓存 key 要避免不同租户互相污染。
发送消息时,异步任务要知道自己处理的是哪个租户的数据。
上传文件时,文件路径和访问权限也要带有租户归属。
可以把 SaaS 的租户边界理解为一条贯穿全系统的主线:
#mermaid-svg-6gftJYrigMK2udJb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6gftJYrigMK2udJb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6gftJYrigMK2udJb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6gftJYrigMK2udJb .error-icon{fill:#552222;}#mermaid-svg-6gftJYrigMK2udJb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6gftJYrigMK2udJb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6gftJYrigMK2udJb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6gftJYrigMK2udJb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6gftJYrigMK2udJb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6gftJYrigMK2udJb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6gftJYrigMK2udJb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6gftJYrigMK2udJb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6gftJYrigMK2udJb .marker.cross{stroke:#333333;}#mermaid-svg-6gftJYrigMK2udJb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6gftJYrigMK2udJb p{margin:0;}#mermaid-svg-6gftJYrigMK2udJb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6gftJYrigMK2udJb .cluster-label text{fill:#333;}#mermaid-svg-6gftJYrigMK2udJb .cluster-label span{color:#333;}#mermaid-svg-6gftJYrigMK2udJb .cluster-label span p{background-color:transparent;}#mermaid-svg-6gftJYrigMK2udJb .label text,#mermaid-svg-6gftJYrigMK2udJb span{fill:#333;color:#333;}#mermaid-svg-6gftJYrigMK2udJb .node rect,#mermaid-svg-6gftJYrigMK2udJb .node circle,#mermaid-svg-6gftJYrigMK2udJb .node ellipse,#mermaid-svg-6gftJYrigMK2udJb .node polygon,#mermaid-svg-6gftJYrigMK2udJb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6gftJYrigMK2udJb .rough-node .label text,#mermaid-svg-6gftJYrigMK2udJb .node .label text,#mermaid-svg-6gftJYrigMK2udJb .image-shape .label,#mermaid-svg-6gftJYrigMK2udJb .icon-shape .label{text-anchor:middle;}#mermaid-svg-6gftJYrigMK2udJb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6gftJYrigMK2udJb .rough-node .label,#mermaid-svg-6gftJYrigMK2udJb .node .label,#mermaid-svg-6gftJYrigMK2udJb .image-shape .label,#mermaid-svg-6gftJYrigMK2udJb .icon-shape .label{text-align:center;}#mermaid-svg-6gftJYrigMK2udJb .node.clickable{cursor:pointer;}#mermaid-svg-6gftJYrigMK2udJb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6gftJYrigMK2udJb .arrowheadPath{fill:#333333;}#mermaid-svg-6gftJYrigMK2udJb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6gftJYrigMK2udJb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6gftJYrigMK2udJb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6gftJYrigMK2udJb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6gftJYrigMK2udJb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6gftJYrigMK2udJb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6gftJYrigMK2udJb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6gftJYrigMK2udJb .cluster text{fill:#333;}#mermaid-svg-6gftJYrigMK2udJb .cluster span{color:#333;}#mermaid-svg-6gftJYrigMK2udJb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6gftJYrigMK2udJb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6gftJYrigMK2udJb rect.text{fill:none;stroke-width:0;}#mermaid-svg-6gftJYrigMK2udJb .icon-shape,#mermaid-svg-6gftJYrigMK2udJb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6gftJYrigMK2udJb .icon-shape p,#mermaid-svg-6gftJYrigMK2udJb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6gftJYrigMK2udJb .icon-shape .label rect,#mermaid-svg-6gftJYrigMK2udJb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6gftJYrigMK2udJb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6gftJYrigMK2udJb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6gftJYrigMK2udJb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户请求
识别租户上下文
认证与权限
业务处理
数据访问
配置加载
套餐限制
缓存读写
消息事件
文件存储
审计日志
这也是为什么成熟的 SaaS 系统通常会有一个统一的租户上下文。
所谓租户上下文,就是请求进入系统之后,系统能够明确当前请求属于哪个租户,并且把这个租户信息传递到权限、业务、数据、缓存、日志、消息等后续环节中。
如果没有统一的租户上下文,开发人员就只能靠手写参数来维护租户边界。这个接口记得传,那个接口忘了传;这个查询加了条件,那个导出功能漏了条件;同步请求里有租户信息,异步任务里却丢了租户信息。系统越大,风险越高。
所以,多租户设计的第一原则是:
租户边界不能靠开发人员自觉,必须成为系统机制。
tenant_id 是字段,租户上下文才是架构。
二、三种典型隔离模式:共享表、独立 Schema、独立数据库
谈多租户,最绕不开的问题就是:数据到底怎么隔离?
常见的 SaaS 多租户数据隔离模式,大体可以分为三类:共享数据库共享表、共享数据库独立 Schema、独立数据库。
这三种方案没有绝对高低,只有适不适合。它们分别代表了不同的成本、隔离性和运维复杂度。
1. 共享数据库共享表:最常见,也最容易埋雷
共享数据库共享表,是最常见的多租户模式。
所有租户共享一个数据库,也共享同一套业务表。每张关键业务表里增加 tenant_id 字段,用来标识这条数据属于哪个租户。
例如订单表可以这样设计:
orders
- id
- tenant_id
- customer_id
- amount
- status
- created_at
查询某个租户的订单时,必须带上租户条件:
select * from orders
where tenant_id = ?
and status = ?
这种模式最大的优点是成本低。
所有客户共享同一套数据库资源,开通新租户非常简单,不需要创建新数据库,也不需要维护多套表结构。系统升级时,只要升级一套表结构即可。对于早期 SaaS、标准化 SaaS、中小客户为主的 SaaS 来说,这通常是最现实的选择。
它还有一个优势:产品统一性强。
所有客户使用同一套数据结构、同一套代码逻辑、同一套升级路径。研发团队不需要维护大量客户专属数据库,也不需要为不同租户写不同迁移脚本。这非常符合 SaaS 标准化和规模化的商业模型。
但共享表模式的问题也很突出。
最大的问题是隔离弱。所有租户的数据混在同一批表里,隔离完全依赖 tenant_id 条件。一旦查询漏掉租户过滤,就可能产生数据越权。
这种风险在系统早期还不明显,因为功能少、接口少、开发人员都熟悉代码。但随着功能增多,问题会出现在各种边缘场景里:导出功能、报表统计、后台管理、异步任务、搜索索引、数据修复脚本、临时 SQL、运营后台。
还有一个问题是大租户可能影响小租户。
如果某个大客户数据量特别大,订单、日志、文件、报表数据远超其他客户,那么共享表查询性能可能被它拖累。虽然可以通过索引、分区、冷热数据、读写分离等方式优化,但本质上大家还是在共享同一套数据库资源。
此外,单个租户的数据迁移、备份和恢复也比较麻烦。比如某个客户要求导出完整数据,或者要迁移到独立库,就需要从共享表里按 tenant_id 抽取数据,还要处理关联表、历史日志、文件、配置、计费记录等一系列问题。
所以,共享库共享表适合大量中小客户、标准化程度高、客单价不高、对物理隔离要求不强的 SaaS。
它不是低级方案,而是成本效率最高的方案之一。只是使用它时,必须在工程机制上补足隔离风险。
2. 共享数据库独立 Schema:隔离更清楚,但维护更复杂
第二种模式,是共享数据库,但每个租户使用独立 Schema。
在 PostgreSQL、Oracle 等数据库中,Schema 可以理解为数据库内部的命名空间。不同租户的数据表结构相同,但分别放在不同 Schema 下。
例如:
tenant_a.orders
tenant_b.orders
tenant_c.orders
这种模式比共享表隔离更清楚。每个租户有自己的表空间,查询时可以切换到对应 Schema,不一定每张表都依赖 tenant_id 条件。单个租户的数据备份、迁移、清理也比共享表更方便。
它适合那些客户数量不算特别庞大、但每个客户相对重要的 SaaS 场景。比如面向学校、机构、连锁企业、区域组织的 SaaS,租户数量可能是几十、几百,而不是几十万。每个客户都有一定规模,也希望数据边界比共享表更清晰。
但独立 Schema 会带来明显的运维复杂度。
如果有 300 个租户,就可能有 300 套 Schema。每次数据库结构升级,都要对这些 Schema 执行迁移。某个 Schema 迁移失败怎么办?不同 Schema 版本不一致怎么办?如何批量检查表结构?如何监控每个租户的数据量?这些都需要自动化工具支撑。
如果没有成熟的迁移、监控和版本管理机制,独立 Schema 会让团队后期非常痛苦。
所以它是一个折中方案:隔离性比共享表强,成本比独立数据库低,但运维复杂度明显高于共享表。
3. 独立数据库:隔离最强,也最昂贵
第三种模式,是每个租户使用独立数据库。
这种方案隔离最强。每个客户的数据物理上分开,备份、恢复、迁移、扩容、安全审计都可以单独处理。对于大客户、金融、医疗、政企、教育、集团型企业等场景,独立数据库甚至独立部署往往更容易被客户接受。
独立数据库还有一个现实价值:方便服务高价值客户。
如果某个客户合同金额足够高,对数据隔离、性能、合规、专属运维都有要求,那么为它提供独立数据库是合理的。它不仅是技术方案,也是商业方案。
但独立数据库的成本最高。
如果每个租户都对应一个数据库,数据库实例数量、连接管理、监控告警、备份策略、迁移脚本、版本升级都会变复杂。应用层还需要租户路由机制:请求进来后,系统要知道当前租户应该连接哪个数据库。
如果租户数量很多,独立数据库会变成沉重的运维负担。
所以,独立数据库适合高价值客户、强合规客户、数据量特别大的客户,以及私有化或专属部署场景,不适合无差别地用在所有租户身上。
三种模式可以这样对比:
#mermaid-svg-BLoce1DvZ5oJpVrk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BLoce1DvZ5oJpVrk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BLoce1DvZ5oJpVrk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BLoce1DvZ5oJpVrk .error-icon{fill:#552222;}#mermaid-svg-BLoce1DvZ5oJpVrk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BLoce1DvZ5oJpVrk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BLoce1DvZ5oJpVrk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BLoce1DvZ5oJpVrk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BLoce1DvZ5oJpVrk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BLoce1DvZ5oJpVrk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BLoce1DvZ5oJpVrk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BLoce1DvZ5oJpVrk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BLoce1DvZ5oJpVrk .marker.cross{stroke:#333333;}#mermaid-svg-BLoce1DvZ5oJpVrk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BLoce1DvZ5oJpVrk p{margin:0;}#mermaid-svg-BLoce1DvZ5oJpVrk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BLoce1DvZ5oJpVrk .cluster-label text{fill:#333;}#mermaid-svg-BLoce1DvZ5oJpVrk .cluster-label span{color:#333;}#mermaid-svg-BLoce1DvZ5oJpVrk .cluster-label span p{background-color:transparent;}#mermaid-svg-BLoce1DvZ5oJpVrk .label text,#mermaid-svg-BLoce1DvZ5oJpVrk span{fill:#333;color:#333;}#mermaid-svg-BLoce1DvZ5oJpVrk .node rect,#mermaid-svg-BLoce1DvZ5oJpVrk .node circle,#mermaid-svg-BLoce1DvZ5oJpVrk .node ellipse,#mermaid-svg-BLoce1DvZ5oJpVrk .node polygon,#mermaid-svg-BLoce1DvZ5oJpVrk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BLoce1DvZ5oJpVrk .rough-node .label text,#mermaid-svg-BLoce1DvZ5oJpVrk .node .label text,#mermaid-svg-BLoce1DvZ5oJpVrk .image-shape .label,#mermaid-svg-BLoce1DvZ5oJpVrk .icon-shape .label{text-anchor:middle;}#mermaid-svg-BLoce1DvZ5oJpVrk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BLoce1DvZ5oJpVrk .rough-node .label,#mermaid-svg-BLoce1DvZ5oJpVrk .node .label,#mermaid-svg-BLoce1DvZ5oJpVrk .image-shape .label,#mermaid-svg-BLoce1DvZ5oJpVrk .icon-shape .label{text-align:center;}#mermaid-svg-BLoce1DvZ5oJpVrk .node.clickable{cursor:pointer;}#mermaid-svg-BLoce1DvZ5oJpVrk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BLoce1DvZ5oJpVrk .arrowheadPath{fill:#333333;}#mermaid-svg-BLoce1DvZ5oJpVrk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BLoce1DvZ5oJpVrk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BLoce1DvZ5oJpVrk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BLoce1DvZ5oJpVrk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BLoce1DvZ5oJpVrk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BLoce1DvZ5oJpVrk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BLoce1DvZ5oJpVrk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BLoce1DvZ5oJpVrk .cluster text{fill:#333;}#mermaid-svg-BLoce1DvZ5oJpVrk .cluster span{color:#333;}#mermaid-svg-BLoce1DvZ5oJpVrk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BLoce1DvZ5oJpVrk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BLoce1DvZ5oJpVrk rect.text{fill:none;stroke-width:0;}#mermaid-svg-BLoce1DvZ5oJpVrk .icon-shape,#mermaid-svg-BLoce1DvZ5oJpVrk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BLoce1DvZ5oJpVrk .icon-shape p,#mermaid-svg-BLoce1DvZ5oJpVrk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BLoce1DvZ5oJpVrk .icon-shape .label rect,#mermaid-svg-BLoce1DvZ5oJpVrk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BLoce1DvZ5oJpVrk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BLoce1DvZ5oJpVrk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BLoce1DvZ5oJpVrk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 多租户数据隔离模式
共享库共享表
共享库独立Schema
独立数据库
成本最低
隔离较弱
适合大量中小客户
逻辑隔离较强
迁移升级复杂
适合中大型客户
隔离最强
运维成本最高
适合大客户和强合规
现实中,很多成熟 SaaS 不会只采用一种模式,而是采用混合模式。
普通客户走共享库共享表,大客户走独立数据库,私有化客户单独部署一套环境。这样既保留了 SaaS 的成本效率,又能满足高价值客户的隔离诉求。
三、怎么选隔离模式:不要看技术偏好,要看客户结构
多租户隔离模式怎么选,不能只看架构师喜欢哪种,也不能只看哪种听起来更高级。
真正决定方案的,是客户结构、客单价、数据规模、合规要求和团队运维能力。
如果你的 SaaS 面向大量中小客户,比如轻量 CRM、在线表单、项目协作、营销工具、客服工具、知识库工具,那么共享库共享表通常是最合理的起点。因为这类产品客户数量多,单个客户数据量不一定大,客单价也未必支持高成本隔离。如果一开始就给每个客户建独立数据库,运维成本很快会吞掉利润。
如果你的 SaaS 面向中大型企业、学校、机构、连锁组织,每个客户都有一定体量,也比较重视数据边界,那么独立 Schema 或混合隔离更值得考虑。它能在成本和隔离之间取得折中,但前提是团队要有能力管理多 Schema 迁移和版本一致性。
如果你的 SaaS 面向金融、医疗、政企、大型集团,客户对合规、安全、审计、独立备份有强要求,那么独立数据库或独立部署就不是"过度设计",而是进入市场的门槛。在这些场景里,客户信任和合规要求往往比基础设施成本更重要。
隔离模式,本质上也是成本模型。
低客单价客户,不适合高成本隔离。
高价值客户,不应该只提供低隔离方案。
标准化客户,应该尽量共享以降低成本。
强合规客户,必须提供更强边界来换取信任。
所以,多租户方案不是单纯的技术选择,而是产品定价、客户分层和交付模式的一部分。
可以用一个简单决策逻辑来理解:
#mermaid-svg-lnvfLPhgGBViLNNo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lnvfLPhgGBViLNNo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lnvfLPhgGBViLNNo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lnvfLPhgGBViLNNo .error-icon{fill:#552222;}#mermaid-svg-lnvfLPhgGBViLNNo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lnvfLPhgGBViLNNo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lnvfLPhgGBViLNNo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lnvfLPhgGBViLNNo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lnvfLPhgGBViLNNo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lnvfLPhgGBViLNNo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lnvfLPhgGBViLNNo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lnvfLPhgGBViLNNo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lnvfLPhgGBViLNNo .marker.cross{stroke:#333333;}#mermaid-svg-lnvfLPhgGBViLNNo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lnvfLPhgGBViLNNo p{margin:0;}#mermaid-svg-lnvfLPhgGBViLNNo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lnvfLPhgGBViLNNo .cluster-label text{fill:#333;}#mermaid-svg-lnvfLPhgGBViLNNo .cluster-label span{color:#333;}#mermaid-svg-lnvfLPhgGBViLNNo .cluster-label span p{background-color:transparent;}#mermaid-svg-lnvfLPhgGBViLNNo .label text,#mermaid-svg-lnvfLPhgGBViLNNo span{fill:#333;color:#333;}#mermaid-svg-lnvfLPhgGBViLNNo .node rect,#mermaid-svg-lnvfLPhgGBViLNNo .node circle,#mermaid-svg-lnvfLPhgGBViLNNo .node ellipse,#mermaid-svg-lnvfLPhgGBViLNNo .node polygon,#mermaid-svg-lnvfLPhgGBViLNNo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lnvfLPhgGBViLNNo .rough-node .label text,#mermaid-svg-lnvfLPhgGBViLNNo .node .label text,#mermaid-svg-lnvfLPhgGBViLNNo .image-shape .label,#mermaid-svg-lnvfLPhgGBViLNNo .icon-shape .label{text-anchor:middle;}#mermaid-svg-lnvfLPhgGBViLNNo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lnvfLPhgGBViLNNo .rough-node .label,#mermaid-svg-lnvfLPhgGBViLNNo .node .label,#mermaid-svg-lnvfLPhgGBViLNNo .image-shape .label,#mermaid-svg-lnvfLPhgGBViLNNo .icon-shape .label{text-align:center;}#mermaid-svg-lnvfLPhgGBViLNNo .node.clickable{cursor:pointer;}#mermaid-svg-lnvfLPhgGBViLNNo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lnvfLPhgGBViLNNo .arrowheadPath{fill:#333333;}#mermaid-svg-lnvfLPhgGBViLNNo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lnvfLPhgGBViLNNo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lnvfLPhgGBViLNNo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lnvfLPhgGBViLNNo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lnvfLPhgGBViLNNo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lnvfLPhgGBViLNNo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lnvfLPhgGBViLNNo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lnvfLPhgGBViLNNo .cluster text{fill:#333;}#mermaid-svg-lnvfLPhgGBViLNNo .cluster span{color:#333;}#mermaid-svg-lnvfLPhgGBViLNNo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lnvfLPhgGBViLNNo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lnvfLPhgGBViLNNo rect.text{fill:none;stroke-width:0;}#mermaid-svg-lnvfLPhgGBViLNNo .icon-shape,#mermaid-svg-lnvfLPhgGBViLNNo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lnvfLPhgGBViLNNo .icon-shape p,#mermaid-svg-lnvfLPhgGBViLNNo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lnvfLPhgGBViLNNo .icon-shape .label rect,#mermaid-svg-lnvfLPhgGBViLNNo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lnvfLPhgGBViLNNo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lnvfLPhgGBViLNNo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lnvfLPhgGBViLNNo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
选择多租户隔离模式
是否有强合规或专属部署要求
独立数据库或独立部署
客户数量是否很大
共享库共享表
单租户数据量是否较大
独立Schema或独立库
共享库共享表
不过,早期 SaaS 不一定要把所有方案都做出来。
很多产品在 MVP 阶段,最现实的选择就是共享库共享表。先验证业务,先拿到客户,先把产品跑起来。这个选择没有问题。
真正要注意的是:早期可以简单,但不能没有演进空间。
即使一开始采用共享表,也应该尽量做好几件事。
第一,创建统一租户上下文。不要让业务代码到处手动传 tenant_id。
第二,封装数据访问层。不要让所有开发人员随意写不带租户条件的查询。
第三,配置、权限、计费都围绕租户建模。不要把客户差异写死在代码里。
第四,文件、日志、缓存、消息都带租户维度。不要只在数据库里考虑租户。
第五,预留租户迁移可能。未来如果某个大客户要独立库,系统至少有机会把它迁出去。
很多 SaaS 架构演进不是从"简单模式"直接跳到"复杂模式",而是从共享表开始,逐步支持大客户独立库、私有化部署、专属集群和混合云。
这就要求早期虽然简单,但边界不能乱。
四、多租户真正难的,是全链路一致性
数据库隔离只是多租户设计的第一层。
真正困难的是:租户上下文能不能贯穿整个系统,并且在权限、配置、计费、缓存、消息、文件、审计等环节保持一致。
很多系统看起来已经做了多租户,因为表里有 tenant_id,查询也加了租户条件。但一旦进入复杂业务,问题就会暴露。
权限必须绑定租户
SaaS 的权限不是简单的"用户有什么角色",而是"用户在某个租户下有什么角色"。
同一个用户,可能同时属于多个租户。比如一个顾问同时服务 A 公司和 B 公司,他在 A 公司是管理员,在 B 公司只是普通成员。如果系统只按用户 ID 判断权限,而不结合租户上下文,就会出现越权风险。
更完整的权限判断至少要包含五个问题:
当前用户是谁?
当前租户是谁?
当前用户是否属于该租户?
当前用户在该租户下是什么角色?
该角色能访问哪些功能和数据?
所以 SaaS 权限模型通常不能只做"用户 - 角色 - 权限",而要变成"用户 - 租户 - 角色 - 权限"。
这一点如果早期没想清楚,后期做多组织、多角色、多空间、多项目权限时会非常痛苦。
配置必须租户化
SaaS 客户之间天然存在差异。
A 客户启用审批流,B 客户不用审批。
A 客户需要自定义字段,B 客户只用标准字段。
A 客户购买高级报表,B 客户套餐不包含。
A 客户页面上叫"学员",B 客户页面上叫"客户"。
如果这些差异全部写死在代码里,系统很快就会变成一堆客户专属逻辑。今天加一个 if,明天加一个开关,后天为某个客户拉一个分支,最后 SaaS 就会变成定制项目。
所以,多租户 SaaS 必须有租户级配置意识。
配置中心可以从简单开始,比如功能开关、套餐参数、页面显示名称、通知模板。随着系统成熟,再逐步扩展到字段配置、流程配置、数据规则、报表配置。
关键不是一开始就做一个庞大的低代码平台,而是要避免把客户差异全部硬编码进业务逻辑。
计费要围绕租户建模
SaaS 的商业化对象通常是租户,而不是单个用户。
一个租户购买了哪个套餐,最多能创建多少用户,能使用哪些功能,每月有多少 API 调用额度,能上传多少文件,是否支持高级报表,是否允许使用 AI 功能,这些都应该绑定到租户。
如果计费模型一开始没有围绕租户设计,后面做订阅、试用、续费、升级、降级、用量统计会非常麻烦。
一个常见的关系是:
#mermaid-svg-vD2nBmO7A0GC7VT5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vD2nBmO7A0GC7VT5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vD2nBmO7A0GC7VT5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vD2nBmO7A0GC7VT5 .error-icon{fill:#552222;}#mermaid-svg-vD2nBmO7A0GC7VT5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vD2nBmO7A0GC7VT5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vD2nBmO7A0GC7VT5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vD2nBmO7A0GC7VT5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vD2nBmO7A0GC7VT5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vD2nBmO7A0GC7VT5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vD2nBmO7A0GC7VT5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vD2nBmO7A0GC7VT5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vD2nBmO7A0GC7VT5 .marker.cross{stroke:#333333;}#mermaid-svg-vD2nBmO7A0GC7VT5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vD2nBmO7A0GC7VT5 p{margin:0;}#mermaid-svg-vD2nBmO7A0GC7VT5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vD2nBmO7A0GC7VT5 .cluster-label text{fill:#333;}#mermaid-svg-vD2nBmO7A0GC7VT5 .cluster-label span{color:#333;}#mermaid-svg-vD2nBmO7A0GC7VT5 .cluster-label span p{background-color:transparent;}#mermaid-svg-vD2nBmO7A0GC7VT5 .label text,#mermaid-svg-vD2nBmO7A0GC7VT5 span{fill:#333;color:#333;}#mermaid-svg-vD2nBmO7A0GC7VT5 .node rect,#mermaid-svg-vD2nBmO7A0GC7VT5 .node circle,#mermaid-svg-vD2nBmO7A0GC7VT5 .node ellipse,#mermaid-svg-vD2nBmO7A0GC7VT5 .node polygon,#mermaid-svg-vD2nBmO7A0GC7VT5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vD2nBmO7A0GC7VT5 .rough-node .label text,#mermaid-svg-vD2nBmO7A0GC7VT5 .node .label text,#mermaid-svg-vD2nBmO7A0GC7VT5 .image-shape .label,#mermaid-svg-vD2nBmO7A0GC7VT5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-vD2nBmO7A0GC7VT5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vD2nBmO7A0GC7VT5 .rough-node .label,#mermaid-svg-vD2nBmO7A0GC7VT5 .node .label,#mermaid-svg-vD2nBmO7A0GC7VT5 .image-shape .label,#mermaid-svg-vD2nBmO7A0GC7VT5 .icon-shape .label{text-align:center;}#mermaid-svg-vD2nBmO7A0GC7VT5 .node.clickable{cursor:pointer;}#mermaid-svg-vD2nBmO7A0GC7VT5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vD2nBmO7A0GC7VT5 .arrowheadPath{fill:#333333;}#mermaid-svg-vD2nBmO7A0GC7VT5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vD2nBmO7A0GC7VT5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vD2nBmO7A0GC7VT5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vD2nBmO7A0GC7VT5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vD2nBmO7A0GC7VT5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vD2nBmO7A0GC7VT5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vD2nBmO7A0GC7VT5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vD2nBmO7A0GC7VT5 .cluster text{fill:#333;}#mermaid-svg-vD2nBmO7A0GC7VT5 .cluster span{color:#333;}#mermaid-svg-vD2nBmO7A0GC7VT5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vD2nBmO7A0GC7VT5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vD2nBmO7A0GC7VT5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-vD2nBmO7A0GC7VT5 .icon-shape,#mermaid-svg-vD2nBmO7A0GC7VT5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vD2nBmO7A0GC7VT5 .icon-shape p,#mermaid-svg-vD2nBmO7A0GC7VT5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vD2nBmO7A0GC7VT5 .icon-shape .label rect,#mermaid-svg-vD2nBmO7A0GC7VT5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vD2nBmO7A0GC7VT5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vD2nBmO7A0GC7VT5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vD2nBmO7A0GC7VT5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 租户
订阅
套餐
功能权益
用量额度
用量统计
超限控制
这套模型不一定早期全部实现,但概念上要想清楚。
否则,当销售突然要推出基础版、专业版、企业版时,研发会发现所有功能限制都散落在业务代码里,没有统一入口。
缓存、消息和文件都要防串租
缓存是多租户系统里很容易被忽略的风险点。
比如用户权限缓存,如果 key 只写成:
user:1001:permissions
但用户 1001 在不同租户下权限不同,就可能读到错误权限。更稳妥的设计应该包含租户维度:
tenant:2001:user:1001:permissions
类似问题还会出现在配置缓存、报表缓存、业务对象缓存、限流计数、临时 token、验证码、导出任务状态中。凡是缓存了和租户有关的数据,都要考虑租户维度。
消息系统也一样。
异步任务一旦进入消息队列,就脱离了原来的 HTTP 请求上下文。如果消息体里没有携带租户 ID,消费者就可能不知道当前处理的是哪个租户的数据。发送通知、生成报表、同步数据、处理 Webhook、写入搜索索引,都应该带上租户上下文。
文件存储同样需要租户归属。
上传文件时,文件记录要有租户 ID;对象存储路径最好也能体现租户维度;文件访问接口必须做租户和权限校验。否则,文件越权往往比数据库越权更隐蔽。
审计日志必须记录租户上下文
审计日志是 SaaS 排障、安全和客户信任的重要基础。
一条合格的审计日志,不应该只记录"用户删除了订单",而应该记录:
在哪个租户下操作;
哪个用户操作;
操作了什么对象;
操作前后发生了什么变化;
操作时间;
来源 IP;
请求 ID;
操作是否成功;
失败原因是什么。
多租户系统一旦出现数据争议、权限争议、客户投诉,审计日志往往是最重要的证据。
如果日志里没有租户 ID,很多问题根本查不清。
可以把多租户全链路一致性概括成这样:
#mermaid-svg-yGpdr0ekv4LD3H6z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yGpdr0ekv4LD3H6z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yGpdr0ekv4LD3H6z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yGpdr0ekv4LD3H6z .error-icon{fill:#552222;}#mermaid-svg-yGpdr0ekv4LD3H6z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yGpdr0ekv4LD3H6z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yGpdr0ekv4LD3H6z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yGpdr0ekv4LD3H6z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yGpdr0ekv4LD3H6z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yGpdr0ekv4LD3H6z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yGpdr0ekv4LD3H6z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yGpdr0ekv4LD3H6z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yGpdr0ekv4LD3H6z .marker.cross{stroke:#333333;}#mermaid-svg-yGpdr0ekv4LD3H6z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yGpdr0ekv4LD3H6z p{margin:0;}#mermaid-svg-yGpdr0ekv4LD3H6z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yGpdr0ekv4LD3H6z .cluster-label text{fill:#333;}#mermaid-svg-yGpdr0ekv4LD3H6z .cluster-label span{color:#333;}#mermaid-svg-yGpdr0ekv4LD3H6z .cluster-label span p{background-color:transparent;}#mermaid-svg-yGpdr0ekv4LD3H6z .label text,#mermaid-svg-yGpdr0ekv4LD3H6z span{fill:#333;color:#333;}#mermaid-svg-yGpdr0ekv4LD3H6z .node rect,#mermaid-svg-yGpdr0ekv4LD3H6z .node circle,#mermaid-svg-yGpdr0ekv4LD3H6z .node ellipse,#mermaid-svg-yGpdr0ekv4LD3H6z .node polygon,#mermaid-svg-yGpdr0ekv4LD3H6z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yGpdr0ekv4LD3H6z .rough-node .label text,#mermaid-svg-yGpdr0ekv4LD3H6z .node .label text,#mermaid-svg-yGpdr0ekv4LD3H6z .image-shape .label,#mermaid-svg-yGpdr0ekv4LD3H6z .icon-shape .label{text-anchor:middle;}#mermaid-svg-yGpdr0ekv4LD3H6z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yGpdr0ekv4LD3H6z .rough-node .label,#mermaid-svg-yGpdr0ekv4LD3H6z .node .label,#mermaid-svg-yGpdr0ekv4LD3H6z .image-shape .label,#mermaid-svg-yGpdr0ekv4LD3H6z .icon-shape .label{text-align:center;}#mermaid-svg-yGpdr0ekv4LD3H6z .node.clickable{cursor:pointer;}#mermaid-svg-yGpdr0ekv4LD3H6z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yGpdr0ekv4LD3H6z .arrowheadPath{fill:#333333;}#mermaid-svg-yGpdr0ekv4LD3H6z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yGpdr0ekv4LD3H6z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yGpdr0ekv4LD3H6z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yGpdr0ekv4LD3H6z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yGpdr0ekv4LD3H6z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yGpdr0ekv4LD3H6z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yGpdr0ekv4LD3H6z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yGpdr0ekv4LD3H6z .cluster text{fill:#333;}#mermaid-svg-yGpdr0ekv4LD3H6z .cluster span{color:#333;}#mermaid-svg-yGpdr0ekv4LD3H6z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yGpdr0ekv4LD3H6z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yGpdr0ekv4LD3H6z rect.text{fill:none;stroke-width:0;}#mermaid-svg-yGpdr0ekv4LD3H6z .icon-shape,#mermaid-svg-yGpdr0ekv4LD3H6z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yGpdr0ekv4LD3H6z .icon-shape p,#mermaid-svg-yGpdr0ekv4LD3H6z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yGpdr0ekv4LD3H6z .icon-shape .label rect,#mermaid-svg-yGpdr0ekv4LD3H6z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yGpdr0ekv4LD3H6z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yGpdr0ekv4LD3H6z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yGpdr0ekv4LD3H6z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 租户上下文
权限校验
数据访问
配置加载
套餐计费
缓存Key
消息事件
文件归属
审计日志
多租户真正难的地方,不是某一张表怎么设计,而是系统所有关键链路都不能丢失租户上下文。
如果只有数据库做了 tenant_id,但权限、配置、计费、缓存、消息、文件、审计都没有租户意识,系统依然是不安全、不稳定、不可规模化的。
五、结语:多租户设计决定 SaaS 的成本、边界和上限
SaaS 的多租户架构,本质上是在共享和隔离之间做平衡。
共享越多,成本越低,升级越方便,标准化程度越高。
隔离越强,安全性越好,大客户接受度越高,合规能力越强。
但隔离越强,运维成本、架构复杂度和自动化要求也越高。
所以,多租户没有唯一正确答案。
共享库共享表适合大量中小客户和标准化 SaaS。
共享库独立 Schema 适合中大型客户和更强逻辑隔离。
独立数据库适合高价值客户、强合规场景和私有化部署。
混合模式适合已经进入成熟期、需要服务不同客户层级的 SaaS 产品。
但无论选择哪种模式,都要守住一个原则:租户边界必须清楚。
租户上下文要统一,不能靠开发人员手工传递。
数据访问要机制化隔离,不能全靠写 SQL 时自觉。
权限、配置、计费要围绕租户建模,不能后期临时拼凑。
缓存、消息、文件、审计都要有租户意识,不能只在数据库里考虑多租户。
早期架构可以简单,但要为未来大客户独立库、专属环境、数据迁移保留空间。
很多 SaaS 系统后期的痛苦,都是早期一句"先不管租户,后面再说"埋下的。
tenant_id 看起来只是一个字段,但它背后代表的是 SaaS 系统的边界、安全、成本、扩展能力和商业模式。
一句话总结:
多租户不是 SaaS 的一个功能,而是 SaaS 的底层秩序。谁把租户边界设计清楚,谁就更有可能把 SaaS 做成真正可规模化的产品。