从0到1:RuoYi-Vue-Pro多租户架构设计与实战指南
引言:多租户架构的痛点与解决方案
在企业级应用开发中,如何高效管理多个租户的数据隔离与资源共享是一个核心挑战。传统的独立部署模式不仅运维成本高昂,而且难以快速响应租户的定制化需求。RuoYi-Vue-Pro作为一款成熟的开源后台管理系统,其内置的SaaS(Software as a Service,软件即服务)多租户功能为这一问题提供了优雅的解决方案。
本文将深入剖析RuoYi-Vue-Pro的多租户架构设计,从核心原理到实际应用,帮助开发者全面掌握多租户系统的实现方法。通过阅读本文,您将能够:
- 理解多租户架构的三种实现模式及其优缺点
- 掌握RuoYi-Vue-Pro中多租户功能的核心组件与工作流程
- 学会如何配置和扩展多租户系统以满足不同业务需求
- 解决多租户环境下常见的技术难题,如数据隔离、权限控制等
多租户架构概述
多租户模式对比
多租户架构主要有三种实现模式,每种模式都有其适用场景和优缺点:
| 模式 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 独立数据库 | 为每个租户提供独立的数据库实例 | 隔离级别最高,数据安全有保障,可定制性强 | 硬件成本高,维护复杂,扩展性差 | 对数据隔离要求极高的金融、医疗等行业 |
| 共享数据库,独立Schema | 多个租户共享一个数据库实例,但每个租户拥有独立的Schema | 隔离级别中等,维护成本较低,扩展性较好 | 数据库资源竞争可能影响性能 | 中小型企业应用,租户数量适中 |
| 共享数据库,共享Schema | 所有租户共享同一个数据库和Schema,通过租户ID区分数据 | 硬件成本最低,维护简单,扩展性最好 | 隔离级别最低,数据安全依赖于应用层控制 | 租户数量多、定制化需求少的SaaS应用 |
RuoYi-Vue-Pro采用的是共享数据库,共享Schema的模式,通过在数据表中添加租户ID字段实现数据隔离。这种设计在资源利用率和扩展性之间取得了很好的平衡,非常适合中小型SaaS应用。
RuoYi-Vue-Pro多租户核心特性
RuoYi-Vue-Pro的多租户功能具有以下核心特性:
- 基于MyBatis-Plus的TenantLineInnerInterceptor实现SQL自动拦截与租户ID注入
- 灵活的租户上下文管理,支持通过请求头、Cookie等方式传递租户信息
- 细粒度的忽略规则配置,可针对URL、表名、缓存等设置租户忽略策略
- 完善的租户权限控制,防止租户间数据越权访问
- 多租户与缓存、消息队列等中间件的无缝集成
RuoYi-Vue-Pro多租户核心实现
整体架构设计
RuoYi-Vue-Pro的多租户功能主要通过以下几个核心组件实现:
核心组件解析
1. 租户上下文管理:TenantContextHolder
TenantContextHolder是整个多租户系统的核心,负责在当前线程中存储和传递租户信息。其主要代码实现如下:
public class TenantContextHolder {
/**
* 当前租户编号
*/
private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
/**
* 是否忽略租户
*/
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
// 获取租户ID
public static Long getTenantId() {
return TENANT_ID.get();
}
// 设置租户ID
public static void setTenantId(Long tenantId) {
TENANT_ID.set(tenantId);
}
// 设置是否忽略租户
public static void setIgnore(Boolean ignore) {
IGNORE.set(ignore);
}
// 判断是否忽略租户
public static boolean isIgnore() {
return Boolean.TRUE.equals(IGNORE.get());
}
// 清除上下文信息
public static void clear() {
TENANT_ID.remove();
IGNORE.remove();
}
}
这里使用了TransmittableThreadLocal而非普通的ThreadLocal,是为了解决线程池环境下租户信息传递的问题。TransmittableThreadLocal能够在使用线程池等池化技术时,自动传递ThreadLocal中的值,确保租户信息在异步操作中不会丢失。
2. 数据层拦截:TenantDatabaseInterceptor
TenantDatabaseInterceptor实现了MyBatis-Plus的TenantLineHandler接口,负责在SQL执行过程中自动注入租户ID条件。其核心代码如下:
public class TenantDatabaseInterceptor implements TenantLineHandler {
/**
* 忽略的表
*/
private final Map<String, Boolean> ignoreTables = new HashMap<>();
@Override
public Expression getTenantId() {
return new LongValue(TenantContextHolder.getRequiredTenantId());
}
@Override
public boolean ignoreTable(String tableName) {
// 情况一:全局忽略多租户
if (TenantContextHolder.isIgnore()) {
return true;
}
// 情况二:忽略多租户的表
tableName = SqlParserUtils.removeWrapperSymbol(tableName);
Boolean ignore = ignoreTables.get(tableName.toLowerCase());
if (ignore == null) {
ignore = computeIgnoreTable(tableName);
synchronized (ignoreTables) {
addIgnoreTable(tableName, ignore);
}
}
return ignore;
}
private boolean computeIgnoreTable(String tableName) {
// 找不到的表,说明不是项目里的,不进行拦截(忽略租户)
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
if (tableInfo == null) {
return true;
}
// 如果继承了 TenantBaseDO 基类,显然不忽略租户
if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) {
return false;
}
// 如果添加了 @TenantIgnore 注解,则忽略租户
TenantIgnore tenantIgnore = tableInfo.getEntityType().getAnnotation(TenantIgnore.class);
return tenantIgnore != null;
}
}
该拦截器的主要工作流程是:
- 获取当前线程中的租户ID
- 判断当前表是否需要忽略租户过滤
- 如果不需要忽略,则自动在SQL的WHERE子句中添加租户ID条件
3. 自动配置:YudaoTenantAutoConfiguration
YudaoTenantAutoConfiguration是多租户功能的自动配置类,负责注册相关的Bean:
@AutoConfiguration
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true)
@EnableConfigurationProperties(TenantProperties.class)
public class YudaoTenantAutoConfiguration {
// 注册租户服务
@Bean
public TenantFrameworkService tenantFrameworkService(TenantCommonApi tenantApi) {
return new TenantFrameworkServiceImpl(tenantApi);
}
// 注册租户忽略AOP切面
@Bean
public TenantIgnoreAspect tenantIgnoreAspect() {
return new TenantIgnoreAspect();
}
// 注册MyBatis-Plus租户拦截器
@Bean
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
MybatisPlusInterceptor interceptor) {
TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
// 添加到interceptor中,需要加在首个,主要是为了在分页插件前面
MyBatisUtils.addInterceptor(interceptor, inner, 0);
return inner;
}
// 注册租户上下文Web过滤器
@Bean
public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new TenantContextWebFilter());
registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
return registrationBean;
}
// 注册租户安全过滤器
@Bean
public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties,
WebProperties webProperties,
GlobalExceptionHandler globalExceptionHandler,
TenantFrameworkService tenantFrameworkService) {
FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new TenantSecurityWebFilter(webProperties, tenantProperties, getTenantIgnoreUrls(),
globalExceptionHandler, tenantFrameworkService));
registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
return registrationBean;
}
// 其他Bean注册...
}
通过@ConditionalOnProperty注解,我们可以通过配置文件灵活地开启或关闭多租户功能。默认情况下,多租户功能是开启的。
4. 安全控制:TenantSecurityWebFilter
TenantSecurityWebFilter负责在Web请求处理过程中进行租户相关的安全校验:
public class TenantSecurityWebFilter extends ApiRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
Long tenantId = TenantContextHolder.getTenantId();
// 1. 登录用户,校验是否有权限访问该租户,避免越权问题
LoginUser user = SecurityFrameworkUtils.getLoginUser();
if (user != null) {
// 如果获取不到租户编号,则尝试使用登录用户的租户编号
if (tenantId == null) {
tenantId = user.getTenantId();
TenantContextHolder.setTenantId(tenantId);
// 如果传递了租户编号,则进行比对,避免越权
} else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) {
log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
user.getTenantId(), user.getId(), user.getUserType(),
TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
"您无权访问该租户的数据"));
return;
}
}
// 如果非允许忽略租户的URL,则校验租户是否合法
if (!isIgnoreUrl(request)) {
// 2. 如果请求未带租户的编号,不允许访问
if (tenantId == null) {
log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod());
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),
"请求的租户标识未传递,请进行排查"));
return;
}
// 3. 校验租户是否合法,例如是否被禁用、是否到期
try {
tenantFrameworkService.validTenant(tenantId);
} catch (Throwable ex) {
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
ServletUtils.writeJSON(response, result);
return;
}
} else {
// 如果是允许忽略租户的URL,若未传递租户编号,则默认忽略租户编号
if (tenantId == null) {
TenantContextHolder.setIgnore(true);
}
}
// 继续过滤
chain.doFilter(request, response);
}
}
该过滤器的主要职责包括:
- 处理租户ID的传递与验证,防止租户间的越权访问
- 校验租户状态,确保只有合法租户能够访问系统
- 实现URL级别的租户过滤忽略规则
5. 配置属性:TenantProperties
TenantProperties类定义了多租户功能的可配置属性:
@ConfigurationProperties(prefix = "yudao.tenant")
@Data
public class TenantProperties {
/**
* 租户是否开启
*/
private static final Boolean ENABLE_DEFAULT = true;
/**
* 是否开启
*/
private Boolean enable = ENABLE_DEFAULT;
/**
* 需要忽略多租户的请求
*/
private Set<String> ignoreUrls = new HashSet<>();
/**
* 需要忽略跨租户访问的请求
*/
private Set<String> ignoreVisitUrls = Collections.emptySet();
/**
* 需要忽略多租户的表
*/
private Set<String> ignoreTables = Collections.emptySet();
/**
* 需要忽略多租户的Spring Cache缓存
*/
private Set<String> ignoreCaches = Collections.emptySet();
}
通过这些配置,我们可以灵活地控制多租户功能的开关,以及设置需要忽略租户过滤的URL、表名和缓存等。
多租户功能实战
配置多租户属性
在application.yml中添加如下配置,即可开启并定制多租户功能:
yudao:
tenant:
enable: true # 是否开启多租户功能,默认为true
ignore-urls: # 需要忽略多租户的URL
- /api/auth/login
- /api/auth/logout
- /api/captcha/image
ignore-tables: # 需要忽略多租户的表
- sys_config
- sys_dict_data
ignore-caches: # 需要忽略多租户的缓存
- menu
- dict
实体类集成多租户
要让实体类支持多租户,只需让其继承TenantBaseDO类即可:
@Data
@TableName("sys_user")
public class SysUser extends TenantBaseDO {
// 字段定义...
}
// TenantBaseDO类定义
public abstract class TenantBaseDO extends BaseDO {
/**
* 租户ID
*/
@TableField(value = "tenant_id", fill = FieldFill.INSERT)
private Long tenantId;
// getter和setter...
}
TenantBaseDO类中定义了tenantId字段,并使用MyBatis-Plus的@TableField注解指定了自动填充策略。这样,在插入数据时,租户ID会自动填充到tenantId字段中。
忽略多租户过滤
在某些场景下,我们可能需要忽略多租户过滤,例如查询系统级别的公共数据。RuoYi-Vue-Pro提供了多种忽略方式:
1. 注解方式
在Mapper接口方法上添加@TenantIgnore注解:
public interface SysDictDataMapper extends BaseMapper<SysDictData> {
@TenantIgnore // 忽略该方法的多租户过滤
List<SysDictData> selectByDictType(String dictType);
}
在Service类或方法上添加@TenantIgnore注解:
@Service
@TenantIgnore // 忽略该Service所有方法的多租户过滤
public class SysDictDataServiceImpl implements SysDictDataService {
// 实现...
}
// 或
@Service
public class SysDictDataServiceImpl implements SysDictDataService {
@TenantIgnore // 忽略该方法的多租户过滤
@Override
public List<SysDictData> getByDictType(String dictType) {
// 实现...
}
}
在Controller类或方法上添加@TenantIgnore注解:
@RestController
@RequestMapping("/api/dict")
@TenantIgnore // 忽略该Controller所有接口的多租户过滤
public class SysDictController {
// 接口定义...
}
// 或
@RestController
@RequestMapping("/api/dict")
public class SysDictController {
@TenantIgnore // 忽略该接口的多租户过滤
@GetMapping("/type/{dictType}")
public CommonResult<List<SysDictDataRespVO>> getDictDataList(@PathVariable String dictType) {
// 实现...
}
}
在实体类上添加@TenantIgnore注解,忽略整个实体类的多租户过滤:
@Data
@TableName("sys_dict_data")
@TenantIgnore // 忽略该实体类的多租户过滤
public class SysDictData extends BaseDO {
// 字段定义...
}
2. 编程方式
通过TenantContextHolder手动设置忽略多租户:
TenantContextHolder.setIgnore(true);
try {
// 执行不需要多租户过滤的操作
return sysDictDataMapper.selectByDictType(dictType);
} finally {
// 恢复默认设置
TenantContextHolder.setIgnore(false);
}
多租户缓存处理
在多租户环境下,缓存的处理需要特别注意,避免不同租户的数据相互干扰。RuoYi-Vue-Pro提供了TenantRedisCacheManager来处理多租户缓存:
@Bean
@Primary // 引入租户时,tenantRedisCacheManager为主Bean
public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate,
RedisCacheConfiguration redisCacheConfiguration,
YudaoCacheProperties yudaoCacheProperties,
TenantProperties tenantProperties) {
// 创建RedisCacheWriter对象
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
// 创建TenantRedisCacheManager对象
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches());
}
TenantRedisCacheManager会自动在缓存Key中添加租户ID前缀,确保不同租户的缓存数据相互隔离。例如,租户1和租户2的用户缓存Key会分别变成"tenant:1:user:100"和"tenant:2:user:100"。
多租户权限控制
RuoYi-Vue-Pro提供了完善的多租户权限控制机制,通过TenantSecurityWebFilter实现:
// 登录用户的租户权限校验
LoginUser user = SecurityFrameworkUtils.getLoginUser();
if (user != null) {
// 如果获取不到租户编号,则尝试使用登录用户的租户编号
if (tenantId == null) {
tenantId = user.getTenantId();
TenantContextHolder.setTenantId(tenantId);
// 如果传递了租户编号,则进行比对,避免越权
} else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) {
log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
user.getTenantId(), user.getId(), user.getUserType(),
TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
"您无权访问该租户的数据"));
return;
}
}
这段代码确保了用户只能访问自己租户的数据,防止了租户间的越权访问。
多租户高级特性
动态数据源路由
在一些复杂场景下,可能需要为不同的租户配置不同的数据源。RuoYi-Vue-Pro支持基于租户的动态数据源路由:
public class TenantDynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContextHolder.getTenantId();
}
}
通过实现AbstractRoutingDataSource类,并重写determineCurrentLookupKey方法,我们可以根据当前租户ID动态选择数据源。
租户定制化配置
RuoYi-Vue-Pro支持为不同租户提供定制化的配置:
@Service
public class TenantConfigServiceImpl implements TenantConfigService {
@Autowired
private SysConfigMapper sysConfigMapper;
@Override
public String getConfigValue(String configKey) {
Long tenantId = TenantContextHolder.getTenantId();
// 先查询租户自定义配置
SysConfig config = sysConfigMapper.selectByTenantIdAndKey(tenantId, configKey);
if (config != null) {
return config.getConfigValue();
}
// 如果租户没有自定义配置,则查询系统默认配置
return sysConfigMapper.selectByKey(configKey);
}
}
通过这种方式,每个租户可以拥有自己的定制化配置,同时继承系统的默认配置。
结语
RuoYi-Vue-Pro的多租户架构设计充分考虑了企业级应用的实际需求,通过灵活的配置和完善的功能,为SaaS应用开发提供了强有力的支持。本文从核心原理到实际应用,详细介绍了RuoYi-Vue-Pro多租户功能的实现细节和使用方法。
多租户架构是SaaS应用的核心技术之一,掌握它不仅能够帮助我们更好地理解RuoYi-Vue-Pro的设计思想,也能为我们构建自己的SaaS应用提供宝贵的参考。希望本文能够对您有所帮助,欢迎在评论区交流讨论。
最后,如果您觉得本文对您有帮助,请不要吝啬您的Star,您的支持是我们持续开源的动力!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



