揭秘!多租户 SaaS 系统这样设计:数据库级/表级隔离 + 资源配额全攻略
在参与多个大型 SaaS 平台的架构设计之后,我逐渐发现,多租户架构的核心价值并不只是“共享”,而是“隔离 + 配额”。 一方面,我们必须确保不同租户之间的数据严格分离,以满足合规性与安全性;另一方面,还需要限制资源消耗,避免某些租户“独占”系统性能。
本文将结合实践经验,全面拆解 多租户 SaaS 系统的数据隔离方案(数据库级 / 表级 / 行级)与资源配额控制策略,并给出核心代码示例,帮助你在实际项目中快速落地。
什么是多租户架构?多租户(Multi-Tenancy)是一种典型的 SaaS 模式:
单实例运行:一套系统为多个租户(Tenant)服务。逻辑隔离:每个租户拥有独立的业务空间,但共享基础设施(数据库、存储、计算资源)。多租户架构需要解决两个关键问题:
数据隔离 —— 确保租户之间互不干扰。资源配额 —— 控制存储、API 调用、并发用户数等,防止“资源抢占”。数据隔离方案对比与实现在多租户架构下,数据隔离常见有三种方式:数据库级、表级和行级。
数据库级隔离架构思路:每个租户独立一个数据库。
复制
+-------------------+ +-------------------+ +-------------------+
| Tenant A Database | | Tenant B Database | | Tenant N Database |
+-------------------+ +-------------------+ +-------------------+
| Users Table | | Users Table | | Users Table |
| Orders Table | | Orders Table | | Orders Table |
+-------------------+ +-------------------+ +-------------------+1.2.3.4.5.6.
代码实现:动态数据源路由
复制
// 文件路径: src/main/java/com/icoderoad/tenant/TenantRoutingDataSource.java
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContextHolder.getTenantId(); // 基于 ThreadLocal 获取租户ID
}
}
// 文件路径: src/main/java/com/icoderoad/tenant/TenantContextHolder.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(); }
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.
架构思路:所有租户共享数据库,但每个租户有独立的表(加前缀)。
复制
+---------------------+
| Shared Database |
+---------------------+
| TenantA_Users_Table |
| TenantA_Orders_Table|
| TenantB_Users_Table |
| TenantB_Orders_Table|
+---------------------+1.2.3.4.5.6.7.8.
代码实现:动态表名拦截器(MyBatis)
复制
// 文件路径: src/main/java/com/icoderoad/tenant/TableNameInterceptor.java
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class TableNameInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String tenantId = TenantContextHolder.getTenantId();
String modifiedSql = boundSql.getSql().replaceAll("\\b(user|order)\\b", tenantId + "_$1");
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, modifiedSql);
return invocation.proceed();
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.
架构思路:单库单表,通过 tenant_id 字段区分租户。
复制
+-------------------+
| Shared Database |
+-------------------+
| Users Table | tenant_id + user_id
| Orders Table | tenant_id + order_id
+-------------------+1.2.3.4.5.6.
代码实现:自动注入租户 ID
复制
// 文件路径: src/main/java/com/icoderoad/tenant/TenantIdInterceptor.java
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class TenantIdInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object parameter = invocation.getArgs()[1];
String tenantId = TenantContextHolder.getTenantId();
if (parameter instanceof BaseEntity) {
((BaseEntity) parameter).setTenantId(tenantId);
}
return invocation.proceed();
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.
资源配额控制
在多租户系统中,资源配额控制防止“资源独占”。
通用资源模型复制
// 文件路径: src/main/java/com/icoderoad/quota/TenantQuota.java
@Entity
@Table(name = "tenant_quota")
public class TenantQuota {
@Id
private String tenantId;
private Long storageQuota;
private Long storageUsed;
private Long apiCallQuota;
private Long apiCallsUsed;
private Integer concurrentUserQuota;
public boolean canUseStorage(long size) {
return (storageUsed + size) <= storageQuota;
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.
复制
// 文件路径: src/main/java/com/icoderoad/quota/QuotaInterceptor.java
public class QuotaInterceptor implements HandlerInterceptor {
@Autowired private TenantQuotaService quotaService;
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
String tenantId = getTenantIdFromRequest(req);
TenantQuota quota = quotaService.getQuota(tenantId);
if (quota.getApiCallsUsed() >= quota.getApiCallQuota()) {
res.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
res.getWriter().write("API call quota exceeded");
return false;
}
quotaService.recordApiCall(tenantId);
return true;
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
复制
// 文件路径: src/main/java/com/icoderoad/quota/RedisQuotaServiceImpl.java
@Service
public class RedisQuotaServiceImpl implements QuotaService {
@Autowired private RedisTemplate<String, Long> redisTemplate;
private static final String QUOTA_KEY_PREFIX = "tenant:quota:";
private static final String USAGE_KEY_PREFIX = "tenant:usage:";
@Override
public boolean checkAndConsume(String tenantId, String resourceType, long amount) {
String quotaKey = QUOTA_KEY_PREFIX + tenantId + ":" + resourceType;
String usageKey = USAGE_KEY_PREFIX + tenantId + ":" + resourceType;
String script =
"local usage = redis.call(GET, KEYS[2]) or 0 " +
"if usage + ARGV[1] > tonumber(ARGV[2]) then return 0 " +
"else return redis.call(INCRBY, KEYS[2], ARGV[1]) end";
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Arrays.asList(quotaKey, usageKey), amount, redisTemplate.opsForValue().get(quotaKey));
return result != null && result > 0;
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.
配额管理建议:
多层控制:应用层 + 基础设施层双保险。提前预警:当资源使用接近阈值时提醒租户升级。弹性伸缩:结合计费与限流机制。结论
多租户 SaaS 架构的核心挑战在于:数据的干净隔离与资源的公平分配。
在数据层面,数据库/表/行级隔离各有优劣,需要根据业务规模与成本选择。在资源层面,通用配额模型 + Redis 分布式限流是高并发场景下的最佳实践。在安全层面,基于 JWT 的租户上下文和 Spring Security 的细粒度权限控制可确保租户之间权限清晰。通过上述方案,我们已经在多个 SaaS 项目中实现了从数百到数十万租户的平滑扩展,既保证了数据安全,又实现了资源的高效利用。
未来的 SaaS 架构演进中,多租户隔离与配额管理仍会是不可或缺的基础能力。
阅读剩余
THE END