postgresql提供五种原生多租户隔离方案:一、schema隔离;二、行级安全策略(rls);三、ltree树形路径隔离;四、schema+rls混合模式;五、tenant_id动态分区分片。

在构建 SaaS 系统时,若发现租户数据混杂、越权访问频发或权限逻辑反复侵入业务代码,则很可能是多租户隔离机制未下沉至数据库层。PostgreSQL 提供了多种原生、可组合的隔离路径,无需依赖中间件或应用层硬编码过滤。以下是针对该问题的多种实现方案:
一、基于 Schema 的租户隔离方案
该方案为每个租户分配独立的 schema,实现物理级数据隔离。所有表、视图、函数均置于租户专属命名空间内,配合 search_path 动态切换,使同一套 SQL 在不同会话中自动命中对应租户对象。
1、为租户创建专属 schema:CREATE SCHEMA tenant_123 AUTHORIZATION app_user;
2、授予租户角色对该 schema 的 USAGE 和 CREATE 权限:GRANT USAGE, CREATE ON SCHEMA tenant_123 TO tenant_123_role;
3、在连接初始化阶段执行:SET search_path = tenant_123, public;
4、确保应用层所有 DDL 与 DML 均不显式指定 schema 名,完全依赖 search_path 解析。
二、基于行级安全策略(RLS)的共享表方案
该方案保留单套表结构,通过 PostgreSQL 的 RLS 机制,在查询执行前由数据库引擎自动注入租户上下文过滤条件。所有租户数据共存于同一张表,但彼此不可见,适用于租户数量庞大、Schema 管理成本敏感的场景。
1、在目标表(如 orders)上启用 RLS:ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
2、创建策略,强制限定当前会话所属租户:CREATE POLICY tenant_isolation_policy ON orders FOR ALL USING (tenant_id = current_setting('app.tenant_id', true)::BIGINT);
3、在应用连接池建立后,立即执行:SET app.tenant_id = '123';
4、确保应用层所有数据库连接均绑定唯一租户 ID,并在连接释放前重置该变量。
三、基于 ltree 扩展的树形租户路径隔离方案
该方案适用于存在租户层级关系(如集团-子公司-部门)的 SaaS 场景。利用 ltree 类型存储路径(如 '1.5.20'),结合 RLS 实现“本公司及下属所有租户”的递归可见性控制,避免应用层多次查询子节点 ID。
1、启用 ltree 扩展:CREATE EXTENSION IF NOT EXISTS ltree;
2、在租户主表(如 sys_tenant)中添加 ltree 类型字段 path,并建立 GIST 索引:CREATE INDEX idx_tenant_path ON sys_tenant USING GIST (path);
3、定义 RLS 策略,允许访问自身及其子路径:CREATE POLICY tenant_tree_policy ON sys_tenant FOR SELECT USING (path
4、在会话启动时设置当前租户路径:SET app.tenant_path = '1.5';
四、混合模式:Schema + RLS 协同方案
该方案将 Schema 隔离与 RLS 结合使用,既利用 Schema 实现强隔离和对象命名自由,又借助 RLS 对跨租户共享表(如系统配置表、审计日志表)实施细粒度行级控制,兼顾安全性与灵活性。
1、为每个租户创建 schema(如 tenant_123),同时保留一个 shared_schema 存放全局只读表;
2、在 shared_schema.audit_logs 表上启用 RLS,并定义策略:USING (tenant_id = current_setting('app.tenant_id', true)::BIGINT);
3、在租户 schema 内建视图引用 shared_schema 表:CREATE VIEW audit_log AS SELECT * FROM shared_schema.audit_logs;
4、确保 shared_schema 对所有租户角色仅有 SELECT 权限,且禁止直接访问其底层表。
五、基于动态分区与租户 ID 的水平分片方案
该方案面向超大规模租户(千级以上)且单表数据量持续增长的场景。以 tenant_id 为分区键对核心业务表进行 LIST 分区,每个租户数据落于独立子表,配合约束排除与索引下推,提升查询效率并降低锁竞争。
1、创建按 tenant_id 分区的主表:CREATE TABLE orders (id BIGSERIAL, tenant_id BIGINT NOT NULL, ...) PARTITION BY LIST (tenant_id);
2、为每个活跃租户创建子分区:CREATE TABLE orders_t123 PARTITION OF orders FOR VALUES IN (123);
3、在子分区上分别建立本地索引:CREATE INDEX idx_orders_t123_tenant_time ON orders_t123 (tenant_id, created_at);
4、确保查询语句中始终包含 tenant_id = ? 条件,以触发分区裁剪。









