揭秘!多租戶 SaaS 系統(tǒng)這樣設(shè)計:數(shù)據(jù)庫級/表級隔離 + 資源配額全攻略
在參與多個大型 SaaS 平臺的架構(gòu)設(shè)計之后,我逐漸發(fā)現(xiàn),多租戶架構(gòu)的核心價值并不只是“共享”,而是“隔離 + 配額”。 一方面,我們必須確保不同租戶之間的數(shù)據(jù)嚴(yán)格分離,以滿足合規(guī)性與安全性;另一方面,還需要限制資源消耗,避免某些租戶“獨(dú)占”系統(tǒng)性能。
本文將結(jié)合實(shí)踐經(jīng)驗,全面拆解 多租戶 SaaS 系統(tǒng)的數(shù)據(jù)隔離方案(數(shù)據(jù)庫級 / 表級 / 行級)與資源配額控制策略,并給出核心代碼示例,幫助你在實(shí)際項目中快速落地。
什么是多租戶架構(gòu)?
多租戶(Multi-Tenancy)是一種典型的 SaaS 模式:
- 單實(shí)例運(yùn)行:一套系統(tǒng)為多個租戶(Tenant)服務(wù)。
- 邏輯隔離:每個租戶擁有獨(dú)立的業(yè)務(wù)空間,但共享基礎(chǔ)設(shè)施(數(shù)據(jù)庫、存儲、計算資源)。
多租戶架構(gòu)需要解決兩個關(guān)鍵問題:
- 數(shù)據(jù)隔離 —— 確保租戶之間互不干擾。
- 資源配額 —— 控制存儲、API 調(diào)用、并發(fā)用戶數(shù)等,防止“資源搶占”。
數(shù)據(jù)隔離方案對比與實(shí)現(xiàn)
在多租戶架構(gòu)下,數(shù)據(jù)隔離常見有三種方式:數(shù)據(jù)庫級、表級和行級。
數(shù)據(jù)庫級隔離
架構(gòu)思路:每個租戶獨(dú)立一個數(shù)據(jù)庫。
+-------------------+ +-------------------+ +-------------------+
| Tenant A Database | | Tenant B Database | | Tenant N Database |
+-------------------+ +-------------------+ +-------------------+
| Users Table | | Users Table | | Users Table |
| Orders Table | | Orders Table | | Orders Table |
+-------------------+ +-------------------+ +-------------------+代碼實(shí)現(xiàn):動態(tài)數(shù)據(jù)源路由
// 文件路徑: 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(); }
}表級隔離
架構(gòu)思路:所有租戶共享數(shù)據(jù)庫,但每個租戶有獨(dú)立的表(加前綴)。
+---------------------+
| Shared Database |
+---------------------+
| TenantA_Users_Table |
| TenantA_Orders_Table|
| TenantB_Users_Table |
| TenantB_Orders_Table|
+---------------------+代碼實(shí)現(xiàn):動態(tài)表名攔截器(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();
}
}行級隔離
架構(gòu)思路:單庫單表,通過 tenant_id 字段區(qū)分租戶。
+-------------------+
| Shared Database |
+-------------------+
| Users Table | tenant_id + user_id
| Orders Table | tenant_id + order_id
+-------------------+代碼實(shí)現(xiàn):自動注入租戶 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();
}
}資源配額控制
在多租戶系統(tǒng)中,資源配額控制防止“資源獨(dú)占”。
通用資源模型
// 文件路徑: 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 調(diào)用
// 文件路徑: 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;
}
}認(rèn)證與權(quán)限控制
- JWT 提取租戶 ID 在過濾器中解析 JWT,并寫入
TenantContextHolder。 - Spring Security 細(xì)粒度權(quán)限 通過
isTenantUser(tenantId)方法實(shí)現(xiàn)基于租戶的訪問限制。
最佳實(shí)踐與方案選擇
- 數(shù)據(jù)庫級隔離:適用于安全性要求極高、租戶數(shù)量有限的場景。
- 表級隔離:兼顧隔離性與成本。
- 行級隔離:適用于大規(guī)模、多租戶場景。
配額管理建議:
- 多層控制:應(yīng)用層 + 基礎(chǔ)設(shè)施層雙保險。
- 提前預(yù)警:當(dāng)資源使用接近閾值時提醒租戶升級。
- 彈性伸縮:結(jié)合計費(fèi)與限流機(jī)制。
結(jié)論
多租戶 SaaS 架構(gòu)的核心挑戰(zhàn)在于:數(shù)據(jù)的干凈隔離與資源的公平分配。
- 在數(shù)據(jù)層面,數(shù)據(jù)庫/表/行級隔離各有優(yōu)劣,需要根據(jù)業(yè)務(wù)規(guī)模與成本選擇。
- 在資源層面,通用配額模型 + Redis 分布式限流是高并發(fā)場景下的最佳實(shí)踐。
- 在安全層面,基于 JWT 的租戶上下文和 Spring Security 的細(xì)粒度權(quán)限控制可確保租戶之間權(quán)限清晰。
通過上述方案,我們已經(jīng)在多個 SaaS 項目中實(shí)現(xiàn)了從數(shù)百到數(shù)十萬租戶的平滑擴(kuò)展,既保證了數(shù)據(jù)安全,又實(shí)現(xiàn)了資源的高效利用。
未來的 SaaS 架構(gòu)演進(jìn)中,多租戶隔離與配額管理仍會是不可或缺的基礎(chǔ)能力。
























