面试官:"如果要设计一个支持上千家企业共用的SaaS系统,你会怎么保证数据隔离和系统扩展性?"
一、开篇:理解多租户本质
想象一下:你要设计一个CRM系统,同时服务小米、华为、腾讯等上千家企业,每家的数据必须完全隔离,但代码只需一套
多租户核心挑战:
数据隔离:确保A公司绝对看不到B公司数据 性能保障:千家企业共享资源时的性能稳定性 扩展能力:支持从10家到10万家客户的平滑扩展 定制化需求:不同企业的个性化需求支持
这就像建造五星级酒店,每个房间完全独立,但共享基础设施
二、核心架构设计
2.1 多租户数据隔离方案
三种主流隔离方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 独立数据库 | 隔离性最好,性能最优 | 成本高,维护复杂 | 大型企业客户 |
| 共享数据库独立schema | 较好隔离性,中等成本 | 跨schema查询复杂 | 中型企业客户 |
| 共享数据库共享schema | 成本最低,扩展性好 | 隔离性依赖应用层 | SaaS标准产品 |
推荐方案:基于租户ID的共享schema设计
publicclassTenantContext{
privatestaticfinalThreadLocal<String> currentTenant =newThreadLocal<>();
publicstaticvoidsetTenantId(String tenantId){
currentTenant.set(tenantId);
}
publicstaticStringgetTenantId(){
returncurrentTenant.get();
}
publicstaticvoidclear(){
currentTenant.remove();
}
}
// 在Spring拦截器中自动设置租户上下文
@Component
publicclassTenantInterceptorimplementsHandlerInterceptor{
@Override
publicbooleanpreHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler){
String tenantId = request.getHeader("X-Tenant-ID");
if(tenantId !=null) {
TenantContext.setTenantId(tenantId);
}
returntrue;
}
}
2.2 数据库层面多租户实现
MyBatis多租户SQL拦截器:
@Intercepts({@Signature(type = StatementHandler.class,
method="prepare",
args = {Connection.class,Integer.class})})
publicclassTenantInterceptorimplementsInterceptor{
@Override
publicObjectintercept(Invocation invocation)throwsThrowable{
StatementHandler handler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(handler);
MappedStatement mappedStatement = (MappedStatement)
metaObject.getValue("delegate.mappedStatement");
// 获取当前租户ID
String tenantId = TenantContext.getTenantId();
if(tenantId !=null&& isMultiTenantTable(mappedStatement)) {
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
String originalSql = boundSql.getSql();
String modifiedSql = addTenantCondition(originalSql, tenantId);
metaObject.setValue("delegate.boundSql.sql", modifiedSql);
}
returninvocation.proceed();
}
privateStringaddTenantCondition(String sql, String tenantId){
// 解析SQL并添加租户条件
returnsql.replace("WHERE","WHERE tenant_id = '"+ tenantId +"' AND ");
}
}
2.3 多级缓存架构
Redis多租户缓存设计:
@Service
publicclassTenantAwareCacheManager{
@Autowired
privateRedisTemplate<String, Object> redisTemplate;
publicvoidput(String key, Object value, Duration ttl){
String tenantKey = buildTenantKey(key);
redisTemplate.opsForValue().set(tenantKey, value, ttl);
}
publicObjectget(String key){
String tenantKey = buildTenantKey(key);
returnredisTemplate.opsForValue().get(tenantKey);
}
privateStringbuildTenantKey(String key){
String tenantId = TenantContext.getTenantId();
return"tenant:"+ tenantId +":"+ key;
}
}
// 缓存配置
@Configuration
@EnableCaching
publicclassCacheConfigextendsCachingConfigurerSupport{
@Bean
publicCacheManagercacheManager(RedisConnectionFactory factory){
returnRedisCacheManager.builder(factory)
.cacheDefaults(defaultCacheConfig())
.withInitialCacheConfigurations(initCacheConfigs())
.build();
}
privateRedisCacheConfigurationdefaultCacheConfig(){
returnRedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(2))
.disableCachingNullValues();
}
}
三、关键技术实现
3.1 租户识别与路由
Spring Cloud Gateway租户路由:
@Component
publicclassTenantRouteFilterimplementsGlobalFilter{
@Override
publicMono<Void>filter(ServerWebExchange exchange,
GatewayFilterChain chain){
ServerHttpRequest request = exchange.getRequest();
// 从Header、Domain、JWT等多维度识别租户
String tenantId = extractTenantId(request);
if(tenantId !=null) {
// 设置租户上下文
exchange.getAttributes().put("tenantId", tenantId);
// 路由到对应服务实例
returnchain.filter(exchange.mutate()
.request(request.mutate()
.header("X-Tenant-ID", tenantId)
.build())
.build());
}
returnchain.filter(exchange);
}
privateStringextractTenantId(ServerHttpRequest request){
// 1. 从Header获取
String headerTenant = request.getHeaders().getFirst("X-Tenant-ID");
if(headerTenant !=null)returnheaderTenant;
// 2. 从域名获取
String domain = request.getURI().getHost();
String domainTenant = resolveTenantFromDomain(domain);
if(domainTenant !=null)returndomainTenant;
// 3. 从JWT Token获取
returnresolveTenantFromJWT(request);
}
}
3.2 动态数据源路由
AbstractRoutingDataSource实现多租户数据源:
publicclassTenantDataSourceRouterextendsAbstractRoutingDataSource{
@Override
protectedObjectdetermineCurrentLookupKey(){
returnTenantContext.getTenantId();
}
}
@Configuration
publicclassDataSourceConfig{
@Bean
@ConfigurationProperties(prefix ="spring.datasource.master")
publicDataSourcemasterDataSource(){
returnDataSourceBuilder.create().build();
}
@Bean
publicDataSourcedataSource(){
Map<Object, Object> targetDataSources =newHashMap<>();
targetDataSources.put("master", masterDataSource());
TenantDataSourceRouter router =newTenantDataSourceRouter();
router.setDefaultTargetDataSource(masterDataSource());
router.setTargetDataSources(targetDataSources);
router.afterPropertiesSet();
returnrouter;
}
}
3.3 权限与资源隔离
Spring Security多租户权限控制:
@Component
publicclassTenantAwarePermissionEvaluatorimplementsPermissionEvaluator{
@Override
publicbooleanhasPermission(Authentication authentication,
Object targetDomainObject,
Object permission){
// 获取当前用户租户信息
String userTenant = getTenantFromAuthentication(authentication);
String objectTenant = getTenantFromDomainObject(targetDomainObject);
// 租户不匹配直接拒绝
if(!userTenant.equals(objectTenant)) {
returnfalse;
}
// 进一步检查具体权限
returncheckBusinessPermission(authentication, targetDomainObject, permission);
}
@Override
publicbooleanhasPermission(Authentication authentication,
Serializable targetId,
String targetType,
Object permission){
// 基于ID的权限检查
Object domainObject = loadDomainObject(targetType, targetId);
returnhasPermission(authentication, domainObject, permission);
}
}
四、完整架构示例
4.1 系统架构图
[客户端] -> [API网关] -> [租户识别] -> [服务路由]
| | | |
v v v v
[认证中心] <- [负载均衡] <- [租户上下文] <- [业务服务]
| | | |
v v v v
[数据存储] -> [缓存集群] -> [消息队列] -> [文件存储]
4.2 多租户配置管理
# application-multitenant.yml
multitenant:
strategy:DATABASE_PER_TENANT# 隔离策略
default-tenant:default
tenant-resolution:
strategies:HEADER,DOMAIN,JWT
header-name:X-Tenant-ID
domain-pattern:(.+)\\.company\\.com
database:
pool-size:10
max-connections:100
connection-timeout:3000
cache:
enabled:true
timeout:3600
max-size:10000
五、面试陷阱与加分项
5.1 常见陷阱问题
问题1:"如何防止租户A通过修改ID访问租户B的数据?"
参考答案:
应用层强制过滤:所有查询自动添加租户条件 数据库视图:为每个租户创建专用视图 权限校验:每次数据访问验证租户权限
问题2:"某个租户的慢查询影响整个系统怎么办?"
参考答案:
资源隔离:使用数据库资源组或连接池隔离 限流降级:对问题租户进行限流 监控告警:实时监控每个租户的资源使用
问题3:"如何支持租户自定义字段?"
参考答案:
扩展字段表:使用JSON字段或扩展表结构 元数据驱动:动态生成表结构或查询 NoSQL补充:用MongoDB等存储自定义数据
5.2 面试加分项
业界实践参考:
Salesforce:元数据驱动的多租户架构 AWS:基于IAM的跨账户资源管理 阿里云:数据库代理实现自动路由
高级特性:
跨租户数据共享:安全的跨租户数据访问机制 租户迁移工具:在线迁移租户数据到独立数据库 性能隔离:基于QoS的资源分配保障
监控运维:
租户级监控:每个租户的独立监控视图 容量规划:基于租户增长预测的扩容策略 成本分摊:精确计算每个租户的资源成本
六、总结与互动
多租户设计哲学:隔离是基础,共享是价值,扩展是能力——三位一体构建优秀SaaS架构
记住这个架构公式:租户识别 + 数据隔离 + 资源管理 + 监控运维= 完美多租户系统
思考题:在你的业务场景中,会选择哪种多租户隔离方案?为什么?欢迎在评论区分享实战经验!
关注我,每天搞懂一道面试题,助你轻松拿下Offer!