1. 动态数据源的核心原理与实现
在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
2. 三种租户隔离策略对比
2.1 数据库级隔离
这是最彻底的隔离方案,每个租户使用独立的数据库实例。在金融、医疗等对数据隔离要求严格的场景特别适用。实现时需要关注几个要点:
- 连接池管理:建议为每个租户配置独立的Druid连接池参数
- 故障隔离:某个租户数据库故障不应影响其他租户
- 资源分配:通过
TenantDatabaseConfig类控制每个租户的连接数上限
java复制@Bean
@ConditionalOnProperty(name = "tenant.isolation.type", havingValue = "database")
public DataSource dynamicDataSource() {
return DruidDataSourceBuilder.create().build();
}
2.2 Schema级隔离
适合中等规模的SaaS应用,所有租户共享数据库实例但使用不同的Schema。我们在教育SaaS系统中采用这种方案,相比数据库级隔离节省了约40%的运维成本。关键实现技巧包括:
- SQL重写:通过拦截器自动添加Schema前缀
- 缓存优化:共享数据库时需要特别注意查询缓存隔离
- 跨Schema查询:通过
IGNORE_TENANT_TABLES配置系统表白名单
sql复制-- 原始SQL
SELECT * FROM user WHERE id = 1;
-- 重写后SQL
SELECT * FROM tenant_001.user WHERE id = 1;
2.3 表级隔离
最适合快速迭代的初创项目,所有租户数据存储在相同的表结构中,通过tenant_id字段区分。这种方案的最大优势是架构简单,但需要特别注意:
- 索引优化:所有查询必须包含tenant_id条件
- 数据安全:确保没有遗漏tenant_id过滤的SQL
- 批量操作:需要特殊处理跨租户的数据迁移
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());
}
}
3. 实战中的性能优化技巧
3.1 连接池调优
在多租户环境下,连接池配置直接影响系统稳定性。我们通过压测发现这些经验值最合理:
| 参数 | 单租户建议值 | 多租户建议值 |
|---|---|---|
| initialSize | 5 | 3 |
| maxActive | 20 | 15 |
| minIdle | 5 | 2 |
| maxWait(ms) | 60000 | 30000 |
特别提醒:数据库级隔离时,总连接数=租户数×maxActive,要确保数据库能承受这个连接压力。
3.2 二级缓存处理
当使用Schema或表级隔离时,需要特别注意MyBatis二级缓存的问题。我们推荐两种解决方案:
- 租户隔离缓存:改造CacheKey生成逻辑,自动包含tenantId
java复制public class TenantCacheKey extends CacheKey {
private final String tenantId;
// 重写equals和hashCode方法
}
- 禁用跨租户缓存:在配置中强制关闭可能引起问题的缓存
yaml复制mybatis-plus:
configuration:
cache-enabled: false
3.3 批量操作优化
处理批量数据时,传统的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);
}
4. 常见问题排查指南
4.1 上下文丢失问题
在异步线程或RPC调用中经常出现租户上下文丢失,我们通过以下方式解决:
- 线程池装饰器:封装任务执行逻辑
java复制public class TenantThreadPoolExecutor extends ThreadPoolExecutor {
@Override
public void execute(Runnable command) {
super.execute(TenantContext.wrap(command));
}
}
- Feign拦截器:自动传播租户ID
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);
}
}
}
4.2 慢SQL监控
多租户系统特别需要关注SQL性能,我们建议:
- 启用Druid的监控功能
yaml复制spring:
datasource:
druid:
stat-view-servlet:
enabled: true
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
- 对租户ID字段建立组合索引
sql复制CREATE INDEX idx_tenant_user ON user(tenant_id, id);
4.3 数据迁移方案
当租户需要迁移数据时,我们开发了专用的数据导出工具,关键逻辑包括:
- 使用游标分批读取数据
- 压缩传输节省带宽
- 数据校验机制确保一致性
java复制public void exportTenantData(String tenantId, OutputStream out) {
TenantHelper.execute(tenantId, () -> {
try (ZipOutputStream zipOut = new ZipOutputStream(out)) {
// 导出用户数据
exportTableData("sys_user", zipOut);
// 导出业务数据...
}
});
}
在实际项目中,动态数据源的稳定性直接影响整个系统的可靠性。建议在预发布环境进行充分的压力测试,特别关注连接泄漏和内存增长情况。我们团队曾经因为未及时关闭连接池,导致生产环境数据库连接耗尽,这个教训值得大家引以为戒。
