1. 项目概述:从单体到多租户SaaS的惊险一跃
“一套代码,服务一千个客户”,这听起来像是每个技术团队都梦寐以求的蓝图,既能最大化代码复用、降低维护成本,又能实现业务的规模化扩张。但当你真正手握一个像若依(RuoYi)这样成熟、完善的开源单体后台管理系统,并试图将其改造成一个支撑上千家不同企业客户的SaaS平台时,才会发现这绝非简单的“魔改”,而是一场涉及架构、数据、业务和安全等多维度的系统性重构。我带领团队走完了这条路,期间踩过的坑、绕过的弯,以及最终沉淀下来的方案,或许能给你一些实实在在的参考。
若依本身是一个优秀的、基于Spring Boot的权限管理系统,它提供了用户、角色、菜单、部门等后台管理的基础骨架,代码结构清晰,生态丰富。但它的基因是“单体”和“单租户”,即一套部署服务于一个组织内部。而SaaS(Software as a Service)的核心是“多租户”,要求一套运行实例能为多个相互隔离的客户(租户)提供服务。这其中的鸿沟,就是我们需要用“魔改”来填补的。我们的目标很明确:在尽可能复用若依优秀基础功能的前提下,以最小的改动成本和未来的可维护性为代价,实现安全、高效、可扩展的多租户能力。这不是推翻重来,而是在原有地基上,巧妙地加盖一栋能容纳千户的摩天大楼。
2. 核心架构选型:数据隔离是首要命题
改造的第一步,也是决定后续所有技术路径的基石,就是确定多租户的数据隔离策略。市面上主流有三种方案,我们做了详细的对比和压力测试。
2.1 三种多租户数据模型深度对比
我们首先排除了“独立数据库”方案。即为每个租户分配一个独立的物理数据库。它的隔离性最强,安全性最高,性能最好,但成本也最高。管理1000个数据库的连接、备份、迁移和版本升级将是运维的噩梦,且资源利用率低。这对于我们初期希望快速验证商业模式、控制成本的目标不符。
剩下的就是在“共享数据库,共享数据表”和“共享数据库,独立数据表”之间做抉择。前者是所有租户的数据都存放在同一套表的相同行中,通过一个 tenant_id 字段来区分。后者是每个租户拥有自己专属的一套表,表名通常包含租户标识,例如 order_tenantA , order_tenantB 。
我们最终选择了 “共享数据库,共享数据表” 方案,并在此基础上做了大量增强。理由如下:
- 开发与维护成本最低 :无需动态创建表,ORM映射(MyBatis)保持简单,SQL编写和复杂查询构建几乎无需改变,只需在查询中自动附加
tenant_id条件。这对于快速改造若依这样已有大量SQL和Mapper的项目至关重要。 - 租户容量弹性最大 :理论上,只要数据库性能撑得住,租户数量可以无限扩展。而分表方案在租户数量极大时,数据库中的表数量会爆炸,可能触及数据库元数据管理的上限。
- 若依生态兼容性最好 :若依的代码生成器、已有的业务模块(如系统管理、日志监控)都能相对平滑地迁移,我们只需要在数据访问层做统一的拦截处理。
当然,这个方案的缺点也很明显:数据隔离依赖于应用层逻辑,存在误操作导致数据泄露的风险;所有租户的数据挤在一起,对单表性能和大数据量下的查询优化提出了更高要求。但这些缺点,我们通过后续的技术手段进行了严格的规避和优化。
2.2 租户标识与请求链路透传设计
确定了数据模型,接下来要解决如何在一次请求中,让系统无感知地识别当前是哪个租户。我们设计了一套从请求入口到数据层的完整透传方案。
核心思路:将租户标识(Tenant ID)作为一个与业务解耦的上下文(Context),在整个请求生命周期中传递。
-
入口识别 :
- 子域名 :这是我们的一级路由策略。例如,客户A访问
a.company.com,客户B访问b.company.com。在网关层(我们使用了Spring Cloud Gateway)或第一个进入的Web过滤器(Filter)中,解析请求的Host头,从子域名映射出对应的tenant_id。这种方式对用户最友好,也符合SaaS的使用习惯。 - 请求头/路径参数 :作为备用方案,在移动端API或特殊场景下,支持通过
X-Tenant-Id请求头或URL路径参数(如/api/xxx?tenantId=abc)传递。但子域名是首选。
- 子域名 :这是我们的一级路由策略。例如,客户A访问


442

被折叠的 条评论
为什么被折叠?



