首页 / 资讯中心 / 文章详情

多租户SaaS架构入门:三种隔离模型与租户路由实践

多租户SaaS架构入门:三种隔离模型与租户路由实践 ★ FEATURED ARTICLE
做SaaS架构的人几乎都绕不开“多租户”这三个字。不管你是从零搭一个全新的SaaS产品还是把公司已有的单租户系统改造成SaaS形态多租户都是第一道必须迈过去的坎。我自己在过去的项目里既见过创业团队因为一开始没想清楚租户模型上线后花了好几个月补数据隔离的窟窿也见过成熟平台为了追求极致的租户隔离把运维成本堆到几乎不可维护。这篇基础篇不堆高大上的架构图而是把我踩过的坑、验证过可行的方案以及每次选型背后的真实考量尽量讲透。这篇文章适合谁看刚接手SaaS产品研发的工程师、准备从项目制交付转型到产品化交付的技术负责人以及正在评估自研多租户能力还是直接购买成熟组件的架构师。看完你至少能回答这几个问题多租户到底在解决什么三种主流隔离模型各自要付出什么代价、适合什么场景租户路由怎么做以及最常见的“租户数据串号”是怎么发生的、又该怎么防。内容偏基础但每一节都会给到可以直接抄作业的落地建议。1. 多租户SaaS架构的核心概念与需求拆解1.1 什么是多租户为什么SaaS离不开它SaaS的全称是Software as a Service软件即服务。它的商业模式决定了同一套软件要同时服务很多家客户这些客户在SaaS语境里通常不叫“用户”而叫“租户”。“租”这个字很形象客户不是花钱买断软件而是按周期付费使用租的是能力不是资产。多租户架构就是让多个租户共用同一套软件基础设施同时保证每个租户的数据互相隔离、互不可见并且能在租户层面独立配置、独立计量。这里有两个关键词共享和隔离。共享是为了摊薄成本隔离是为了守住安全底线。一个成熟的多租户架构本质上就是在“共享”和“隔离”这对矛盾之间不断找最优解。为什么SaaS离不开多租户因为SaaS的商业模式建立在规模效应上。如果每个客户都部署一套独立代码和独立数据库那就退回到了传统软件的项目制交付交付成本、运维成本、升级成本都会随客户数量线性增长根本谈不上规模化。多租户的核心价值在于让新增客户的边际成本趋近于零这也是SaaS产品能用相对低价服务大量客户的前提。顺带说一句现在“多租户”这个词已经不只在传统业务系统里出现了。像一些热门的开源AI应用平台比如dify社区版也在做多租户能力逻辑和我们做业务系统一模一样不同团队共享平台能力但各自的API Key、知识库、应用配置必须严格隔离。这说明多租户已经是一种通用的平台化底座能力凡是“一套系统服务多家客户”的场景都躲不开它。1.2 多租户与多用户先把边界划清楚很多人第一次接触多租户时最容易混淆的概念就是“多租户”和“多用户”。这两个概念确实有关联但它们解决的是完全不同层面的问题。我见过不少团队在需求评审阶段就因为边界没扯清楚导致权限模型设计得极其拧巴后面改起来非常痛苦。我习惯用一个比喻来说清楚把SaaS系统想象成一栋办公楼。多用户解决的是“办公楼里的人怎么分工”的问题——谁有门禁卡、谁能进哪间办公室、谁能用哪个会议室这些是用户与权限的范畴。而多租户解决的是“整栋楼怎么分给不同公司”的问题——A公司在3楼B公司在5楼A公司的人绝对不能跑到B公司的办公区里。A公司用了多少电、开了多少灯这些消耗要记到A公司的账上。反映到系统设计上多用户依赖的是RBAC这类权限模型它管的是“用户能操作哪些功能、访问哪些数据”。而多租户是更高一层的边界它定义了数据归属的“产权”问题。在实际代码里多租户通常表现为给所有业务表加一个tenant_id字段多用户则是在这个基础上通过角色、权限去细分某个租户内部不同人的操作范围。所以多租户是权限的前置条件——先确认数据属于哪个租户再谈这个租户下的用户能干什么。1.3 三种经典隔离模型池化、独享库、独享实例多租户的数据隔离方案业界基本收敛到三种经典模型从共享程度高到隔离程度高依次是共享数据库共享Schema通常叫池化模式、共享数据库独享Schema、独享数据库再往上还可以到独享实例。池化模式是所有租户共用同一个数据库、同一组表靠tenant_id字段区分数据归属。这是成本最低、运维最省的模式新增租户不需要做任何数据库操作插入一条租户记录就能开通服务。缺点是隔离性最弱SQL里一旦漏写了租户过滤条件就可能查到别的租户的数据这是典型的生产事故源头。另一个隐患是所有租户挤在同一张表里某个大租户的数据量暴涨会影响所有人的查询性能。独享Schema模式下所有租户仍然共享同一个数据库实例但每个租户拥有独立的SchemaPostgreSQL天然支持SQL Server也有类似概念MySQL则常通过独立database实现。它的优点是逻辑隔离比池化好可以按租户做备份恢复也方便给特定租户做轻度定制缺点是数据库实例内的Schema多了以后管理和连接开销都会上升备份脚本也要跟着复杂起来。独享数据库模式是隔离最彻底的方式每个租户一个独立数据库甚至独立实例。安全性和性能隔离优势非常明显大租户不会拖垮小租户也最容易满足金融、医疗等行业的合规审计要求。但代价是成本高、运维重数据库数量随租户数线性增长连版本升级都要做很多遍。三种模式没有绝对的好坏取决于产品定价和目标客户。客单价低的海量中小客户更适合池化中大型客户为主的SaaS更适合独享Schema或独享数据库。很多成熟的SaaS产品本身就在用混合模式——默认池化高套餐自动升级为独享。具体怎么选我在第2章展开讲。2. 多租户架构设计的关键决策点2.1 租户身份识别与上下文传递设计多租户架构时要做的第一个决策不是数据库怎么分而是“系统怎么知道当前请求属于哪个租户”。这个看似简单的问题如果不在入口层统一处理后面会在业务代码里遍地是坑。租户身份识别通常有三个来源。最常用的是域名每个租户绑定独立域名或子域名比如tenantA.saas.com网关根据域名解析出租户ID其次是请求头在HTTP请求里带一个自定义Header比如X-Tenant-Id这种方式在前后端分离的架构里很常见最后是登录态用户登录后从Token中解析所属租户。我推荐的做法是域名或路径识别放在网关层做它的职责是路由真正的租户上下文在认证通过后从Token里解析出来并写入请求上下文比如Java的ThreadLocal或者Go的context.Context。这里要特别提醒绝对不能信任前端传来的租户ID必须以后端从Token解析出来的为准。前端传参是外部输入是可以被伪造的很多租户越权漏洞的根源就在这里。租户上下文一旦确定就要保证它在一次请求的整个调用链路里都能被访问到。无论是同步的RPC调用还是异步的消息队列消费都必须把租户上下文透传下去。异步链路是最容易遗漏的地方很多团队在线程池里跑任务时把租户上下文搞丢了结果定时任务处理的数据张冠李戴。这个问题没有捷径只能在框架层统一封装比如写一个自定义的线程池包装器或者在消息体里显式携带tenant_id消费端解析后重建上下文。2.2 数据隔离方案选型的取舍逻辑三种隔离模型的优劣在上一章列过这一节重点说选型的思考框架。我自己的判断维度有四个租户规模与客单价、安全合规要求、运维能力和未来的演进空间。客单价是硬约束。如果产品面向个人或小微企业月费几十块到几百块那几乎只能选池化模式因为独享数据库的硬件成本可能比一年客单价还高。反过来客单价高到几万甚至几十万客户往往有明确的合规审计要求独享数据库或至少独享Schema是标配。我见过团队在启动时拍脑袋选了池化等签下第一个金融客户时对面直接要求物理隔离最后只能加班做迁移非常被动。运维能力也要诚实评估。独享数据库听着很美好但几十个库一起做备份、升级、监控的运维压力相当大。如果团队里只有一两个DBA我更建议从独享Schema或者池化起步。好消息是这三种模式之间可以渐进迁移池化可以先拆到独享Schema再拆到独享数据库。所以初始选型不要求一步到位但数据模型上必须预留租户维度字段否则后面想拆都拆不动。从实际项目经验看一个比较稳妥的起步组合是默认所有租户池化共享在租户表里增加一个deployment_mode字段标识该租户的隔离级别开通新租户时根据套餐自动分配。这样一套代码可以同时支持三种模式既照顾了中小客户也留住了高端客户架构上不至于返工。2.3 租户维度的扩展性与资源配额设计多租户架构里还有一个日常不会注意、但早晚会遇到的坑租户维度的配额管理。如果没有配额机制一个小租户因为某段异常代码触发了全表扫描可能把整个数据库实例的CPU打满连累所有租户。这就是业界常说的“吵闹的邻居”问题。我建议从第一天就给关键资源设计租户维度的配额比如单个租户的存储空间上限、API调用频次、最大并发数、消息队列积压上限、定时任务执行时间阈值。这些配额初期不需要做得很复杂在租户配置表里加几个字段就够了比如max_storage、max_qps、max_concurrency然后在网关或中间件层统一做限流和校验即可。扩展性方面池化模式最大的瓶颈是单表数据量。当某些租户的数据量特别大时需要为这样的“大租户”提供升级路径。我的做法是给大租户单独分配物理资源池把它的数据迁移到独立Schema或独立数据库中同时用前面提到的deployment_mode字段记录这种特殊调度。这也是多租户架构里混合隔离最典型的应用场景。再往深一层说多租户架构的扩展性不应该只停留在数据库层面整个链路都要有租户维度的感知。比如对象存储里的文件路径建议按租户分目录日志系统里必须打上tenant_id标签缓存Key也要带租户前缀。这些看似都是小事但等到需要按租户做数据导出、合规审计或者故障恢复时就能体会到当初“多留一手”的价值了。3. 从零搭建多租户SaaS的实操路径3.1 数据模型设计中的租户字段处理真正开始写代码时多租户改造会落到最琐碎的地方——数据模型。这里有一条硬性原则所有业务表都必须有租户标识并且这个标识要作为所有查询的隐含过滤条件。表结构上我习惯在每张业务表里都加tenant_id字段并把它作为联合主键或联合唯一索引的一部分。以订单表为例主键不应该是单纯的id而应该是(tenant_id, id)这样的联合主键。这么做的好处是从数据库层面就杜绝了跨租户主键冲突和越权访问的可能。对于MySQL这类不支持多Schema概念的数据库这一点尤其重要。ORM框架层面多数主流框架都提供了多租户支持。比如MyBatis-Plus有租户拦截器Hibernate有内置的multiTenancy配置Rails生态也有apartment这样的经典方案。我强烈建议把这层逻辑做在框架的拦截器里而不是靠程序员自觉在每个SQL后面手动加and tenant_id ?。人一定会犯错手动拼接早晚漏一次漏一次就是一次P0事故。除了业务表还要注意几个容易被忽略的地方。唯一约束要考虑租户维度比如“部门名称”这个字段A租户和B租户都可以有叫“技术部”的部门所以唯一索引必须是(tenant_id, name)而不能只是name。数据库索引也要把tenant_id放在靠前的位置否则大租户的数据会拖慢所有租户的查询。还有一些JSON类型的扩展字段无论怎么存查询时都要有租户条件作为隐式过滤。3.2 租户路由与连接管理的最佳实践租户路由解决的是“请求到了之后去哪个数据库里取数”的问题。池化模式下所有租户在一个库里路由很简单但一旦用了混合隔离模型租户路由就成了一个关键模块。我踩过一个很深的坑当时一个看似正常的查询走了几十秒排查下来发现连接池里所有连接都被打到了一个已经满负荷的数据库实例上。原因是好几套服务共用了同一个连接池租户路由只做了域名解析完全没有按租户分发连接。这个教训让我后来在路由层花了很多心思。推荐的做法是在数据库访问中间件里加一个租户路由拦截器根据当前租户上下文查询租户配置表拿到该租户的隔离级别和数据库连接信息然后动态选择数据源。在Spring体系里这可以用AbstractRoutingDataSource实现本质上是一个路由数据源根据key切换到不同的真实数据源。需要提醒的是租户与数据库的映射关系一定要加缓存并且设计好缓存失效策略否则每个请求都查一次配置表再好的数据库也扛不住。热词里有人问“解析到SaaS站点域名的逻辑是啥”这正好是租户路由的前半段。典型逻辑是DNS把*.saas.com泛解析到接入层网关网关解析请求的Host头提取子域名前缀比如tenantA然后查租户配置表确认租户是否存在、套餐是否有效、状态是否正常最后把解析出的租户ID放进请求上下文并转发给后端服务。整个过程要求网关层有缓存否则每个请求都穿透到数据库查配置代价会很大。3.3 多租户下的权限模型设计租户和权限的关系我在1.2里用办公楼比喻讲过。落到设计上多租户的权限模型可以理解为“两层权限”第一层是租户边界决定数据属于谁第二层是RBAC决定该租户内部的用户能做什么。具体做法是在角色表、用户表上都加tenant_id字段角色的scope限定在租户内。一个用户理论上可以属于多个租户比如某人在A公司是管理员同时又是B公司的普通成员所以用户与租户的关系要通过一个membership关联表来表达典型字段包括user_id、tenant_id、role_id、status。用户切换租户时系统重新下发对应租户的Token业务请求里携带的就是当前生效租户的身份。还有一个实际问题系统里通常会有“平台管理员”和“租户管理员”两种角色。平台管理员要能管理全部租户租户管理员只能管自己的租户。这两个角色的权限边界一定要在代码层隔离平台管理端和租户业务端最好拆成不同的接口服务或者至少不同的路由分组。我见过不少事故就是因为管理端接口和业务端接口共用同一套鉴权逻辑导致租户用户能访问到平台管理接口后果非常严重。像dify社区版这类AI应用平台做多租户底层逻辑完全一致平台能力共享各团队/组织的API Key、知识库、应用配置严格隔离。区别只是租户资源里多出了模型配额、Token消耗这类AI特有的计量项。所以理解传统业务系统的多租户模型迁移到AI平台场景是很容易的。4. 多租户架构中的常见问题与避坑实录4.1 租户数据串号事故级别最高的坑多租户系统里最知名的事故就是“数据串号”——A租户的用户看到了B租户的数据。我在排查这类问题时总结了三个最常见的成因第一是SQL漏写租户条件第二是缓存Key没带租户维度第三是关联查询时用了错误表的租户字段。针对第一个成因前面建议在ORM拦截器里统一处理这里不重复。补充一点如果系统里存在原生SQL或者报表类复杂查询拦截器可能覆盖不全这类SQL要单独review并且在测试环境里造好多个租户的数据做专项验证。第二个成因很隐蔽比如getOrder(id)这类缓存如果Key只用订单ID两个租户的订单ID一旦重合就会串数据。我有一次排查一个“间歇性串号”问题查了两天才定位到是Redis缓存没有带租户前缀。从那以后我定了一条规矩所有缓存Key的第一段必须是租户ID。第三个成因是关联查询时表顺序和条件写错了。比如订单和客户关联如果join条件里只写了o.customer_id c.id没有写c.tenant_id o.tenant_id那么不同租户的同ID客户就可能被错误的关联上。这条规则写SQL时就要注意凡是跨表通过业务ID关联的关联条件里都要带上租户ID一个都不能少。4.2 数据库连接池耗尽问题池化模式下所有租户共用一个数据源连接池是最容易成为瓶颈的地方。某个租户的慢查询一旦把连接全部占住其他租户的所有请求都会被阻塞这比单租户系统的连接池问题影响面大得多因为它直接影响全部客户。解决思路有两个方向。一是限流和降级在入口层对单个租户做并发限制防止单租户拖垮全局二是把连接池拆成公共池和专用池默认租户走公共池大租户或VIP租户走专用池这样即使某一个大租户把专用池耗尽也不会影响普通租户。第二个方案成本更高但中大型SaaS产品普遍在用。关于慢查询实践中的一个关键认知是多租户系统的慢查询日志和监控必须带tenant_id维度。否则你只看到一条SQL慢却不知道是哪个租户的数据导致的问题都没法定向。我们做监控时把“每个租户的P99耗时”单独做成报表一旦某个租户指标异常立刻能定位到具体租户再结合它的数据量和执行计划来分析原因。这种“租户维度排障”的能力是单租户系统不需要、但多租户系统一定要尽早建起来的。4.3 大租户与小租户的资源公平问题“吵闹的邻居”在多租户系统里很普遍。比如一个租户每天早上8点集中跑批导入数据占满数据库IO其他租户早上查报表就会明显变慢。这个问题在池化模式下尤其突出也是客户投诉的高发区。基本的治理手段是分级和限流。给租户划分等级普通租户、高级租户、VIP租户分别设置不同的配额和优先级超配的请求排队或拒绝。数据库层可以用独立Schema或独立实例隔离VIP租户应用层可以对低价租户的API做更严格限流消息队列里可以按租户设置独立的消费队列和积压阈值。还有一个容易被忽略的点定时任务。很多系统有批量任务比如每天晚上做全量同步、数据清洗如果按全局跑很容易把整库资源占满。多租户架构下这类任务必须按租户分片执行给每个租户设置执行时间窗口和最大耗时。我们遇到过一个极端场景某个租户数据量特别大定时任务一整晚都跑不完最后改成按租户分批执行——一次只处理一部分租户做完一批再做下一批总算把资源占用控制住了。这类问题在第1天不显眼数据规模上来以后一定会找上门。5. 多租户架构的演进路线与配套能力5.1 从单租户改造为多租户的迁移思路很多团队不是从零起步而是已有的单租户系统要改造成多租户。这里我给一个相对稳妥的思路但提前泼一盆冷水改造的成本往往比想象中高一定要先做范围评估。第一步是做数据摸底把核心业务表过一遍确认哪些表需要加tenant_id哪些表属于全局配置表比如系统字典、租户套餐定义不需要加。第二步是接入租户上下文先打通认证和网关确保每个请求都能解析出租户ID。第三步才是加字段、做数据迁移和回归验证这个阶段最容易出事建议分业务模块逐步推进不要一次性全量上线。改造过程中最大的痛点通常是历史数据。存量数据没有租户归属需要业务上确定归属规则比如按企业客户ID来划分历史数据或者把暂时无法识别的数据归入一个“默认租户”过渡。这一步听起来简单实际执行时要跟业务方核对大量规则建议尽早拉上产品经理一起梳理别纯靠技术拍板。要特别提醒的是多租户改造不等于简单加一个字段。全局唯一约束、缓存Key、文件存储路径、日志检索方式全都要跟着调整。我见过一个团队花了两周给表加了tenant_id上线后发现定时任务还是按全局跑文件存储路径也没有租户区分隔离形同虚设。改造是否彻底检验标准很简单两个租户的数据从存储、缓存、文件到日志全程都碰不到对方。5.2 租户维度的可观测性设计多租户系统上线之后可观测性会是一个反复被提起的话题。单租户系统里一个接口慢了就是接口慢了多租户系统里你还要回答是哪个租户慢了是普遍慢还是特定租户慢没有租户维度数据这两个问题都答不上来。所以日志、监控、链路追踪全部要有租户维度。日志上打印业务日志时把tenant_id作为结构化字段带上监控上每个核心API的耗时、错误率按租户维度做聚合链路追踪上把tenant_id作为自定义Tag透传到整条调用链。这样才能支撑起按租户排障的能力。成本方面也要考虑。按租户维度全量记录日志是有代价的存储成本和查询性能都是问题。我的做法是全量日志保留短期按天滚动清理租户维度的聚合指标长期保留在监控系统里真正需要深挖某个租户问题时再按租户ID去日志系统过滤检索。另外告警规则也要带租户维度比如某个租户的错误率超过阈值就单独告警而不是等全局指标出问题才被动处理。多租户系统的可用性管理和单租户最大的区别就在这里局部问题要能局部发现、局部处置。5.3 多租户与网关、分布式架构的结合多租户不是孤立的设计它和网关、微服务、分布式架构的耦合非常深。前面讲到的租户路由最初的落点往往就是网关。公司从单体向微服务演进时多租户能力也要跟着拆分解耦。网关层要统一负责租户识别和基础校验。每个请求进来先解析域名或路径拿到租户ID校验租户状态和套餐然后构造统一的请求上下文Header转发给下游。这样做的好处是下游所有服务不需要各自解析租户信息只管从请求头里取。RPC调用时把租户上下文从Header透传到下游服务消息队列场景则把tenant_id作为消息头或消息体字段消费端解析后重建租户上下文。这套链路任何一个环节断了都会出现上下文丢失进而导致数据越权或任务错乱。分布式事务是另一个头疼的问题。多租户下的分布式事务隔离边界必须按租户去控制。比如下单流程涉及订单服务和库存服务如果订单服务处理的是租户A的数据库存服务不能因为租户B的数据异常而把整个事务回滚掉。事务的发起、回滚范围要精确到租户粒度这比单租户系统的全局事务复杂得多。实践中我建议把长事务拆成短事务并在事务日志里记录租户ID方便出问题时按租户维度重新对账。我在做架构规划时习惯把多租户当成一个横切关注点来对待而不是一个功能模块。它和权限、限流、审计、可观测性这些能力天然交织应该由框架和平台层统一提供而不是让每个业务服务各自实现一套。这也是为什么很多团队最终会把多租户能力沉淀成内部基础组件供所有业务线复用——前面所有章节讲的租户上下文、路由、配额最终都应该收敛到这么一套组件里。踩过这么多坑之后我个人的体会是多租户架构设计没有银弹三种隔离模型也好各种路由和缓存技巧也罢本质上都是在成本、隔离性、运维复杂度之间做权衡。对基础篇来说最重要的不是记住某个具体方案而是建立两套思维一是“所有链路都要带租户身份”的横切思维二是“先想清楚共享和隔离的边界再动手”的决策思维。如果你正在启动一个SaaS项目我建议先从池化模式入手把租户上下文、数据隔离、配额管理这套基本功打扎实等客户结构逐步清晰了再沿着独享Schema、独享数据库的路径演进。最后再分享一个小技巧每一个新模块上线前都在多租户测试环境里用两个真实租户的数据做一遍交叉验证——A租户登录永远看不到B租户的任何数据。这个最朴素的测试用例比任何架构评审都管用。
阅读完成 · 觉得有帮助?
咨询建站