若依框架(RuoYi)是一个基于Spring Boot和Spring Cloud的快速开发平台,以其简洁、高效和易于扩展的特点而受到广泛欢迎。在企业级应用中,多租户架构是一种常见的设计模式,用于在同一系统中为不同客户提供独立的数据隔离或共享能力。本文将深入探讨如何在若依框架中实现多租户架构设计。
多租户架构是指在一个软件系统中支持多个租户(Tenant),每个租户可以独立使用系统功能,同时保证数据的安全性和隔离性。根据数据隔离的程度,多租户架构通常分为以下三种模式:
在实际项目中,选择哪种模式取决于业务需求、性能要求和成本预算。
在共享数据库+共享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)
);
为了支持多租户,若依框架需要动态切换数据源。可以通过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;
}
}
为了确保每个请求都能正确关联到对应的租户,需要引入租户上下文管理机制。
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();
}
}
@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(); // 请求完成后清理租户上下文
}
}
为了在查询时自动添加tenant_id
条件,可以使用MyBatis的拦截器。
@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();
}
}
通过以上步骤,我们可以在若依框架中实现多租户架构。关键在于合理设计数据库结构、动态管理数据源以及确保数据隔离的安全性。根据实际需求选择合适的多租户模式,并结合Spring的AOP、拦截器和MyBatis的插件机制,可以有效提升系统的灵活性和扩展性。