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多租户架构的核心技术和最佳实践。现在就开始构建你的企业级多租户应用吧!
登录后查看全文
热门项目推荐
相关项目推荐
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0153- DDeepSeek-V4-ProDeepSeek-V4-Pro(总参数 1.6 万亿,激活 49B)面向复杂推理和高级编程任务,在代码竞赛、数学推理、Agent 工作流等场景表现优异,性能接近国际前沿闭源模型。Python00
LongCat-Video-Avatar-1.5最新开源LongCat-Video-Avatar 1.5 版本,这是一款经过升级的开源框架,专注于音频驱动人物视频生成的极致实证优化与生产级就绪能力。该版本在 LongCat-Video 基础模型之上构建,可生成高度稳定的商用级虚拟人视频,支持音频-文本转视频(AT2V)、音频-文本-图像转视频(ATI2V)以及视频续播等原生任务,并能无缝兼容单流与多流音频输入。00
auto-devAutoDev 是一个 AI 驱动的辅助编程插件。AutoDev 支持一键生成测试、代码、提交信息等,还能够与您的需求管理系统(例如Jira、Trello、Github Issue 等)直接对接。 在IDE 中,您只需简单点击,AutoDev 会根据您的需求自动为您生成代码。Kotlin03
Intern-S2-PreviewIntern-S2-Preview,这是一款高效的350亿参数科学多模态基础模型。除了常规的参数与数据规模扩展外,Intern-S2-Preview探索了任务扩展:通过提升科学任务的难度、多样性与覆盖范围,进一步释放模型能力。Python00
skillhubopenJiuwen 生态的 Skill 托管与分发开源方案,支持自建与可选 ClawHub 兼容。Python0112
项目优选
收起
暂无描述
Dockerfile
733
4.75 K
deepin linux kernel
C
31
16
Ascend Extension for PyTorch
Python
652
797
Claude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed.
Get Started
Rust
1.25 K
153
旨在打造算法先进、性能卓越、高效敏捷、安全可靠的密码套件,通过轻量级、可剪裁的软件技术架构满足各行业不同场景的多样化要求,让密码技术应用更简单,同时探索后量子等先进算法创新实践,构建密码前沿技术底座!
C
1.1 K
611
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.01 K
1.01 K
华为昇腾面向大规模分布式训练的多模态大模型套件,支撑多模态生成、多模态理解。
Python
147
237
昇腾LLM分布式训练框架
Python
168
200
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
434
395
暂无简介
Dart
986
253