RuoYi-Cloud 多租户架构深度解析与实践指南
2026-02-04 04:09:53作者:范靓好Udolf
引言:企业级SaaS应用的核心挑战
在当今云原生时代,多租户(Multi-Tenancy)架构已成为企业级SaaS(Software as a Service)应用的标配。你是否曾面临这样的困境:
- 不同客户数据如何安全隔离?
- 系统资源如何高效共享?
- 租户自定义需求如何灵活满足?
- 系统扩展性如何保证?
RuoYi-Cloud作为基于Spring Cloud Alibaba的分布式微服务架构,为多租户场景提供了完整的解决方案。本文将深入解析RuoYi-Cloud的多租户架构设计,带你掌握企业级SaaS应用的核心技术。
多租户架构模式对比
在深入RuoYi-Cloud实现之前,我们先了解三种主流的多租户模式:
flowchart TD
A[多租户架构模式] --> B[独立数据库]
A --> C[共享数据库独立Schema]
A --> D[共享数据库共享Schema]
B --> B1[最高隔离级别]
B --> B2[最高成本]
B --> B3[最佳性能]
C --> C1[中等隔离级别]
C --> C2[中等成本]
C --> C3[中等性能]
D --> D1[最低隔离级别]
D --> D2[最低成本]
D --> D3[最低性能]
模式选择策略表
| 模式 | 隔离级别 | 成本 | 性能 | 适用场景 |
|---|---|---|---|---|
| 独立数据库 | 高 | 高 | 高 | 金融、医疗等高安全要求 |
| 共享数据库独立Schema | 中 | 中 | 中 | 中型企业应用 |
| 共享数据库共享Schema | 低 | 低 | 低 | 小型应用、初创公司 |
RuoYi-Cloud多租户架构设计
核心架构图
graph TB
subgraph "客户端层"
A[Web前端] --> B[移动端]
end
subgraph "网关层"
C[API Gateway] --> D[租户识别]
D --> E[路由转发]
end
subgraph "业务服务层"
F[认证服务] --> G[用户服务]
H[数据服务] --> I[租户上下文]
end
subgraph "数据层"
J[主数据库 master] --> K[租户1数据库]
J --> L[租户2数据库]
J --> M[租户N数据库]
end
E --> F
E --> G
E --> H
I --> J
租户识别机制
RuoYi-Cloud采用基于子域名和请求头的双重租户识别策略:
// 租户上下文管理器
public class TenantContext {
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static String getTenantId() {
return CURRENT_TENANT.get();
}
public static void clear() {
CURRENT_TENANT.remove();
}
}
// 租户拦截器
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 从子域名提取租户ID
String tenantId = extractTenantFromDomain(request);
if (tenantId == null) {
// 从请求头获取租户ID
tenantId = request.getHeader("X-Tenant-ID");
}
if (tenantId != null) {
TenantContext.setTenantId(tenantId);
}
return true;
}
private String extractTenantFromDomain(HttpServletRequest request) {
String serverName = request.getServerName();
// 解析 tenant1.example.com 格式
if (serverName.contains(".")) {
String[] parts = serverName.split("\\.");
if (parts.length >= 3) {
return parts[0];
}
}
return null;
}
}
动态数据源路由
RuoYi-Cloud基于MyBatis-Plus的动态数据源实现多租户数据隔离:
# application.yml 配置
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
master:
url: jdbc:mysql://localhost:3306/ry_master
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
tenant_001:
url: jdbc:mysql://localhost:3306/ry_tenant_001
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
tenant_002:
url: jdbc:mysql://localhost:3306/ry_tenant_002
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
// 动态数据源选择器
public class TenantDataSourceSelector {
@Autowired
private DynamicDataSourceProvider dynamicDataSourceProvider;
public DataSource determineTargetDataSource() {
String tenantId = TenantContext.getTenantId();
if (tenantId == null) {
// 返回主数据源
return dynamicDataSourceProvider.getDataSource("master");
}
String dataSourceKey = "tenant_" + tenantId;
DataSource dataSource = dynamicDataSourceProvider.getDataSource(dataSourceKey);
if (dataSource == null) {
// 租户数据源不存在,创建新的数据源
dataSource = createTenantDataSource(tenantId);
dynamicDataSourceProvider.addDataSource(dataSourceKey, dataSource);
}
return dataSource;
}
private DataSource createTenantDataSource(String tenantId) {
// 动态创建租户数据库连接
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/ry_tenant_" + tenantId);
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
return dataSource;
}
}
多租户数据隔离策略
1. 数据库级别隔离
-- 租户数据库初始化脚本
CREATE DATABASE IF NOT EXISTS `ry_tenant_001` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS `ry_tenant_002` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 租户表结构(每个租户独立的表)
CREATE TABLE `ry_tenant_001`.`sys_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT,
`tenant_id` varchar(50) NOT NULL DEFAULT '001',
`user_name` varchar(30) NOT NULL,
`nick_name` varchar(30) NOT NULL,
PRIMARY KEY (`user_id`),
KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. Schema级别隔离
-- 共享数据库,不同Schema
CREATE SCHEMA `tenant_001` DEFAULT CHARACTER SET utf8mb4;
CREATE SCHEMA `tenant_002` DEFAULT CHARACTER SET utf8mb4;
-- 为每个租户创建相同的表结构
CREATE TABLE `tenant_001`.`sys_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_name` varchar(30) NOT NULL,
`nick_name` varchar(30) NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 数据行级别隔离
// 基于MyBatis-Plus的多租户插件
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 多租户插件
TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 获取当前租户ID
String tenantId = TenantContext.getTenantId();
return new StringValue(tenantId);
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
// 忽略不需要租户隔离的表
return "sys_tenant".equals(tableName) ||
"sys_config".equals(tableName);
}
});
interceptor.addInnerInterceptor(tenantInterceptor);
return interceptor;
}
}
租户管理功能实现
租户注册与配置
// 租户服务接口
public interface TenantService {
/**
* 创建新租户
*/
Tenant createTenant(TenantCreateRequest request);
/**
* 初始化租户数据
*/
void initTenantData(String tenantId);
/**
* 禁用租户
*/
void disableTenant(String tenantId);
/**
* 启用租户
*/
void enableTenant(String tenantId);
}
// 租户实体
@Data
@TableName("sys_tenant")
public class Tenant {
@TableId(type = IdType.AUTO)
private Long id;
@NotBlank
private String tenantId;
@NotBlank
private String tenantName;
private String description;
private Integer status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
租户自定义配置
// 租户配置管理
@Service
public class TenantConfigService {
@Autowired
private TenantConfigMapper tenantConfigMapper;
/**
* 获取租户特定配置
*/
public String getConfig(String tenantId, String configKey) {
return tenantConfigMapper.selectConfigValue(tenantId, configKey);
}
/**
* 设置租户配置
*/
public void setConfig(String tenantId, String configKey, String configValue) {
TenantConfig config = new TenantConfig();
config.setTenantId(tenantId);
config.setConfigKey(configKey);
config.setConfigValue(configValue);
config.setUpdateTime(new Date());
tenantConfigMapper.insertOrUpdate(config);
}
/**
* 获取租户所有配置
*/
public Map<String, String> getAllConfigs(String tenantId) {
List<TenantConfig> configs = tenantConfigMapper.selectByTenantId(tenantId);
return configs.stream()
.collect(Collectors.toMap(
TenantConfig::getConfigKey,
TenantConfig::getConfigValue
));
}
}
性能优化与最佳实践
数据库连接池优化
# 租户数据源连接池配置
spring:
datasource:
dynamic:
datasource:
master:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
tenant_001:
hikari:
maximum-pool-size: 10
minimum-idle: 3
connection-timeout: 30000
缓存策略设计
// 租户感知的缓存管理器
@Component
public class TenantAwareCacheManager implements CacheManager {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Cache getCache(String name) {
String tenantId = TenantContext.getTenantId();
String cacheKey = tenantId != null ? tenantId + ":" + name : name;
return new RedisCache(cacheKey, redisTemplate);
}
// 清除特定租户的缓存
public void clearTenantCache(String tenantId, String cacheName) {
String pattern = tenantId + ":" + cacheName + "*";
Set<String> keys = redisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
}
监控与告警
sequenceDiagram
participant C as 客户端
participant G as 网关
participant S as 服务
participant D as 数据库
participant M as 监控系统
C->>G: 请求(含租户ID)
G->>S: 转发请求
S->>D: 数据库操作
D-->>S: 返回数据
S-->>G: 返回响应
G-->>C: 返回结果
Note right of S: 记录租户操作日志
S->>M: 发送监控指标
M->>M: 分析性能数据
M->>M: 检测异常模式
M->>M: 触发告警规则
安全考虑与数据保护
租户数据隔离安全策略
// 租户数据访问权限校验
@Aspect
@Component
public class TenantDataAccessAspect {
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
"@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public Object checkTenantAccess(ProceedingJoinPoint joinPoint) throws Throwable {
String currentTenantId = TenantContext.getTenantId();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 检查方法参数中的租户ID是否匹配
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof BaseEntity) {
BaseEntity entity = (BaseEntity) arg;
if (entity.getTenantId() != null &&
!entity.getTenantId().equals(currentTenantId)) {
throw new SecurityException("跨租户数据访问被拒绝");
}
}
}
return joinPoint.proceed();
}
}
审计日志记录
// 租户操作审计
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class TenantAuditEntity {
@CreatedBy
@Column(name = "create_by", updatable = false)
private String createBy;
@CreatedDate
@Column(name = "create_time", updatable = false)
private Date createTime;
@LastModifiedBy
@Column(name = "update_by")
private String updateBy;
@LastModifiedDate
@Column(name = "update_time")
private Date updateTime;
@Column(name = "tenant_id")
private String tenantId;
// 记录操作IP
@Column(name = "oper_ip")
private String operIp;
// 记录用户代理
@Column(name = "user_agent")
private String userAgent;
}
部署与运维指南
Docker Compose多租户部署
version: '3.8'
services:
# 主数据库
mysql-master:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: ry_master
ports:
- "3306:3306"
volumes:
- ./mysql/master:/var/lib/mysql
# 租户数据库实例
mysql-tenant-001:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: ry_tenant_001
ports:
- "3307:3306"
volumes:
- ./mysql/tenant_001:/var/lib/mysql
mysql-tenant-002:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: ry_tenant_002
ports:
- "3308:3306"
volumes:
- ./mysql/tenant_002:/var/lib/mysql
# 应用服务
ruoyi-cloud-app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_DATASOURCE_MASTER_URL=jdbc:mysql://mysql-master:3306/ry_master
- SPRING_DATASOURCE_TENANT_001_URL=jdbc:mysql://mysql-tenant-001:3306/ry_tenant_001
- SPRING_DATASOURCE_TENANT_002_URL=jdbc:mysql://mysql-tenant-002:3306/ry_tenant_002
depends_on:
- mysql-master
- mysql-tenant-001
- mysql-tenant-002
自动化运维脚本
#!/bin/bash
# 租户数据库备份脚本
TENANTS=("tenant_001" "tenant_002" "tenant_003")
BACKUP_DIR="/backup/$(date +%Y%m%d)"
MYSQL_USER="root"
MYSQL_PASSWORD="root123"
mkdir -p $BACKUP_DIR
for tenant in "${TENANTS[@]}"; do
echo "Backing up database for tenant: $tenant"
mysqldump -u$MYSQL_USER -p$MYSQL_PASSWORD \
--single-transaction \
--routines \
--triggers \
ry_$tenant > "$BACKUP_DIR/ry_${tenant}_backup.sql"
# 压缩备份文件
gzip "$BACKUP_DIR/ry_${tenant}_backup.sql"
echo "Backup completed for tenant: $tenant"
done
echo "All tenant backups completed in: $BACKUP_DIR"
总结与展望
RuoYi-Cloud的多租户架构为企业级SaaS应用提供了完整的解决方案,具备以下优势:
- 灵活的数据隔离策略:支持数据库级、Schema级、数据行级多种隔离方式
- 动态数据源管理:基于租户上下文自动路由到正确的数据源
- 完善的租户管理:提供租户创建、配置、监控等完整生命周期管理
- 强大的扩展能力:支持水平扩展,轻松应对大量租户场景
- 严格的安全控制:确保租户数据隔离和访问安全
未来发展方向:
- 支持更多数据库类型(PostgreSQL、Oracle等)
- 提供租户数据迁移工具
- 增强多租户下的性能监控和优化
- 支持跨租户的数据分析和报表功能
通过本文的深入解析,相信你已经掌握了RuoYi-Cloud多租户架构的核心技术和最佳实践。现在就开始构建你的企业级多租户应用吧!
登录后查看全文
热门项目推荐
相关项目推荐
Kimi-K2.5Kimi K2.5 是一款开源的原生多模态智能体模型,它在 Kimi-K2-Base 的基础上,通过对约 15 万亿混合视觉和文本 tokens 进行持续预训练构建而成。该模型将视觉与语言理解、高级智能体能力、即时模式与思考模式,以及对话式与智能体范式无缝融合。Python00- QQwen3-Coder-Next2026年2月4日,正式发布的Qwen3-Coder-Next,一款专为编码智能体和本地开发场景设计的开源语言模型。Python00
xw-cli实现国产算力大模型零门槛部署,一键跑通 Qwen、GLM-4.7、Minimax-2.1、DeepSeek-OCR 等模型Go06
PaddleOCR-VL-1.5PaddleOCR-VL-1.5 是 PaddleOCR-VL 的新一代进阶模型,在 OmniDocBench v1.5 上实现了 94.5% 的全新 state-of-the-art 准确率。 为了严格评估模型在真实物理畸变下的鲁棒性——包括扫描伪影、倾斜、扭曲、屏幕拍摄和光照变化——我们提出了 Real5-OmniDocBench 基准测试集。实验结果表明,该增强模型在新构建的基准测试集上达到了 SOTA 性能。此外,我们通过整合印章识别和文本检测识别(text spotting)任务扩展了模型的能力,同时保持 0.9B 的超紧凑 VLM 规模,具备高效率特性。Python00
KuiklyUI基于KMP技术的高性能、全平台开发框架,具备统一代码库、极致易用性和动态灵活性。 Provide a high-performance, full-platform development framework with unified codebase, ultimate ease of use, and dynamic flexibility. 注意:本仓库为Github仓库镜像,PR或Issue请移步至Github发起,感谢支持!Kotlin07
VLOOKVLOOK™ 是优雅好用的 Typora/Markdown 主题包和增强插件。 VLOOK™ is an elegant and practical THEME PACKAGE × ENHANCEMENT PLUGIN for Typora/Markdown.Less00
项目优选
收起
deepin linux kernel
C
27
11
OpenHarmony documentation | OpenHarmony开发者文档
Dockerfile
525
3.72 K
Ascend Extension for PyTorch
Python
332
395
暂无简介
Dart
766
189
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
878
586
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
336
165
React Native鸿蒙化仓库
JavaScript
302
352
Nop Platform 2.0是基于可逆计算理论实现的采用面向语言编程范式的新一代低代码开发平台,包含基于全新原理从零开始研发的GraphQL引擎、ORM引擎、工作流引擎、报表引擎、规则引擎、批处理引引擎等完整设计。nop-entropy是它的后端部分,采用java语言实现,可选择集成Spring框架或者Quarkus框架。中小企业可以免费商用
Java
12
1
🎉 (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统
Vue
1.33 K
748
openJiuwen agent-studio提供零码、低码可视化开发和工作流编排,模型、知识库、插件等各资源管理能力
TSX
985
246