多租户系统不知道选哪种隔离方案?这 3 种方案对比一目了然

物理隔离太贵、逻辑隔离太危险、Schema 隔离又复杂...这篇文章用最直白的语言为你讲解每种方案的原理、优势、风险。看完这篇,SaaS 架构师的谈资又多了一个。

之前给大家分享了尝试给多租户系统做数据隔离时碰到的一些问题,今天给大家分享下多租户系统数据隔离的常用方案,以及每种方案的优劣势。

为什么要考虑数据隔离?

在选择具体方案之前,先想想为什么要做数据隔离

1. 安全是头等大事

想象一下,你的客户 A 是可口可乐,客户 B 是百事可乐。如果因为代码里少写了一个 WHERE tenant_id = ?,导致可口可乐能看到百事的销售数据…这不仅仅是 Bug,这是事故。

更严重的是,如果你服务的是医疗、金融行业,数据泄露可能直接违反 GDPR、HIPAA 等法规,罚款能让公司关门。

2. 性能也是个问题

共享资源就意味着相互影响。某个大客户跑了个全表扫描,其他客户的查询都变慢了。这种"一个人生病,全家吃药"的情况,对 SaaS 业务来说是致命的。

3. 客户信任和商务需求

有些大客户在签合同时会明确要求:“我的数据必须独立存储”。这不是技术问题,是商务问题。你不答应,合同就签不下来。

三种常见的隔离方案

好,现在我们知道了"为什么要做",接下来看看"怎么做"。业界主流有三种方案,各有利弊。

方案一:物理隔离 - 每个租户独立数据库

这是最彻底的方案:每个租户一个独立的数据库实例。

租户 A -> 数据库 A (db_tenant_a)
租户 B -> 数据库 B (db_tenant_b)
租户 C -> 数据库 C (db_tenant_c)

优点:

  • 安全性最高:物理隔离,根本不可能串数据
  • 性能完全独立:谁也不影响谁
  • 合规性好:轻松满足各种审计要求
  • 支持深度定制:某个租户要加字段?随便改

缺点:

  • 成本高:100 个租户就是 100 个数据库,资源消耗巨大
  • 运维复杂:升级数据库版本?要操作 100 次
  • 扩展性差:租户数量增长时,基础设施压力巨大
  • 跨租户分析困难:想统计所有租户的订单总量?很麻烦

适合场景:

  • 租户数量少(< 100)
  • 客户对安全性要求极高(金融、医疗)
  • 客户愿意为独立环境付费
  • 每个租户需要深度定制

方案二:PostgreSQL Schema 隔离 - 共享数据库,独立 Schema

这是个折中方案:所有租户共用一个数据库实例,但每个租户有独立的 Schema(或者叫命名空间)。

数据库实例 (PostgreSQL)
├── schema_tenant_a
│   ├── orders
│   └── customers
├── schema_tenant_b
│   ├── orders
│   └── customers
└── schema_tenant_c
    ├── orders
    └── customers

优点:

  • 安全性较好:有一定的逻辑隔离
  • 成本适中:节省了数据库实例成本
  • 备份恢复灵活:可以单独备份某个租户的 Schema
  • 支持一定的定制:不同 Schema 可以有不同的表结构

缺点:

  • Schema 数数量有限制:PostgreSQL Schema 理论上无限,但过多会有性能问题
  • 连接池管理复杂:需要动态切换 Schema
  • 跨租户查询麻烦:需要跨 Schema join
  • 性能仍然相互影响:共享同一个数据库实例的 CPU、内存、IO

实现要点: 主要是在请求进来时动态切换 Schema,比如通过拦截器识别租户后执行 SET search_path TO schema_xxx,让后续查询自动路由到对应的 Schema。

适合场景:

  • 租户数量中等(100-1000)
  • 数据安全要求较高,但预算有限
  • 部分租户需要定制化
  • 使用 PostgreSQL 等支持 Schema 的数据库

方案三:共享表 + tenant_id - 逻辑隔离

这是最经济的方案:所有租户共用同一套表,通过 tenant_id 字段来区分数据。

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    tenant_id VARCHAR(50) NOT NULL,  -- 关键字段
    order_no VARCHAR(100),
    amount DECIMAL(10,2),
    created_at TIMESTAMP,
    INDEX idx_tenant_id (tenant_id)
);

查询时需要始终带上租户条件:

SELECT * FROM orders WHERE tenant_id = 'tenant_123' AND status = 'paid';

优点:

  • 成本最低:一套表服务所有租户
  • 运维简单:升级一次,所有租户生效
  • 扩展性好:轻松支持上万租户
  • 跨租户分析方便:一个 SQL 搞定统计需求

缺点:

  • 安全风险最高:代码里漏了 WHERE tenant_id = ? 就完蛋
  • 性能相互影响:大租户的慢查询会拖累小租户
  • 无法定制:所有租户必须用相同的表结构
  • 数据恢复粒度粗:误删某个租户的数据,恢复很麻烦

适合场景:

  • 租户数量大(> 1000)
  • 小微客户为主,付费能力有限
  • 数据敏感度较低(非金融、医疗)
  • 需要频繁的跨租户数据分析

进阶方案:混合隔离

实际业务中,我们往往不会只用一种方案,而是根据租户级别采用不同的隔离策略

                     API Gateway
                  (识别租户 & 路由)
                         |
        +----------------+----------------+
        |                |                |
    大客户            中等客户          小客户
  (独立数据库)      (独立 Schema)      (共享表)
   tenant_vip        tenant_001-100    tenant_101-9999
      |                   |                  |
   [MySQL]            [PostgreSQL]      [PostgreSQL]
                      multi-schema       shared table

分级策略:

租户级别隔离方案原因
企业版独立数据库商务要求 + 定制化
专业版独立 Schema性能保障
基础版共享表成本优先

这种方式既满足了大客户的个性化需求,又控制了整体成本。关键是做好租户升级时的数据迁移,需要保证迁移过程中数据不丢失、服务不中断。