多租户架构设计

深入理解 SaaS 多租户架构的三种隔离模型:共享数据库共享 Schema、共享数据库独立 Schema、独立数据库,掌握租户路由、数据隔离与安全策略。

多租户架构设计

多租户架构是 SaaS 平台的核心基础设施,它允许多个客户(租户)共享同一套应用和基础设施,同时保证数据隔离与性能公平。本文系统讲解三种多租户隔离模型的实现策略与权衡。


1. 多租户核心概念

1.1 租户定义

**租户(Tenant)**是多租户系统中独立客户单位的抽象,通常对应一个企业客户或组织。每个租户拥有独立的数据、配置、用户和权限。

单租户 vs 多租户:

单租户:每个客户部署一套独立系统
  客户 A → [应用 A + 数据库 A + 服务器 A]
  客户 B → [应用 B + 数据库 B + 服务器 B]
  客户 C → [应用 C + 数据库 C + 服务器 C]
  
  优点:完全隔离、定制灵活
  缺点:运维成本高、资源利用率低、升级困难

多租户:所有客户共享一套系统
  ┌─────────────────────────────────────┐
  │           应用层(共享代码)            │
  │  ┌──────┐  ┌──────┐  ┌──────┐      │
  │  │租户 A │  │租户 B │  │租户 C │      │
  │  └──────┘  └──────┘  └──────┘      │
  ├─────────────────────────────────────┤
  │           数据层(共享/隔离)           │
  │     通过租户 ID 区分不同租户数据         │
  └─────────────────────────────────────┘
  
  优点:运维简单、成本低、升级一次性
  缺点:隔离复杂、定制受限

2. 三种隔离模型

2.1 模型一:共享数据库 + 共享 Schema

所有租户共享同一个数据库和同一套表,通过 tenant_id 字段区分数据。

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    tenant_id VARCHAR(64) NOT NULL,  -- 租户标识字段
    order_no VARCHAR(64) NOT NULL,
    amount DECIMAL(18, 2) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    INDEX idx_tenant_id (tenant_id),
    INDEX idx_tenant_order (tenant_id, order_no)
);

-- 查询必须带 tenant_id 条件
SELECT * FROM orders WHERE tenant_id = 'tenant_a' AND order_no = 'O2024001';

架构图

┌─────────────────────────────────────┐
│           应用服务(共享)              │
│  ┌──────┐  ┌──────┐  ┌──────┐      │
│  │租户 A │  │租户 B │  │租户 C │      │
│  └──────┘  └──────┘  └──────┘      │
└───────────────┬─────────────────────┘
                │ SQL + tenant_id
┌───────────────▼─────────────────────┐
│           数据库(共享)               │
│  ┌───────────────────────────────┐  │
│  │  orders 表                     │  │
│  │  ┌────────┬──────────┬───────┐│  │
│  │  │tenant_a│ O2024001 │ 100.00││  │
│  │  │tenant_b│ O2024002 │ 200.00││  │
│  │  │tenant_c│ O2024003 │ 300.00││  │
│  │  └────────┴──────────┴───────┘│  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

优点

  • 运维最简单,成本最低
  • 新增租户零成本(只需插入 tenant_id)
  • 横向扩展容易(只需扩展数据库)

缺点

  • 数据隔离弱(SQL 注入可能跨租户)
  • 单租户大数据量影响全表性能
  • 备份恢复需过滤 tenant_id
  • 复杂查询需全表扫描(除非 tenant_id 在最左前缀)

适用场景:中小型 SaaS、初创产品、租户数据量差异不大。

2.2 模型二:共享数据库 + 独立 Schema

同一数据库实例中为每个租户创建独立的 Schema(命名空间),表结构相同但物理隔离。

-- 租户 A 的 Schema
CREATE SCHEMA tenant_a;
CREATE TABLE tenant_a.orders (...);

-- 租户 B 的 Schema  
CREATE SCHEMA tenant_b;
CREATE TABLE tenant_b.orders (...);

-- 切换 Schema
SET search_path TO tenant_a;
SELECT * FROM orders;  -- 实际查询 tenant_a.orders

架构图

┌─────────────────────────────────────┐
│           应用服务(共享)              │
│  ┌──────┐  ┌──────┐  ┌──────┐      │
│  │租户 A │  │租户 B │  │租户 C │      │
│  └──────┘  └──────┘  └──────┘      │
└───────────────┬─────────────────────┘
                │ 路由到对应 Schema
┌───────────────▼─────────────────────┐
│         数据库实例(共享)              │
│  ┌───────────────────────────────┐  │
│  │ Schema: tenant_a              │  │
│  │   ├─ orders                   │  │
│  │   ├─ users                    │  │
│  │ Schema: tenant_b              │  │
│  │   ├─ orders                   │  │
│  │   ├─ users                    │  │
│  │ Schema: tenant_c              │  │
│  │   ├─ orders                   │  │
│  │   ├─ users                    │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

优点

  • 逻辑隔离好(Schema 级别)
  • 单租户备份恢复简单(导出单个 Schema)
  • 可针对单租户优化(索引、统计信息)

缺点

  • Schema 数量有上限(PostgreSQL 无明确限制,MySQL 中 Database 约 10万)
  • DDL 变更需遍历所有 Schema
  • 连接池管理复杂(动态切换 Schema)

适用场景:中型 SaaS、租户数据量差异较大、有隔离合规要求。

2.3 模型三:独立数据库

每个租户拥有独立的数据库实例,物理上完全隔离。

