LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

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

admin
2025年7月25日 15:22 本文热度 163
在这篇文章中将分享如何设计一个支持多租户的 SaaS 系统,重点探讨租户数据隔离(数据库级别 / 表级别)和资源配额控制的实现方案。


多租户架构概述



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

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


数据隔离方案对比与实现



1. 数据隔离方案对比

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

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

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. 数据隔离方案选择建议

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

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

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


总结



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

数据隔离:

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

资源配额控制:

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

认证与权限:

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

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

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


juejin.cn/post/7516443180431556643


该文章在 2025/7/25 15:22:45 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved