RuoYi-Cloud 多租户架构深度解析与实践指南

RuoYi-Cloud 多租户架构深度解析与实践指南

【免费下载链接】RuoYi-Cloud 🎉 基于Spring Boot、Spring Cloud & Alibaba的分布式微服务架构权限管理系统,同时提供了 Vue3 的版本 【免费下载链接】RuoYi-Cloud 项目地址: https://gitcode.com/yangzongzhuan/RuoYi-Cloud

引言:企业级SaaS应用的核心挑战

在当今云原生时代,多租户(Multi-Tenancy)架构已成为企业级SaaS(Software as a Service)应用的标配。你是否曾面临这样的困境:

  • 不同客户数据如何安全隔离?
  • 系统资源如何高效共享?
  • 租户自定义需求如何灵活满足?
  • 系统扩展性如何保证?

RuoYi-Cloud作为基于Spring Cloud Alibaba的分布式微服务架构,为多租户场景提供了完整的解决方案。本文将深入解析RuoYi-Cloud的多租户架构设计,带你掌握企业级SaaS应用的核心技术。

多租户架构模式对比

在深入RuoYi-Cloud实现之前,我们先了解三种主流的多租户模式:

mermaid

模式选择策略表

模式隔离级别成本性能适用场景
独立数据库金融、医疗等高安全要求
共享数据库独立Schema中型企业应用
共享数据库共享Schema小型应用、初创公司

RuoYi-Cloud多租户架构设计

核心架构图

mermaid

租户识别机制

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);
        }
    }
}

监控与告警

mermaid

安全考虑与数据保护

租户数据隔离安全策略

// 租户数据访问权限校验
@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应用提供了完整的解决方案,具备以下优势:

  1. 灵活的数据隔离策略:支持数据库级、Schema级、数据行级多种隔离方式
  2. 动态数据源管理:基于租户上下文自动路由到正确的数据源
  3. 完善的租户管理:提供租户创建、配置、监控等完整生命周期管理
  4. 强大的扩展能力:支持水平扩展,轻松应对大量租户场景
  5. 严格的安全控制:确保租户数据隔离和访问安全

未来发展方向:

  • 支持更多数据库类型(PostgreSQL、Oracle等)
  • 提供租户数据迁移工具
  • 增强多租户下的性能监控和优化
  • 支持跨租户的数据分析和报表功能

通过本文的深入解析,相信你已经掌握了RuoYi-Cloud多租户架构的核心技术和最佳实践。现在就开始构建你的企业级多租户应用吧!

【免费下载链接】RuoYi-Cloud 🎉 基于Spring Boot、Spring Cloud & Alibaba的分布式微服务架构权限管理系统,同时提供了 Vue3 的版本 【免费下载链接】RuoYi-Cloud 项目地址: https://gitcode.com/yangzongzhuan/RuoYi-Cloud

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值