1. 项目概述
最近在重构一个SaaS平台时,遇到了多租户系统架构设计的挑战。作为技术负责人,我需要在不增加过多运维成本的前提下,确保不同租户数据的完全隔离。经过多次方案对比和实际验证,最终采用"共享数据库+独立Schema"的方案成功落地。本文将分享这套经过实战检验的SpringBoot多租户架构设计方案。
2. 多租户隔离方案选型
2.1 主流隔离方案对比
在SaaS系统中,常见的多租户隔离方案主要有三种:
- 独立数据库方案:每个租户使用独立的数据库实例
- 共享数据库+独立Schema:所有租户共享数据库实例,但每个租户有独立的Schema
- 共享数据库+共享Schema:所有租户共享数据库和Schema,通过租户ID字段区分数据
2.2 方案选型决策过程
经过深入分析业务需求和技术指标,我们最终选择了"共享数据库+独立Schema"方案,主要基于以下考虑:
- 租户规模:预计500+租户,单租户最大用户量1000+
- 数据量级:中等规模数据量,单租户数据量在GB级别
- 运维成本:团队规模有限,需要降低运维复杂度
- 安全要求:部分业务数据涉及客户隐私,需要较高隔离级别
2.3 方案优势分析
相比其他方案,"共享数据库+独立Schema"具有以下优势:
- 隔离性:Schema级别的隔离,避免数据泄露风险
- 运维成本:单数据库管理,降低备份、监控复杂度
- 扩展性:支持按需迁移特定租户到新数据库
- 性能:避免单表数据量过大导致的性能问题
3. 整体架构设计
3.1 架构分层
系统采用五层架构设计:
- 接入层:处理租户标识传递
- 网关层:租户认证和路由转发
- 业务层:租户上下文管理
- 数据访问层:动态Schema切换
- 数据存储层:共享数据库+独立Schema
3.2 核心组件交互
code复制[前端] → [网关] → [业务服务] → [数据库]
↑ ↑ ↑
租户标识 租户校验 动态Schema
4. 关键技术实现
4.1 租户标识传递
支持两种标识传递方式:
- URL路径:/tenant1/api/user/list
- 请求头:X-Tenant-Id: tenant1
推荐使用URL路径方式,便于API管理和调试。
4.2 网关层实现
网关核心职责:
- 解析租户标识
- 校验租户合法性
- 透传租户标识
关键代码实现:
java复制@Component
public class TenantGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 解析租户标识
String tenantId = extractTenantId(exchange.getRequest());
// 2. 校验租户
if(!tenantService.isValidTenant(tenantId)) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
// 3. 透传标识
ServerHttpRequest newRequest = exchange.getRequest().mutate()
.header("X-Tenant-Id", tenantId)
.build();
return chain.filter(exchange.mutate().request(newRequest).build());
}
}
4.3 租户上下文管理
基于ThreadLocal实现租户上下文:
java复制public class TenantContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
CONTEXT.set(tenantId);
}
public static String getTenantId() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
4.4 动态Schema切换
通过MyBatis拦截器实现SQL重写:
java复制public class TenantSchemaInterceptor implements InnerInterceptor {
@Override
public void beforePrepare(StatementHandler sh, Connection connection) {
String tenantId = TenantContextHolder.getTenantId();
String sql = getOriginalSql(sh);
// 重写SQL添加Schema前缀
String newSql = rewriteSqlWithSchema(sql, tenantId);
setNewSql(sh, newSql);
}
}
5. 关键问题解决方案
5.1 异步场景上下文传递
使用TaskDecorator解决异步线程上下文丢失:
java复制@Component
public class TenantTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
String tenantId = TenantContextHolder.getTenantId();
return () -> {
try {
TenantContextHolder.setTenantId(tenantId);
runnable.run();
} finally {
TenantContextHolder.clear();
}
};
}
}
5.2 跨服务调用标识透传
Feign拦截器实现租户标识透传:
java复制@Component
public class FeignTenantInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String tenantId = TenantContextHolder.getTenantId();
if (StringUtils.isNotEmpty(tenantId)) {
template.header("X-Tenant-Id", tenantId);
}
}
}
5.3 租户Schema自动初始化
租户注册时自动创建Schema:
java复制@Service
public class TenantServiceImpl implements TenantService {
public void registerTenant(TenantDTO dto) {
// 创建Schema
jdbcTemplate.execute("CREATE SCHEMA " + dto.getTenantId());
// 初始化表结构
flyway.setSchemas(dto.getTenantId());
flyway.migrate();
// 保存租户信息
tenantRepository.save(dto);
}
}
6. 性能优化建议
- 缓存策略:采用租户隔离的缓存键设计
- 连接池优化:合理设置连接池参数
- SQL优化:避免全表扫描操作
- 索引设计:为常用查询条件添加索引
- 分库策略:热点租户单独分库
7. 安全防护措施
- SQL注入防护:使用预编译语句
- 权限控制:数据库账号最小权限原则
- 审计日志:记录关键数据访问操作
- 数据加密:敏感字段加密存储
- 定期巡检:检查跨租户访问风险
8. 监控与运维
- 租户维度监控:接口成功率、响应时间
- 资源使用监控:CPU、内存、连接数
- 告警机制:异常情况实时告警
- 备份策略:全量备份+增量备份
- 容灾方案:同城双活部署
9. 扩展性设计
- 多租户类型支持:区分不同级别的租户
- 混合部署方案:关键租户独立部署
- 动态扩容:按需调整资源配置
- 多语言支持:国际化方案设计
- 开放平台:API网关对接
10. 实践经验总结
在实际项目落地过程中,我们总结了以下几点经验:
- 隔离级别选择:根据业务需求平衡隔离性和成本
- 性能基准测试:提前进行压力测试
- 灰度发布策略:新功能先小范围验证
- 文档规范化:保持架构文档及时更新
- 团队协作:建立跨职能协作机制
这套架构方案已经在生产环境稳定运行超过6个月,支撑了500+租户的业务需求,日均处理请求量超过100万次,验证了其可靠性和扩展性。