面试官: "如果要设计一个支持上千家企业共用的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<String, Object> 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<Object, Object> 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 面试加分项
-
业界实践参考:
- Salesforce:元数据驱动的多租户架构
- AWS:基于IAM的跨账户资源管理
- 阿里云:数据库代理实现自动路由
-
高级特性:
- 跨租户数据共享:安全的跨租户数据访问机制
- 租户迁移工具:在线迁移租户数据到独立数据库
- 性能隔离:基于QoS的资源分配保障
-
监控运维:
- 租户级监控:每个租户的独立监控视图
- 容量规划:基于租户增长预测的扩容策略
- 成本分摊:精确计算每个租户的资源成本
六、总结与互动
多租户设计哲学:隔离是基础,共享是价值,扩展是能力——三位一体构建优秀SaaS架构
记住这个架构公式:租户识别 + 数据隔离 + 资源管理 + 监控运维 = 完美多租户系统
思考题:在你的业务场景中,会选择哪种多租户隔离方案?为什么?欢迎在评论区分享实战经验!
关注我,每天搞懂一道面试题,助你轻松拿下Offer!