在Ruoyi-vue-plus框架中,动态数据源是实现多租户隔离的基础设施。它的核心思想是通过运行时切换数据库连接,让每个租户访问独立的数据存储空间。我曾在电商SaaS项目中实际应用过这套机制,下面分享几个关键实现细节。
动态数据源的工作流程可以类比为酒店前台:当客人(租户)出示房卡(租户ID)时,前台会根据房卡信息分配对应的房间(数据库)。具体到代码层面,主要依赖三个核心组件:
TenantDataSourceRouter类负责维护租户ID与数据源的映射关系。它的getTenantDataSource方法会检查当前是否已创建该租户的数据源,如果没有就调用createTenantDataSource方法新建连接。java复制// 数据源路由示例代码
public DataSource getTenantDataSource(String tenantId) {
return tenantDataSources.computeIfAbsent(tenantId, this::createTenantDataSource);
}
连接拦截器:TenantDataSourceInterceptor通过MyBatis的拦截器机制,在执行SQL前动态切换数据源。这里有个容易踩的坑:一定要在finally块中清除上下文,否则会导致连接泄漏。
上下文管理器:TenantHelper使用ThreadLocal保存租户信息,确保在同一个请求链路中始终保持正确的租户上下文。我们在实际项目中发现,对于异步任务需要特别处理上下文传递问题。
配置动态数据源时,建议在application.yml中声明基础参数:
yaml复制spring:
datasource:
dynamic:
primary: master
strict: true
druid:
initial-size: 5
max-active: 20
这是最彻底的隔离方案,每个租户使用独立的数据库实例。在金融、医疗等对数据隔离要求严格的场景特别适用。实现时需要关注几个要点:
TenantDatabaseConfig类控制每个租户的连接数上限java复制@Bean
@ConditionalOnProperty(name = "tenant.isolation.type", havingValue = "database")
public DataSource dynamicDataSource() {
return DruidDataSourceBuilder.create().build();
}
适合中等规模的SaaS应用,所有租户共享数据库实例但使用不同的Schema。我们在教育SaaS系统中采用这种方案,相比数据库级隔离节省了约40%的运维成本。关键实现技巧包括:
IGNORE_TENANT_TABLES配置系统表白名单sql复制-- 原始SQL
SELECT * FROM user WHERE id = 1;
-- 重写后SQL
SELECT * FROM tenant_001.user WHERE id = 1;
最适合快速迭代的初创项目,所有租户数据存储在相同的表结构中,通过tenant_id字段区分。这种方案的最大优势是架构简单,但需要特别注意:
java复制// MyBatis-Plus租户处理器配置
public class CustomTenantLineHandler implements TenantLineHandler {
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
return IGNORE_TENANT_TABLES.contains(tableName.toLowerCase());
}
}
在多租户环境下,连接池配置直接影响系统稳定性。我们通过压测发现这些经验值最合理:
| 参数 | 单租户建议值 | 多租户建议值 |
|---|---|---|
| initialSize | 5 | 3 |
| maxActive | 20 | 15 |
| minIdle | 5 | 2 |
| maxWait(ms) | 60000 | 30000 |
特别提醒:数据库级隔离时,总连接数=租户数×maxActive,要确保数据库能承受这个连接压力。
当使用Schema或表级隔离时,需要特别注意MyBatis二级缓存的问题。我们推荐两种解决方案:
java复制public class TenantCacheKey extends CacheKey {
private final String tenantId;
// 重写equals和hashCode方法
}
yaml复制mybatis-plus:
configuration:
cache-enabled: false
处理批量数据时,传统的foreach插入性能较差。我们总结出两种优化方案:
方案一:使用rewriteBatchedStatements
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true
方案二:分批次提交
java复制// 每500条提交一次
List<User> users = getUsers();
int batchSize = 500;
for (int i = 0; i < users.size(); i += batchSize) {
List<User> subList = users.subList(i, Math.min(i + batchSize, users.size()));
userMapper.batchInsert(subList);
}
在异步线程或RPC调用中经常出现租户上下文丢失,我们通过以下方式解决:
java复制public class TenantThreadPoolExecutor extends ThreadPoolExecutor {
@Override
public void execute(Runnable command) {
super.execute(TenantContext.wrap(command));
}
}
java复制public class TenantFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String tenantId = TenantHelper.getTenantId();
if (StringUtils.hasText(tenantId)) {
template.header("X-Tenant-Id", tenantId);
}
}
}
多租户系统特别需要关注SQL性能,我们建议:
yaml复制spring:
datasource:
druid:
stat-view-servlet:
enabled: true
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
sql复制CREATE INDEX idx_tenant_user ON user(tenant_id, id);
当租户需要迁移数据时,我们开发了专用的数据导出工具,关键逻辑包括:
java复制public void exportTenantData(String tenantId, OutputStream out) {
TenantHelper.execute(tenantId, () -> {
try (ZipOutputStream zipOut = new ZipOutputStream(out)) {
// 导出用户数据
exportTableData("sys_user", zipOut);
// 导出业务数据...
}
});
}
在实际项目中,动态数据源的稳定性直接影响整个系统的可靠性。建议在预发布环境进行充分的压力测试,特别关注连接泄漏和内存增长情况。我们团队曾经因为未及时关闭连接池,导致生产环境数据库连接耗尽,这个教训值得大家引以为戒。