RuoYi-Cloud 多租户架构深度解析与实践指南
引言:企业级SaaS应用的核心挑战
在当今云原生时代,多租户(Multi-Tenancy)架构已成为企业级SaaS(Software as a Service)应用的标配。你是否曾面临这样的困境:
- 不同客户数据如何安全隔离?
- 系统资源如何高效共享?
- 租户自定义需求如何灵活满足?
- 系统扩展性如何保证?
RuoYi-Cloud作为基于Spring Cloud Alibaba的分布式微服务架构,为多租户场景提供了完整的解决方案。本文将深入解析RuoYi-Cloud的多租户架构设计,带你掌握企业级SaaS应用的核心技术。
多租户架构模式对比
在深入RuoYi-Cloud实现之前,我们先了解三种主流的多租户模式:
模式选择策略表
| 模式 | 隔离级别 | 成本 | 性能 | 适用场景 |
|---|---|---|---|---|
| 独立数据库 | 高 | 高 | 高 | 金融、医疗等高安全要求 |
| 共享数据库独立Schema | 中 | 中 | 中 | 中型企业应用 |
| 共享数据库共享Schema | 低 | 低 | 低 | 小型应用、初创公司 |
RuoYi-Cloud多租户架构设计
核心架构图
租户识别机制
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);
}
}
}
监控与告警
安全考虑与数据保护
租户数据隔离安全策略
// 租户数据访问权限校验
@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多租户架构的核心技术和最佳实践。现在就开始构建你的企业级多租户应用吧!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



