作为一名有着多年 Java 后端开发经验的技术人员,我参与过多个大型 SaaS 系统的架构设计。在这篇博客中,我将分享如何设计一个支持多租户的 SaaS 系统,重点探讨租户数据隔离(数据库级别 / 表级别)和资源配额控制的实现方案。
一、多租户架构概述
多租户(Multi-Tenant)是指一个软件系统同时服务多个客户(租户),每个租户拥有独立的业务空间,但共享相同的基础设施。SaaS 系统的多租户架构设计需要解决两个核心问题:
•数据隔离:确保租户之间的数据互不干扰,满足安全和合规要求。
•资源配额:控制每个租户使用的系统资源(如存储、API 调用次数),避免资源滥用。
二、数据隔离方案对比与实现
1. 数据隔离方案对比
常见的数据隔离方案有三种,各有优缺点:
2. 数据库级别隔离实现
架构设计:
核心代码实现(数据源动态切换) :
/** * 动态数据源路由 */ publicclassTenantRoutingDataSourceextendsAbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { // 从线程上下文中获取当前租户ID return TenantContextHolder.getTenantId(); } } /** * 租户上下文持有者(使用ThreadLocal存储租户ID) */ publicclassTenantContextHolder { privatestaticfinal ThreadLocal<String> CONTEXT = newThreadLocal<>(); publicstaticvoidsetTenantId(String tenantId) { CONTEXT.set(tenantId); } publicstatic String getTenantId() { return CONTEXT.get(); } publicstaticvoidclear() { CONTEXT.remove(); } } /** * 数据源配置 */ @Configuration publicclassDataSourceConfig { @Bean public DataSource dataSource() { TenantRoutingDataSourceroutingDataSource=newTenantRoutingDataSource(); // 初始化所有租户的数据源 Map<Object, Object> targetDataSources = newHashMap<>(); for (TenantConfig tenant : tenantConfigService.getAllTenants()) { targetDataSources.put(tenant.getTenantId(), createDataSource(tenant.getDbUrl(), tenant.getDbUser(), tenant.getDbPassword())); } routingDataSource.setDefaultTargetDataSource(defaultDataSource()); routingDataSource.setTargetDataSources(targetDataSources); return routingDataSource; } // 其他配置方法... }3. 表级别隔离实现
架构设计:
核心代码实现(表名动态生成) :
/** * 表名处理器(基于MyBatis拦截器) */ @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) publicclassTableNameInterceptorimplementsInterceptor { @Override public Object intercept(Invocation invocation)throws Throwable { StatementHandlerstatementHandler= (StatementHandler) invocation.getTarget(); BoundSqlboundSql= statementHandler.getBoundSql(); StringoriginalSql= boundSql.getSql(); StringtenantId= TenantContextHolder.getTenantId(); // 替换表名(添加租户前缀) StringmodifiedSql= replaceTableNames(originalSql, tenantId); // 通过反射修改SQL FieldsqlField= boundSql.getClass().getDeclaredField("sql"); sqlField.setAccessible(true); sqlField.set(boundSql, modifiedSql); return invocation.proceed(); } private String replaceTableNames(String sql, String tenantId) { // 简单实现,实际应使用正则表达式或SQL解析器 return sql.replaceAll("\b(user|order)\b", tenantId + "_$1"); } }4. 行级别隔离实现
架构设计:
核心代码实现(自动注入租户 ID) :
/** * MyBatis拦截器:自动注入租户ID */ @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}) }) publicclassTenantIdInterceptorimplementsInterceptor { @Override public Object intercept(Invocation invocation)throws Throwable { Objectparameter= invocation.getArgs()[1]; StringtenantId= TenantContextHolder.getTenantId(); // 如果参数是实体类,自动注入tenantId if (parameter instanceof BaseEntity) { ((BaseEntity) parameter).setTenantId(tenantId); } return invocation.proceed(); } } /** * JPA规范:自动添加租户ID条件 */ publicclassTenantAwareJpaRepository<T, ID> extendsSimpleJpaRepository<T, ID> { privatefinal EntityManager entityManager; privatefinal Class<T> domainClass; publicTenantAwareJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.entityManager = entityManager; this.domainClass = entityInformation.getJavaType(); } @Override public List<T> findAll() { CriteriaBuildercb= entityManager.getCriteriaBuilder(); CriteriaQuery<T> query = cb.createQuery(domainClass); Root<T> root = query.from(domainClass); // 添加租户ID条件 query.where(cb.equal(root.get("tenantId"), TenantContextHolder.getTenantId())); return entityManager.createQuery(query).getResultList(); } }三、资源配额控制方案
1. 资源配额管理模型
设计一个通用的资源配额模型,支持多种资源类型:
/** * 资源配额实体 */ @Entity @Table(name = "tenant_quota") publicclassTenantQuota { @Id private String tenantId; // 存储配额(MB) private Long storageQuota; // 已使用存储(MB) private Long storageUsed; // API调用次数配额 private Long apiCallQuota; // 已使用API调用次数 private Long apiCallsUsed; // 并发用户数配额 private Integer concurrentUserQuota; // 上次更新时间 private LocalDateTime lastUpdateTime; // 资源使用记录方法 publicbooleancanUseStorage(long size) { return (storageUsed + size) <= storageQuota; } publicbooleanuseStorage(long size) { if (!canUseStorage(size)) { returnfalse; } this.storageUsed += size; returntrue; } // 其他资源使用方法... }2. 基于拦截器的配额控制实现
/** * API调用配额拦截器 */ publicclassQuotaInterceptorimplementsHandlerInterceptor { @Autowired private TenantQuotaService quotaService; @Override publicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception { StringtenantId= getTenantIdFromRequest(request); TenantQuotaquota= quotaService.getQuota(tenantId); // 检查API调用配额 if (quota.getApiCallsUsed() >= quota.getApiCallQuota()) { response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); response.getWriter().write("API调用超出配额"); returnfalse; } // 记录API调用 quotaService.recordApiCall(tenantId); returntrue; } }3. 分布式环境下的配额控制
使用 Redis 实现分布式计数器,确保并发场景下的配额精确控制:
/** * 基于Redis的分布式配额服务 */ @Service publicclassRedisQuotaServiceImplimplementsQuotaService { @Autowired private RedisTemplate<String, Long> redisTemplate; privatestaticfinalStringQUOTA_KEY_PREFIX="tenant:quota:"; privatestaticfinalStringUSAGE_KEY_PREFIX="tenant:usage:"; @Override publicbooleancheckAndConsume(String tenantId, String resourceType, long amount) { StringquotaKey= QUOTA_KEY_PREFIX + tenantId + ":" + resourceType; StringusageKey= USAGE_KEY_PREFIX + tenantId + ":" + resourceType; // 获取配额 Longquota= redisTemplate.opsForValue().get(quotaKey); if (quota == null || quota <= 0) { returnfalse; } // 使用Lua脚本原子性检查并消费资源 Stringscript= "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"; Longresult= redisTemplate.execute( newDefaultRedisScript<>(script, Long.class), Arrays.asList(quotaKey, usageKey), amount, quota); return result != null && result > 0; } }四、多租户认证与权限控制
1. 租户识别与认证
/** * JWT过滤器:从Token中提取租户ID */ publicclassJwtAuthenticationFilterextendsOncePerRequestFilter { @Override protectedvoiddoFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException { Stringtoken= extractToken(request); if (token != null) { try { Claimsclaims= Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody(); // 提取租户ID并设置到上下文中 StringtenantId= claims.get("tenantId", String.class); TenantContextHolder.setTenantId(tenantId); } catch (Exception e) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return; } } filterChain.doFilter(request, response); } }2. 细粒度权限控制
使用 Spring Security 实现基于租户的权限控制:
/** * 租户权限表达式 */ publicclassTenantSecurityExpressionRootextendsSecurityExpressionRoot implementsMethodSecurityExpressionOperations { private Object filterObject; private Object returnObject; publicTenantSecurityExpressionRoot(Authentication authentication) { super(authentication); } /** * 判断当前用户是否属于指定租户 */ publicbooleanisTenantUser(String tenantId) { StringcurrentTenantId= TenantContextHolder.getTenantId(); return currentTenantId != null && currentTenantId.equals(tenantId); } // 其他权限方法... @Override publicvoidsetFilterObject(Object filterObject) { this.filterObject = filterObject; } @Override public Object getFilterObject() { return filterObject; } @Override publicvoidsetReturnObject(Object returnObject) { this.returnObject = returnObject; } @Override public Object getReturnObject() { return returnObject; } @Override public Object getThis() { returnthis; } }五、方案选择与最佳实践
1. 数据隔离方案选择建议
2. 资源配额控制最佳实践
•分层控制:同时实现应用层和基础设施层的配额控制。
•预付费机制:支持按使用量计费(Pay-as-you-go)和预付费模式。
•弹性扩展:当租户资源使用接近配额时,提供升级提示。
•监控与告警:实时监控资源使用情况,设置异常使用告警。
六、总结
设计一个高效、安全的多租户 SaaS 系统需要综合考虑数据隔离和资源配额控制:
数据隔离:
• 数据库级别:适合对隔离性要求极高的场景。
• 表级别:平衡隔离性和成本的折中方案。
• 行级别:适合租户数量庞大的场景。
资源配额控制:
• 设计通用的配额模型,支持多种资源类型。
• 使用 Redis 实现分布式环境下的精确控制。
• 通过拦截器和 AOP 实现透明的配额检查。
认证与权限:
• 从请求中提取租户 ID,建立上下文。
• 基于租户 ID 实现细粒度的权限控制。
在实际项目中,建议根据租户规模、数据敏感性和预算选择合适的数据隔离方案,并通过弹性的资源配额控制机制确保系统稳定运行。
通过上述方案,我们成功在多个 SaaS 项目中实现了租户数据的安全隔离和资源的合理分配,支持了从几百到数十万租户的平滑扩展。