云计算、AI、云原生、大数据等一站式技术学习平台

网站首页 > 教程文章 正文

设计一个多租户 SaaS 系统,如何实现租户数据隔离(...

jxf315 2025-09-13 02:05:35 教程文章 2 ℃

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

一、多租户架构概述

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

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

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

1. 数据隔离方案对比

常见的数据隔离方案有三种,各有优缺点: #技术分享

| 隔离级别 | 实现方式 | 优点 | 缺点 | 适用场景 | | ---

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

2. 数据库级别隔离实现

架构设计

+-------------------+

| 租户 A 数据库 | | 租户 B 数据库 | | 租户 N 数据库 | +-------------------+

| 用户表 | | 用户表 | | 用户表 | | 订单表 | | 订单表 | | 订单表 | +-------------------+ +-------------------+ +-------------------+

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

public class TenantRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {

        return TenantContextHolder.getTenantId();
    }
}

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(); } }

@Configuration public class DataSourceConfig { @Bean public DataSource dataSource() { TenantRoutingDataSource routingDataSource = new TenantRoutingDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); 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. 表级别隔离实现

架构设计

plaintext

+-------------------+
|  共享数据库        |
+-------------------+

| 租户 A_订单表 | | 租户 B_用户表 | | 租户 B_订单表 | +-------------------+

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

@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 statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String originalSql = boundSql.getSql();
        String tenantId = TenantContextHolder.getTenantId();


        String modifiedSql = replaceTableNames(originalSql, tenantId);


        Field sqlField = boundSql.getClass().getDeclaredField("sql");
        sqlField.setAccessible(true);
        sqlField.set(boundSql, modifiedSql);

        return invocation.proceed();
    }

    private String replaceTableNames(String sql, String tenantId) {

        return sql.replaceAll("\b(user|order)\b", tenantId + "_$1");
    }
}

4. 行级别隔离实现

架构设计

plaintext

+-------------------+
|  共享数据库        |
+-------------------+

| +

| +

| +

| 订单表 | | +

| +

| + amount | +-------------------+

核心代码实现(自动注入租户 ID)

@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();
    }
}

public class TenantAwareJpaRepository<T, ID> extends SimpleJpaRepository<T, ID> { private final EntityManager entityManager; private final Class<T> domainClass; public TenantAwareJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.entityManager = entityManager; this.domainClass = entityInformation.getJavaType(); } @Override public List<T> findAll() { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<T> query = cb.createQuery(domainClass); Root<T> root = query.from(domainClass); query.where(cb.equal(root.get("tenantId"), TenantContextHolder.getTenantId())); return entityManager.createQuery(query).getResultList(); } }

三、资源配额控制方案

1. 资源配额管理模型

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

@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;


    private LocalDateTime lastUpdateTime;


    public boolean canUseStorage(long size) {
        return (storageUsed + size) <= storageQuota;
    }

    public boolean useStorage(long size) {
        if (!canUseStorage(size)) {
            return false;
        }
        this.storageUsed += size;
        return true;
    }


}

2. 基于拦截器的配额控制实现

public class QuotaInterceptor implements HandlerInterceptor {

    @Autowired
    private TenantQuotaService quotaService;

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        String tenantId = getTenantIdFromRequest(request);
        TenantQuota quota = quotaService.getQuota(tenantId);


        if (quota.getApiCallsUsed() >= quota.getApiCallQuota()) {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.getWriter().write("API调用超出配额");
            return false;
        }


        quotaService.recordApiCall(tenantId);
        return true;
    }
}

3. 分布式环境下的配额控制

使用 Redis 实现分布式计数器,确保并发场景下的配额精确控制:

@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;


        Long quota = redisTemplate.opsForValue().get(quotaKey);
        if (quota == null || quota <= 0) {
            return false;
        }


        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, quota);

        return result != null && result > 0;
    }
}

四、多租户认证与权限控制

1. 租户识别与认证

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        String token = extractToken(request);
        if (token != null) {
            try {
                Claims claims = Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token)
                    .getBody();


                String tenantId = claims.get("tenantId", String.class);
                TenantContextHolder.setTenantId(tenantId);
            } catch (Exception e) {
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                return;
            }
        }

        filterChain.doFilter(request, response);
    }
}

2. 细粒度权限控制

使用 Spring Security 实现基于租户的权限控制:

public class TenantSecurityExpressionRoot extends SecurityExpressionRoot
        implements MethodSecurityExpressionOperations {

    private Object filterObject;
    private Object returnObject;

    public TenantSecurityExpressionRoot(Authentication authentication) {
        super(authentication);
    }


    public boolean isTenantUser(String tenantId) {
        String currentTenantId = TenantContextHolder.getTenantId();
        return currentTenantId != null && currentTenantId.equals(tenantId);
    }



    @Override
    public void setFilterObject(Object filterObject) {
        this.filterObject = filterObject;
    }

    @Override
    public Object getFilterObject() {
        return filterObject;
    }

    @Override
    public void setReturnObject(Object returnObject) {
        this.returnObject = returnObject;
    }

    @Override
    public Object getReturnObject() {
        return returnObject;
    }

    @Override
    public Object getThis() {
        return this;
    }
}

五、方案选择与最佳实践

1. 数据隔离方案选择建议

| 因素 | 数据库级别 | 表级别 | 行级别 | | ---

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

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

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

六、总结

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

  1. 数据隔离
  2. 数据库级别:适合对隔离性要求极高的场景。
  3. 表级别:平衡隔离性和成本的折中方案。
  4. 行级别:适合租户数量庞大的场景。
  5. 资源配额控制
  6. 设计通用的配额模型,支持多种资源类型。
  7. 使用 Redis 实现分布式环境下的精确控制。
  8. 通过拦截器和 AOP 实现透明的配额检查。
  9. 认证与权限
  10. 从请求中提取租户 ID,建立上下文。
  11. 基于租户 ID 实现细粒度的权限控制。

在实际项目中,建议根据租户规模、数据敏感性和预算选择合适的数据隔离方案,并通过弹性的资源配额控制机制确保系统稳定运行。通过上述方案,我们成功在多个 SaaS 项目中实现了租户数据的安全隔离和资源的合理分配,支持了从几百到数十万租户的平滑扩展。

最近发表
标签列表