一、为什么SaaS应用需要多租户架构
想象一下你开了一家云服务公司,提供项目管理软件。客户A是卖水果的,客户B是造火箭的,你肯定不希望客户A的员工看到客户B的机密图纸。这时候数据隔离就成了刚需,而多租户架构就是解决这个问题的钥匙。
PostgreSQL作为老牌关系型数据库,提供了三种主流的多租户实现方案:
- 独立数据库(每个租户一个database)
- 共享数据库独立schema(每个租户一个schema)
- 共享表(通过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 { /*...*/ }
四、性能优化必杀技
多租户架构下,这些优化立竿见影:
- 分区表:按租户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');
- 连接池配置:
# Spring配置示例
spring:
datasource:
hikari:
maximum-pool-size: 50
connection-init-sql: SET app.current_tenant = 'default'
- 缓存策略:
# Django缓存键示例(Python)
def get_cache_key(user):
return f"{user.tenant_id}_projects_{user.id}"
五、那些年我们踩过的坑
- 跨租户查询:
-- 错误示范!忘记加tenant_id条件
UPDATE projects SET status = 'DELETED'
WHERE create_time < '2020-01-01';
-- 会把所有租户的老数据都删了!
- 迁移灾难:
# 错误做法:直接导出整个数据库
pg_dump mydb > backup.sql
# 应该按租户单独备份
pg_dump -n tenant_apple mydb > apple.sql
- 监控盲区:
-- 必须按租户统计资源使用
SELECT tenant_id, COUNT(*)
FROM pg_stat_activity
GROUP BY tenant_id;
六、选型决策树
最后送大家一个决策流程图:
- 需要最强隔离 → 选独立数据库
- 租户<100且需要中等隔离 → 选独立schema
- 租户>1000或资源紧张 → 选共享表+RLS
记住:没有银弹!我们团队曾经在电商项目用方案3支撑了5000+租户,但在金融项目却因为合规要求不得不改用方案1。