一、为什么SaaS应用需要多租户架构

想象一下你开了一家云服务公司,提供项目管理软件。客户A是卖水果的,客户B是造火箭的,你肯定不希望客户A的员工看到客户B的机密图纸。这时候数据隔离就成了刚需,而多租户架构就是解决这个问题的钥匙。

PostgreSQL作为老牌关系型数据库,提供了三种主流的多租户实现方案:

  1. 独立数据库(每个租户一个database)
  2. 共享数据库独立schema(每个租户一个schema)
  3. 共享表(通过tenant_id字段区分)
-- 方案2示例:创建租户专属schema
CREATE SCHEMA tenant_apple;  -- 水果公司
CREATE SCHEMA tenant_spacex; -- 火箭公司

-- 在每个schema下创建相同的表结构
CREATE TABLE tenant_apple.projects (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE tenant_spacex.projects (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL 
);

二、三种方案的实战对比

方案1:独立数据库

CREATE DATABASE tenant_apple WITH ENCODING 'UTF8';
CREATE DATABASE tenant_spacex WITH ENCODING 'UTF8';

优点:隔离级别最高,备份恢复灵活
缺点:连接数消耗大,管理成本高

方案2:共享数据库独立schema

-- 设置当前schema(Java的Hibernate可以直接配置)
SET search_path TO tenant_apple;

-- 跨schema查询需要完整路径
SELECT * FROM tenant_spacex.projects 
WHERE name LIKE '%机密%';  -- 这样依然会暴露数据!

优点:平衡了隔离性和资源利用率
缺点:需要严格权限控制

方案3:共享表

CREATE TABLE projects (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100),
    tenant_id VARCHAR(36) NOT NULL  -- 关键字段
);

-- 必须带租户条件查询
SELECT * FROM projects 
WHERE tenant_id = 'apple' AND name LIKE '%水果%';

优点:资源利用率最高
缺点:开发容易遗漏过滤条件

三、Row Level Security实战

PostgreSQL的RLS(行级安全)可以完美解决方案3的安全隐患:

-- 启用RLS策略
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- 创建策略:只允许访问本租户数据
CREATE POLICY tenant_policy ON projects
    USING (tenant_id = current_setting('app.current_tenant'));

-- 设置当前租户(应用层需要维护)
SET app.current_tenant = 'apple';

配合Hibernate过滤器更安全:

// Java示例:自动附加tenant_id条件
@FilterDef(
    name = "tenantFilter", 
    parameters = @ParamDef(name = "tenantId", type = "string")
)
@Filter(
    name = "tenantFilter", 
    condition = "tenant_id = :tenantId"
)
@Entity
public class Project { /*...*/ }

四、性能优化必杀技

多租户架构下,这些优化立竿见影:

  1. 分区表:按租户ID水平分片
CREATE TABLE projects (
    id SERIAL,
    tenant_id VARCHAR(36),
    data JSONB
) PARTITION BY LIST (tenant_id);

CREATE TABLE projects_apple PARTITION OF projects
    FOR VALUES IN ('apple');
  1. 连接池配置
# Spring配置示例
spring:
  datasource:
    hikari:
      maximum-pool-size: 50
      connection-init-sql: SET app.current_tenant = 'default'
  1. 缓存策略
# Django缓存键示例(Python)
def get_cache_key(user):
    return f"{user.tenant_id}_projects_{user.id}" 

五、那些年我们踩过的坑

  1. 跨租户查询
-- 错误示范!忘记加tenant_id条件
UPDATE projects SET status = 'DELETED' 
WHERE create_time < '2020-01-01';
-- 会把所有租户的老数据都删了!
  1. 迁移灾难
# 错误做法:直接导出整个数据库
pg_dump mydb > backup.sql  
# 应该按租户单独备份
pg_dump -n tenant_apple mydb > apple.sql
  1. 监控盲区
-- 必须按租户统计资源使用
SELECT tenant_id, COUNT(*) 
FROM pg_stat_activity
GROUP BY tenant_id;

六、选型决策树

最后送大家一个决策流程图:

  1. 需要最强隔离 → 选独立数据库
  2. 租户<100且需要中等隔离 → 选独立schema
  3. 租户>1000或资源紧张 → 选共享表+RLS

记住:没有银弹!我们团队曾经在电商项目用方案3支撑了5000+租户,但在金融项目却因为合规要求不得不改用方案1。