┌─────────────────────────────────────┐
│           应用服务(共享)              │
│  ┌──────┐  ┌──────┐  ┌──────┐      │
│  │租户 A │  │租户 B │  │租户 C │      │
│  └──────┘  └──────┘  └──────┘      │
└──────┬──────────┬──────────┬────────┘
       │          │          │
┌──────▼──┐  ┌────▼───┐  ┌───▼────┐
│ 数据库 A │  │ 数据库 B │  │ 数据库 C │
│(tenant_a)│  │(tenant_b)│  │(tenant_c)│
└─────────┘  └────────┘  └────────┘

优点

  • 最强隔离(物理隔离)
  • 最高安全性(租户间完全不可见)
  • 可独立备份、恢复、扩容
  • 可独立升级数据库版本

缺点

  • 成本最高(每个租户一个实例)
  • 运维复杂(监控 N 个实例)
  • 资源利用率低(小租户浪费资源)

适用场景

  • 大型企业客户(对隔离性要求极高)
  • 合规要求严苛的行业(金融、医疗、政府)
  • 高客单价 SaaS(可承受独立数据库成本)

2.4 三种模型对比

维度共享 Schema独立 Schema独立数据库
隔离级别应用层数据库逻辑层物理层
成本最低中等最高
运维复杂度
扩展性差(单租户扩展)
数据隔离弱(依赖应用)
租户上限无限制约 10万受硬件限制
定制能力
备份恢复复杂(需过滤)中等简单

3. 租户路由与上下文传递

3.1 基于子域名的路由

租户 A: https://tenant-a.saas-product.com
租户 B: https://tenant-b.saas-product.com

Nginx 配置:
server {
    listen 80;
    server_name ~^(?<tenant>[^.]+)\.saas-product\.com$;
    
    location / {
        proxy_set_header X-Tenant-ID $tenant;
        proxy_pass http://app_backend;
    }
}

3.2 基于请求头/参数的路由

HTTP Header: X-Tenant-ID: tenant-a
或 URL 参数: /api/orders?tenantId=tenant-a
或 JWT Claim: { "sub": "user123", "tenant_id": "tenant-a" }

3.3 应用层租户上下文

// ThreadLocal 传递租户上下文
public class TenantContext {
    private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
    
    public static void setTenant(String tenantId) {
        CURRENT_TENANT.set(tenantId);
    }
    
    public static String getTenant() {
        return CURRENT_TENANT.get();
    }
    
    public static void clear() {
        CURRENT_TENANT.remove();
    }
}

// AOP / 拦截器中设置
@Aspect
@Component
public class TenantAspect {
    @Before("@within(RestController)")
    public void setTenantContext(JoinPoint jp) {
        HttpServletRequest request = ((ServletRequestAttributes) 
            RequestContextHolder.getRequestAttributes()).getRequest();
        String tenantId = request.getHeader("X-Tenant-ID");
        TenantContext.setTenant(tenantId);
    }
}

// MyBatis 拦截器自动添加 tenant_id 条件
@Intercepts(@Signature(type = StatementHandler.class, ...))
public class TenantInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String tenantId = TenantContext.getTenant();
        // 自动改写 SQL,添加 tenant_id = 'xxx' 条件
        return invocation.proceed();
    }
}

4. 多租户安全与性能

4.1 安全策略

1. 跨租户查询防护
   □ 所有数据库查询必须带 tenant_id 条件
   □ ORM / 持久层自动注入(不让业务代码控制)
   □ 审计所有不带 tenant_id 的查询(告警)

2. 租户间资源隔离
   □ 连接池按租户隔离或限制最大连接数
   □ CPU/内存资源配额(Cgroup 限制)
   □ 存储配额限制(防止单个租户耗尽磁盘)

3. 数据加密
   □ 敏感字段加密(AES-256)
   □ 可选:按租户独立加密密钥

4.2 性能公平性

策略实现目的
限流(Rate Limit)令牌桶 / 漏桶,按租户隔离防止单个租户占用全部资源
连接池隔离每个租户独立连接池防止连接耗尽
查询超时限制设置单查询最大执行时间防止慢查询影响全系统
资源配额CPU / 存储 / 请求量配额超配额拒绝或降级

5. 混合隔离模型

实际 SaaS 平台常采用混合模型:小租户用共享 Schema,大客户用独立 Schema 或独立数据库。

路由层:

  租户注册时评估:
    数据量 < 1GB → 共享 Schema(池 A)
    数据量 1-100GB → 独立 Schema(共享数据库)
    数据量 > 100GB 或 企业级 → 独立数据库

租户升级路径:
  共享 Schema ──(数据增长)──→ 独立 Schema ──(企业客户)──→ 独立数据库
  
迁移方式:
  在线迁移:双写 → 数据校验 → 切换路由 → 停止旧端写入

6. 总结

多租户架构选择决策树:

租户数量?
  ├── < 1000 → 独立数据库 或 独立 Schema
  ├── 1000-100K → 独立 Schema
  └── > 100K → 共享 Schema
  
数据隔离要求?
  ├── 极高(金融/医疗)→ 独立数据库
  └── 普通 → 共享 Schema 或独立 Schema

成本敏感度?
  ├── 高 → 共享 Schema
  └── 低 → 按需求选择

建议:
  1. 初创 SaaS 从共享 Schema 起步(最省钱)
  2. 预留 Schema/数据库切换能力(代码中抽象 tenant 路由)
  3. 混合模型是大 SaaS 平台的最终形态
  4. 安全红线:绝不让业务代码决定 tenant_id(框架层强制注入)

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页