揭秘!多租户 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.
拦截器控制 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; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
分布式配额控制(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; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.
认证与权限控制JWT 提取租户 ID 在过滤器中解析 JWT,并写入 TenantContextHolderSpring Security 细粒度权限 通过 isTenantUser(tenantId) 方法实现基于租户的访问限制。最佳实践与方案选择数据库级隔离:适用于安全性要求极高、租户数量有限的场景。表级隔离:兼顾隔离性与成本。行级隔离:适用于大规模、多租户场景。

配额管理建议

多层控制:应用层 + 基础设施层双保险。提前预警:当资源使用接近阈值时提醒租户升级。弹性伸缩:结合计费与限流机制。

结论

多租户 SaaS 架构的核心挑战在于:数据的干净隔离资源的公平分配

在数据层面,数据库/表/行级隔离各有优劣,需要根据业务规模与成本选择。在资源层面,通用配额模型 + Redis 分布式限流是高并发场景下的最佳实践。在安全层面,基于 JWT 的租户上下文和 Spring Security 的细粒度权限控制可确保租户之间权限清晰。

通过上述方案,我们已经在多个 SaaS 项目中实现了从数百到数十万租户的平滑扩展,既保证了数据安全,又实现了资源的高效利用。

未来的 SaaS 架构演进中,多租户隔离与配额管理仍会是不可或缺的基础能力。

阅读剩余
THE END