Skip to content

多租户 (tenant)

概述

租户插件模块(ruoyi-common-tenant)是一个完整的多租户解决方案,提供了数据库、缓存、认证等多个层面的租户隔离功能。该模块确保不同租户的数据完全隔离,避免数据泄露和混淆。

核心特性

  • 数据库隔离:自动为SQL查询添加租户条件
  • 缓存隔离:Redis缓存和Spring Cache支持租户前缀
  • 认证隔离:SaToken认证数据支持租户隔离
  • 动态租户:支持运行时切换租户上下文
  • 忽略机制:支持跳过租户过滤的场景
  • 配置灵活:可配置排除表和开关控制

模块结构

text
ruoyi-common-tenant/
├── config/                 # 配置类
│   └── TenantConfig.java
├── core/                   # 核心组件
│   ├── TenantEntity.java
│   └── TenantSaTokenDao.java
├── exception/              # 异常类
│   └── TenantException.java
├── handle/                 # 处理器
│   ├── PlusTenantLineHandler.java
│   └── TenantKeyPrefixHandler.java
├── helper/                 # 工具类
│   └── TenantHelper.java
├── manager/                # 管理器
│   └── TenantSpringCacheManager.java
└── properties/             # 配置属性
    └── TenantProperties.java

快速开始

1. 添加依赖

pom.xml 中添加依赖:

xml
<dependency>
    <groupId>plus.ruoyi</groupId>
    <artifactId>ruoyi-common-tenant</artifactId>
</dependency>

2. 配置文件

application.yml 中添加配置:

yaml
# 多租户配置
tenant:
  # 是否启用多租户功能
  enable: true
  # 排除表列表(这些表不会应用租户过滤)
  excludes:
    - sys_user
    - sys_role
    - sys_config

3. 实体类配置

继承 TenantEntity 类:

java
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("your_table")
public class YourEntity extends TenantEntity {
    // 你的字段
    private String name;
    private String description;
}

核心组件

TenantHelper

租户助手工具类,提供租户相关的核心操作:

基础功能

java
// 检查多租户功能是否启用
boolean enabled = TenantHelper.isEnable();

// 获取当前租户ID
String tenantId = TenantHelper.getTenantId();

忽略租户模式

在某些场景下需要跳过租户过滤:

java
// 在忽略模式下执行代码
TenantHelper.ignore(() -> {
    // 这里的数据库操作不会添加租户条件
    userService.getAllUsers();
});

// 带返回值的忽略模式
List<User> allUsers = TenantHelper.ignore(() -> {
    return userService.getAllUsers();
});

// 手动控制忽略模式
TenantHelper.enableIgnore();
try {
    // 业务代码
} finally {
    TenantHelper.disableIgnore();
}

动态租户切换

运行时切换到指定租户:

java
// 临时切换到指定租户执行代码
TenantHelper.dynamic("tenant123", () -> {
    // 这里的操作都在tenant123租户下执行
    dataService.queryData();
});

// 带返回值的动态租户切换
List<Data> data = TenantHelper.dynamic("tenant123", () -> {
    return dataService.queryData();
});

// 手动设置动态租户
TenantHelper.setDynamic("tenant123");
try {
    // 业务代码
} finally {
    TenantHelper.clearDynamic();
}

// 全局设置动态租户(存储到Redis)
TenantHelper.setDynamic("tenant123", true);

TenantEntity

多租户实体基类:

java
@Data
@EqualsAndHashCode(callSuper = true)
public class TenantEntity extends BaseEntity {
    /**
     * 租户ID - 自动管理,无需手动设置
     */
    private String tenantId;
}

数据库隔离

PlusTenantLineHandler

自动为SQL添加租户条件:

java
// 原始SQL
SELECT * FROM user WHERE status = 1

// 自动添加租户条件后
SELECT * FROM user WHERE status = 1 AND tenant_id = 'current_tenant_id'

排除表配置

某些系统表不需要租户隔离:

java
// 系统固定排除表(硬编码)
- sys_tenant
- sys_tenant_package  
- sys_gen_table
- sys_gen_table_column
- sys_menu
- sys_role_menu
- sys_role_dept
- sys_user_role
- sys_user_post
- sys_oss_config

// 配置文件排除表
tenant:
  excludes:
    - your_global_table
    - another_global_table

缓存隔离

Redis缓存隔离

java
// 原始Redis键
user:info:123

// 自动添加租户前缀
tenant123:user:info:123

Spring Cache隔离

java
@Cacheable(value = "userCache", key = "#userId")
public User getUser(String userId) {
    // 缓存键自动添加租户前缀
    // 实际缓存键:tenant123:userCache::userId
    return userRepository.findById(userId);
}

认证隔离

SaToken认证数据自动添加全局前缀,确保不同租户的认证信息隔离。

配置说明

TenantProperties

java
@ConfigurationProperties(prefix = "tenant")
public class TenantProperties {
    /**
     * 是否启用多租户功能
     */
    private Boolean enable;
    
    /**
     * 多租户排除表列表
     */
    private List<String> excludes;
}

配置示例

yaml
tenant:
  # 启用多租户
  enable: true
  # 排除表配置
  excludes:
    - sys_config      # 系统配置表
    - sys_dict_type   # 字典类型表
    - sys_dict_data   # 字典数据表
    - sys_notice      # 通知公告表(如果是全局通知)

使用场景

1. SaaS应用

