若依框架中实现多租户架构设计

2025-06发布2次浏览

若依框架(RuoYi)是一个基于Spring Boot和Spring Cloud的快速开发平台,以其简洁、高效和易于扩展的特点而受到广泛欢迎。在企业级应用中,多租户架构是一种常见的设计模式,用于在同一系统中为不同客户提供独立的数据隔离或共享能力。本文将深入探讨如何在若依框架中实现多租户架构设计。


一、多租户架构的基本概念

多租户架构是指在一个软件系统中支持多个租户(Tenant),每个租户可以独立使用系统功能,同时保证数据的安全性和隔离性。根据数据隔离的程度,多租户架构通常分为以下三种模式:

  1. 独立数据库模式:每个租户拥有一个独立的数据库实例。
  2. 共享数据库+独立Schema模式:所有租户共享一个数据库,但每个租户有自己的Schema。
  3. 共享数据库+共享Schema模式:所有租户共享同一个数据库和Schema,通过租户ID字段区分数据。

在实际项目中,选择哪种模式取决于业务需求、性能要求和成本预算。


二、在若依框架中实现多租户架构的设计思路

1. 数据库设计

在共享数据库+共享Schema模式下,我们可以通过在表中添加tenant_id字段来标识数据所属的租户。例如:

CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    tenant_id VARCHAR(50) NOT NULL, -- 租户ID
    username VARCHAR(50),
    password VARCHAR(100),
    email VARCHAR(100)
);

2. 动态数据源配置

为了支持多租户,若依框架需要动态切换数据源。可以通过Spring的AbstractRoutingDataSource实现动态数据源路由。

  • 创建自定义数据源类:
public class MultiTenantDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContext.getTenantId(); // 获取当前线程的租户ID
    }
}
  • 配置多数据源:
spring:
  datasource:
    master:
      url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
      username: root
      password: root
    tenant1:
      url: jdbc:mysql://localhost:3306/tenant1_db?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
      username: root
      password: root
  • 初始化数据源:
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource multiTenantDataSource(@Autowired Map<Object, Object> dataSourceMap) {
        MultiTenantDataSource dataSource = new MultiTenantDataSource();
        dataSource.setDefaultTargetDataSource(dataSourceMap.get("master")); // 默认数据源
        dataSource.setTargetDataSources(dataSourceMap); // 设置所有数据源
        return dataSource;
    }
}

3. 租户上下文管理

为了确保每个请求都能正确关联到对应的租户,需要引入租户上下文管理机制。

  • 创建租户上下文工具类:
public class TenantContext {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setTenantId(String tenantId) {
        contextHolder.set(tenantId);
    }

    public static String getTenantId() {
        return contextHolder.get();
    }

    public static void clear() {
        contextHolder.remove();
    }
}
  • 在拦截器中设置租户ID:
@Component
public class TenantInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String tenantId = request.getHeader("X-Tenant-Id"); // 从请求头获取租户ID
        if (tenantId != null) {
            TenantContext.setTenantId(tenantId);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        TenantContext.clear(); // 请求完成后清理租户上下文
    }
}

4. SQL拦截与动态拼接

为了在查询时自动添加tenant_id条件,可以使用MyBatis的拦截器。

  • 创建SQL拦截器:
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class TenantInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];

        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        String sql = boundSql.getSql();

        // 动态拼接tenant_id条件
        String tenantId = TenantContext.getTenantId();
        if (tenantId != null && !sql.contains("tenant_id")) {
            sql += " AND tenant_id = '" + tenantId + "'";
        }

        BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings(), parameter);
        MappedStatement newMappedStatement = copyFromMappedStatement(mappedStatement, newBoundSql);
        invocation.getArgs()[0] = newMappedStatement;

        return invocation.proceed();
    }

    private MappedStatement copyFromMappedStatement(MappedStatement ms, BoundSql newBoundSql) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), ms.getSqlSource(), ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            builder.keyProperty(String.join(",", ms.getKeyProperties()));
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }
}

三、多租户架构的优势与挑战

优势

  1. 资源共享:多个租户可以共享同一套代码和基础设施,降低维护成本。
  2. 灵活性:可以根据需求灵活选择不同的隔离模式。
  3. 可扩展性:支持动态增加租户,适应业务增长。

挑战

  1. 性能问题:在高并发场景下,数据隔离可能带来性能瓶颈。
  2. 安全性:需要严格控制数据访问权限,防止租户间数据泄露。
  3. 复杂性:多租户架构增加了系统的复杂性,开发和维护成本较高。

四、总结

通过以上步骤,我们可以在若依框架中实现多租户架构。关键在于合理设计数据库结构、动态管理数据源以及确保数据隔离的安全性。根据实际需求选择合适的多租户模式,并结合Spring的AOP、拦截器和MyBatis的插件机制,可以有效提升系统的灵活性和扩展性。