揭秘!多租戶 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 |
+-------------------+ +-------------------+ +-------------------+代碼實現:動態數據源路由
// 文件路徑: 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(); }
}表級隔離
架構思路:所有租戶共享數據庫,但每個租戶有獨立的表(加前綴)。
+---------------------+
| Shared Database |
+---------------------+
| TenantA_Users_Table |
| TenantA_Orders_Table|
| TenantB_Users_Table |
| TenantB_Orders_Table|
+---------------------+代碼實現:動態表名攔截器(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();
}
}行級隔離
架構思路:單庫單表,通過 tenant_id 字段區分租戶。
+-------------------+
| Shared Database |
+-------------------+
| Users Table | tenant_id + user_id
| Orders Table | tenant_id + order_id
+-------------------+代碼實現:自動注入租戶 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();
}
}資源配額控制
在多租戶系統中,資源配額控制防止“資源獨占”。
通用資源模型
// 文件路徑: 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;
}
}攔截器控制 API 調用
// 文件路徑: 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;
}
}分布式配額控制(Redis 方案)
// 文件路徑: 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;
}
}認證與權限控制
- JWT 提取租戶 ID 在過濾器中解析 JWT,并寫入
TenantContextHolder。 - Spring Security 細粒度權限 通過
isTenantUser(tenantId)方法實現基于租戶的訪問限制。
最佳實踐與方案選擇
- 數據庫級隔離:適用于安全性要求極高、租戶數量有限的場景。
- 表級隔離:兼顧隔離性與成本。
- 行級隔離:適用于大規模、多租戶場景。
配額管理建議:
- 多層控制:應用層 + 基礎設施層雙保險。
- 提前預警:當資源使用接近閾值時提醒租戶升級。
- 彈性伸縮:結合計費與限流機制。
結論
多租戶 SaaS 架構的核心挑戰在于:數據的干凈隔離與資源的公平分配。
- 在數據層面,數據庫/表/行級隔離各有優劣,需要根據業務規模與成本選擇。
- 在資源層面,通用配額模型 + Redis 分布式限流是高并發場景下的最佳實踐。
- 在安全層面,基于 JWT 的租戶上下文和 Spring Security 的細粒度權限控制可確保租戶之間權限清晰。
通過上述方案,我們已經在多個 SaaS 項目中實現了從數百到數十萬租戶的平滑擴展,既保證了數據安全,又實現了資源的高效利用。
未來的 SaaS 架構演進中,多租戶隔離與配額管理仍會是不可或缺的基礎能力。