java
@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    // 查询当前租户的订单 - 自动添加租户条件
    public List<Order> getCurrentTenantOrders() {
        return orderMapper.selectList(null);
    }
    
    // 管理员查看所有租户的订单
    public List<Order> getAllTenantsOrders() {
        return TenantHelper.ignore(() -> {
            return orderMapper.selectList(null);
        });
    }
    
    // 数据迁移:将数据从一个租户转移到另一个租户
    public void migrateData(String fromTenant, String toTenant) {
        List<Order> orders = TenantHelper.dynamic(fromTenant, () -> {
            return orderMapper.selectList(null);
        });
        
        TenantHelper.dynamic(toTenant, () -> {
            orders.forEach(order -> {
                order.setId(null); // 清除ID,插入新记录
                orderMapper.insert(order);
            });
        });
    }
}

2. 多租户报表

java
@Service
public class ReportService {
    
    // 生成当前租户报表
    @Cacheable(value = "report", key = "#type")
    public Report generateReport(String type) {
        // 缓存键自动添加租户前缀
        return reportGenerator.generate(type);
    }
    
    // 生成全局报表(所有租户)
    public Report generateGlobalReport(String type) {
        return TenantHelper.ignore(() -> {
            return reportGenerator.generateGlobal(type);
        });
    }
}

3. 系统管理功能

java
@Service
public class SystemService {
    
    // 系统配置管理(全局,不受租户影响)
    public void updateSystemConfig(String key, String value) {
        TenantHelper.ignore(() -> {
            configMapper.updateByKey(key, value);
        });
    }
    
    // 租户特定配置
    public void updateTenantConfig(String key, String value) {
        // 自动应用租户条件
        tenantConfigMapper.updateByKey(key, value);
    }
}

最佳实践

1. 数据库设计

sql
-- 租户表需要包含tenant_id字段
CREATE TABLE user_info (
    id BIGINT PRIMARY KEY,
    tenant_id VARCHAR(20) NOT NULL,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100),
    create_time DATETIME,
    INDEX idx_tenant_id (tenant_id)
);

-- 全局表不包含tenant_id字段
CREATE TABLE sys_config (
    id BIGINT PRIMARY KEY,
    config_key VARCHAR(100) NOT NULL,
    config_value TEXT,
    create_time DATETIME
);

2. 实体类设计

java
// 租户相关实体
@TableName("user_info")
public class UserInfo extends TenantEntity {
    private String username;
    private String email;
    // 不需要手动设置tenantId
}

// 全局实体
@TableName("sys_config")
public class SysConfig extends BaseEntity {
    private String configKey;
    private String configValue;
    // 不继承TenantEntity
}

3. 服务层设计

java
@Service
public class UserService {
    
    // 常规业务方法 - 自动应用租户过滤
    public List<UserInfo> getUserList() {
        return userMapper.selectList(null);
    }
    
    // 系统管理方法 - 需要忽略租户过滤
    public List<UserInfo> getAllUsersForAdmin() {
        return TenantHelper.ignore(() -> {
            return userMapper.selectList(null);
        });
    }
    
    // 跨租户操作 - 使用动态租户
    public void syncUserData(String sourceTenant, String targetTenant) {
        List<UserInfo> users = TenantHelper.dynamic(sourceTenant, () -> {
            return userMapper.selectList(null);
        });
        
        TenantHelper.dynamic(targetTenant, () -> {
            users.forEach(user -> {
                user.setId(null);
                userMapper.insert(user);
            });
        });
    }
}

4. 缓存使用

java
@Service
public class CacheService {
    
    // 租户级缓存
    @Cacheable(value = "userData", key = "#userId")
    public UserData getUserData(String userId) {
        // 缓存键:tenantId:userData::userId
        return userDataMapper.selectById(userId);
    }
    
    // 全局缓存
    @Cacheable(value = "global:systemData", key = "#key")
    public SystemData getSystemData(String key) {
        return TenantHelper.ignore(() -> {
            // 缓存键:global:systemData::key(不添加租户前缀)
            return systemDataMapper.selectByKey(key);
        });
    }
}

注意事项

1. 数据一致性

  • 所有租户相关的表都必须包含 tenant_id 字段
  • 建议为 tenant_id 字段创建索引以提高查询性能
  • 数据库迁移时要注意租户数据的完整性

2. 性能考虑

  • 合理使用缓存来减少数据库查询
  • 大量数据查询时考虑分页处理
  • 监控SQL执行计划,确保租户条件使用了索引

3. 安全注意

  • 严格控制 TenantHelper.ignore() 的使用范围
  • 跨租户操作需要有适当的权限控制
  • 定期审计租户数据访问日志

4. 开发规范

  • 新增实体类时明确是否需要继承 TenantEntity
  • 系统级功能要明确是否需要忽略租户过滤
  • 测试时要验证租户隔离的有效性

故障排除

常见问题

  1. 数据查询不到

    • 检查是否正确设置了租户ID
    • 确认表是否包含 tenant_id 字段
    • 验证表是否被错误地加入了排除列表
  2. 缓存数据错乱

    • 检查缓存键是否正确添加了租户前缀
    • 确认全局缓存是否正确使用了 global: 前缀
  3. 认证异常

    • 检查SaToken配置是否正确
    • 确认多租户SaTokenDao是否正常工作

调试方法

启用SQL日志查看实际执行的SQL:

yaml
logging:
  level:
    com.baomidou.mybatisplus: DEBUG

查看租户ID获取过程:

java
String tenantId = TenantHelper.getTenantId();
log.info("当前租户ID: {}", tenantId);