设计一个多租户 SaaS 系统,如何实现租户数据隔离与资源配额控制?

作为一名有着八年 Java 后端开发经验的技术人员,我参与过多个大型 SaaS 系统的架构设计。在这篇博客中,我将分享如何设计一个支持多租户的 SaaS 系统,重点探讨租户数据隔离(数据库级别 / 表级别)和资源配额控制的实现方案。

一、多租户架构概述

多租户(Multi-Tenant)是指一个软件系统同时服务多个客户(租户),每个租户拥有独立的业务空间,但共享相同的基础设施。SaaS 系统的多租户架构设计需要解决两个核心问题:

  • 数据隔离: 确保租户之间的数据互不干扰,满足安全和合规要求。
  • 资源配额: 控制每个租户使用的系统资源(如存储、API 调用次数),避免资源滥用。

二、数据隔离方案对比与实现

1. 数据隔离方案对比

常见的数据隔离方案有三种,各有优缺点:

隔离级别 实现方式 优点 缺点 适用场景
数据库级别 每个租户使用独立数据库 隔离性强,安全性高 成本高,扩展复杂 对数据隔离要求极高的场景
表级别 所有租户共享数据库,但使用独立表 隔离性较好,成本适中 表数量过多时管理复杂 租户数量中等的场景
行级别 所有租户共享表,通过租户 ID 区分 成本低,易于扩展 隔离性弱,需严格权限控制 租户数量庞大的场景

2. 数据库级别隔离实现

架构设计:

图片

核心代码实现(数据源动态切换) :

scss 复制代码
/**
 * 动态数据源路由
 */
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. 表级别隔离实现

架构设计:

图片

核心代码实现(表名动态生成) :

ini 复制代码
/**
 * 表名处理器(基于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) :

scss 复制代码
/**
 * 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. 资源配额管理模型

设计一个通用的资源配额模型,支持多种资源类型:

kotlin 复制代码
/**
 * 资源配额实体
 */
@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. 基于拦截器的配额控制实现

scss 复制代码
/**
 * 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 实现分布式计数器,确保并发场景下的配额精确控制:

ini 复制代码
/**
 * 基于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. 租户识别与认证

scss 复制代码
/**
 * 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 实现基于租户的权限控制:

typescript 复制代码
/**
 * 租户权限表达式
 */
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. 数据隔离方案选择建议

因素 数据库级别 表级别 行级别
隔离性 最高 中等 最低
成本 最高 中等 最低
扩展性 低(新增租户需创建库) 中等(新增租户需创建表) 高(共享表结构)
维护复杂度 中等
适用租户数量 少(<1000) 中(1000-10 万) 多(>10 万)

2. 资源配额控制最佳实践

  • 分层控制: 同时实现应用层和基础设施层的配额控制。
  • 预付费机制: 支持按使用量计费(Pay-as-you-go)和预付费模式。
  • 弹性扩展: 当租户资源使用接近配额时,提供升级提示。
  • 监控与告警: 实时监控资源使用情况,设置异常使用告警。

六、总结

设计一个高效、安全的多租户 SaaS 系统需要综合考虑数据隔离和资源配额控制:

数据隔离:

  • • 数据库级别:适合对隔离性要求极高的场景。
  • • 表级别:平衡隔离性和成本的折中方案。
  • • 行级别:适合租户数量庞大的场景。

资源配额控制:

  • • 设计通用的配额模型,支持多种资源类型。
  • • 使用 Redis 实现分布式环境下的精确控制。
  • • 通过拦截器和 AOP 实现透明的配额检查。

认证与权限:

  • • 从请求中提取租户 ID,建立上下文。
  • • 基于租户 ID 实现细粒度的权限控制。

在实际项目中,建议根据租户规模、数据敏感性和预算选择合适的数据隔离方案,并通过弹性的资源配额控制机制确保系统稳定运行。

通过上述方案,我们成功在多个 SaaS 项目中实现了租户数据的安全隔离和资源的合理分配,支持了从几百到数十万租户的平滑扩展。

相关推荐
Reggie_L1 小时前
spring-cloud概述
java
贾修行1 小时前
深入浅出理解 Reactor:响应式编程的利器
java·reactor
hqxstudying4 小时前
J2EE模式---前端控制器模式
java·前端·设计模式·java-ee·状态模式·代码规范·前端控制器模式
ZeroToOneDev7 小时前
Java(LinkedList和ArrayList底层分析)
java·开发语言
没有bug.的程序员8 小时前
JAVA面试宝典 -《 架构演进:从单体到 Service Mesh》
java·面试·架构
典学长编程8 小时前
Java从入门到精通!第十一天(Java常见的数据结构)
java·开发语言·数据结构
霍格沃兹软件测试开发8 小时前
Playwright 自动化测试系列(6)| 第三阶段:测试框架集成指南:参数化测试 + 多浏览器并行执行
java·数据库·mysql·自动化
Bonnie_12159 小时前
02-netty基础-java四种IO模型
java·开发语言·nio·jetty
我不是星海9 小时前
建造者设计模式
java·开发语言