每天一道面试题之架构篇|多租户SaaS后台系统架构设计

86 阅读5分钟

面试官: "如果要设计一个支持上千家企业共用的SaaS系统,你会怎么保证数据隔离和系统扩展性?"

一、开篇:理解多租户本质

想象一下:你要设计一个CRM系统,同时服务小米、华为、腾讯等上千家企业,每家的数据必须完全隔离,但代码只需一套

多租户核心挑战

  • 数据隔离:确保A公司绝对看不到B公司数据
  • 性能保障:千家企业共享资源时的性能稳定性
  • 扩展能力:支持从10家到10万家客户的平滑扩展
  • 定制化需求:不同企业的个性化需求支持

这就像建造五星级酒店,每个房间完全独立,但共享基础设施

二、核心架构设计

2.1 多租户数据隔离方案

三种主流隔离方案对比

方案优点缺点适用场景
独立数据库隔离性最好,性能最优成本高,维护复杂大型企业客户
共享数据库独立schema较好隔离性,中等成本跨schema查询复杂中型企业客户
共享数据库共享schema成本最低,扩展性好隔离性依赖应用层SaaS标准产品

推荐方案:基于租户ID的共享schema设计

public class TenantContext {
    private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
    
    public static void setTenantId(String tenantId) {
        currentTenant.set(tenantId);
    }
    
    public static String getTenantId() {
        return currentTenant.get();
    }
    
    public static void clear() {
        currentTenant.remove();
    }
}

// 在Spring拦截器中自动设置租户上下文
@Component
public class TenantInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) {
        String tenantId = request.getHeader("X-Tenant-ID");
        if (tenantId != null) {
            TenantContext.setTenantId(tenantId);
        }
        return true;
    }
}

2.2 数据库层面多租户实现

MyBatis多租户SQL拦截器

@Intercepts({@Signature(type = StatementHandler.class, 
                      method = "prepare", 
                      args = {Connection.class, Integer.class})})
public class TenantInterceptor implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        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);
        }
        
        return invocation.proceed();
    }
    
    private String addTenantCondition(String sql, String tenantId) {
        // 解析SQL并添加租户条件
        return sql.replace("WHERE", "WHERE tenant_id = '" + tenantId + "' AND ");
    }
}

2.3 多级缓存架构

Redis多租户缓存设计

@Service
public class TenantAwareCacheManager {
    
    @Autowired
    private RedisTemplate<StringObject> redisTemplate;
    
    public void put(String key, Object value, Duration ttl) {
        String tenantKey = buildTenantKey(key);
        redisTemplate.opsForValue().set(tenantKey, value, ttl);
    }
    
    public Object get(String key) {
        String tenantKey = buildTenantKey(key);
        return redisTemplate.opsForValue().get(tenantKey);
    }
    
    private String buildTenantKey(String key) {
        String tenantId = TenantContext.getTenantId();
        return "tenant:" + tenantId + ":" + key;
    }
}

// 缓存配置
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        return RedisCacheManager.builder(factory)
            .cacheDefaults(defaultCacheConfig())
            .withInitialCacheConfigurations(initCacheConfigs())
            .build();
    }
    
    private RedisCacheConfiguration defaultCacheConfig() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(2))
            .disableCachingNullValues();
    }
}

三、关键技术实现

3.1 租户识别与路由

Spring Cloud Gateway租户路由

@Component
public class TenantRouteFilter implements GlobalFilter {
    
    @Override
    public Mono<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);
            
            // 路由到对应服务实例
            return chain.filter(exchange.mutate()
                .request(request.mutate()
                    .header("X-Tenant-ID", tenantId)
                    .build())
                .build());
        }
        
        return chain.filter(exchange);
    }
    
    private String extractTenantId(ServerHttpRequest request) {
        // 1. 从Header获取
        String headerTenant = request.getHeaders().getFirst("X-Tenant-ID");
        if (headerTenant != null) return headerTenant;
        
        // 2. 从域名获取
        String domain = request.getURI().getHost();
        String domainTenant = resolveTenantFromDomain(domain);
        if (domainTenant != null) return domainTenant;
        
        // 3. 从JWT Token获取
        return resolveTenantFromJWT(request);
    }
}

3.2 动态数据源路由

AbstractRoutingDataSource实现多租户数据源

public class TenantDataSourceRouter extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContext.getTenantId();
    }
}

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public DataSource dataSource() {
        Map<ObjectObject> targetDataSources = new HashMap<>();
        targetDataSources.put("master"masterDataSource());
        
        TenantDataSourceRouter router = new TenantDataSourceRouter();
        router.setDefaultTargetDataSource(masterDataSource());
        router.setTargetDataSources(targetDataSources);
        router.afterPropertiesSet();
        
        return router;
    }
}

3.3 权限与资源隔离

Spring Security多租户权限控制

@Component
public class TenantAwarePermissionEvaluator implements PermissionEvaluator {
    
    @Override
    public boolean hasPermission(Authentication authentication, 
                              Object targetDomainObject, 
                              Object permission) {
        // 获取当前用户租户信息
        String userTenant = getTenantFromAuthentication(authentication);
        String objectTenant = getTenantFromDomainObject(targetDomainObject);
        
        // 租户不匹配直接拒绝
        if (!userTenant.equals(objectTenant)) {
            return false;
        }
        
        // 进一步检查具体权限
        return checkBusinessPermission(authentication, targetDomainObject, permission);
    }
    
    @Override
    public boolean hasPermission(Authentication authentication, 
                              Serializable targetId, 
                              String targetType, 
                              Object permission) {
        // 基于ID的权限检查
        Object domainObject = loadDomainObject(targetType, targetId);
        return hasPermission(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 面试加分项

  1. 业界实践参考

    • Salesforce:元数据驱动的多租户架构
    • AWS:基于IAM的跨账户资源管理
    • 阿里云:数据库代理实现自动路由
  2. 高级特性

    • 跨租户数据共享:安全的跨租户数据访问机制
    • 租户迁移工具:在线迁移租户数据到独立数据库
    • 性能隔离:基于QoS的资源分配保障
  3. 监控运维

    • 租户级监控:每个租户的独立监控视图
    • 容量规划:基于租户增长预测的扩容策略
    • 成本分摊:精确计算每个租户的资源成本

六、总结与互动

多租户设计哲学隔离是基础,共享是价值,扩展是能力——三位一体构建优秀SaaS架构

记住这个架构公式:租户识别 + 数据隔离 + 资源管理 + 监控运维 = 完美多租户系统


思考题:在你的业务场景中,会选择哪种多租户隔离方案?为什么?欢迎在评论区分享实战经验!

关注我,每天搞懂一道面试题,助你轻松拿下Offer!