← 返回展示台

多租户隔离方案与技术栈选型

前提:Java 团队现状 · 百万级房源 · 千万级租客 · 含非结构化数据与 Agent 能力
本文所有结论都基于本机真实环境核实:PostgreSQL 17.11,已装 pgvector 0.8.7、
pg_trgm、btree_gist、pgcrypto;未装 PostGIS、TimescaleDB。


一、多租户隔离:三方案对比与定案

这是整个 SaaS 架构的第一决策,一旦选错,后期迁移代价是灾难级的。

方案隔离强度百万房源可行性跨租户统计运维成本迁移成本
A. 共享库 + 共享表(tenant_id)弱(靠应用保证)✅ 好✅ 容易✅ 最低✅ 低
B. 共享库 + 独立 schema中⚠️ 中(schema 数千个时元数据膨胀)❌ 需 union 数百 schema⚠️ 高⚠️ 高
C. 独立库强✅ 好❌ 完全做不了❌ 极高❌ 极高

为什么否决 B 和 C

pg_dump、迁移工具与连接池(每个 schema 都要 search_path 管理)。

实际经验:超过 200~500 个 schema 后管理成本急剧上升。

而百万房源量级的 SaaS,租户数大概率在数千家 —— 必然超标。

且每次加字段要跑数千次 DDL 迁移,运维成本不可接受。

定案:A + RLS(行级安全)混合方案

共享 PostgreSQL 集群
  ├── 所有业务表带 tenant_id
  ├── 启用 PostgreSQL 原生 RLS(Row Level Security)
  │     应用通过 SET LOCAL app.tenant_id = 'xxx' 设定上下文
  │     数据库强制过滤,即使应用层漏写 WHERE 也无法越权
  └── 大租户(>5 万房源)单独迁到独立库(预留 escalation 通道)

这个方案的关键优势:安全性不依赖应用代码正确性。

传统「共享表」方案被诟病的就是「应用忘写 WHERE tenant_id=? 就数据泄露」。

RLS 把这条防线下沉到数据库,是唯一能兼顾开发效率与安全的做法。

RLS 落地示例(本机 PostgreSQL 17.11 已实测跑通):

-- 1. 业务表启用 RLS
ALTER TABLE room ENABLE ROW LEVEL SECURITY;
ALTER TABLE room FORCE  ROW LEVEL SECURITY;   -- 连表主也受约束

-- 2. 策略:只能看到自己租户的行
CREATE POLICY p_room_tenant ON room
  USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::bigint);

-- 3. 应用在事务开头设定租户上下文
BEGIN;
SET LOCAL app.tenant_id = '1';
SELECT ...;   -- 自动只返回 tenant_id=1 的行
COMMIT;

🔴 实测踩到的三个坑(全部已验证):

  1. 策略里必须用 NULLIF(...,'') 容错。

若直接写 current_setting('app.tenant_id')::bigint,在未设上下文时

会抛 invalid input syntax for type bigint: "",

导致整个事务崩溃。加了 NULLIF 后行为变为「返回 0 行」——

安全失败(不泄露、不崩),这才是生产可用的行为。

  1. SET LOCAL 只能在事务内使用。

实测在事务外执行会报 WARNING: SET LOCAL can only be used in transaction blocks

且不生效,随后查询因上下文为空而失败。

连接池场景下一律用 SET LOCAL(事务结束自动清除);

若误用会话级 SET,租户上下文会串到下一个请求,造成跨租户数据泄露。

  1. 必须 FORCE ROW LEVEL SECURITY,否则表所有者绕过策略 ——

这是最常见的 RLS 失效原因。

✅ 附带收益:天然免疫 SQL 注入。

实测把上下文伪造成 app.tenant_id = '1 OR 1=1',数据库直接报

invalid input syntax for type bigint —— 因为该值按 bigint 解析,

注入串无法通过类型检查。这比字符串拼接 SQL 安全得多。

实测隔离效果(真实数据):

场景结果
租户 1 视角查询✅ 只返回 A-101(tenant_id=1)
租户 2 视角查询✅ 只返回 B-201(tenant_id=2)
未设上下文✅ 返回 0 行(安全失败)
伪造上下文注入✅ 类型检查拦截

配套落地脚本 ../DDL/08-多租户RLS落地.sql 提供 fn_gen_rls_ddl(),

可自动识别需要启用 RLS 的表并生成 DDL。

实测在本设计中自动识别出 72 张需隔离的表,并正确排除了

tenant_person / tenant_org / credit_profile / tenant 四张全局实体表。

全局实体例外:租客自然人

tenant_person(租客)、tenant_org(企业)、credit_profile(信用)不做租户隔离。

理由(这是本设计最重要的判断之一): 同一个自然人在 A 公司租过房、欠租跑路,

到 B 公司再租房时,如果租客表按租户隔离,B 公司就查不到他的黑名单记录 ——

风控体系直接失效。而风控恰恰是租赁 SaaS 的核心竞争力。

代价与补偿: 全局表存在隐私合规风险。补偿措施:

数据库层拿不到明文;


二、技术栈选型(Java 生态)

选型总原则:优先用团队已掌握的 Java 技术,只在对的地方引入对的中间件。

百万级房源不是「必须上微服务 + 十种中间件」的理由,反而是克制的理由。

层次选型理由
语言/运行时Java 21(LTS)虚拟线程对高并发 I/O 密集的租赁查询收益明显,团队无迁移成本
基础框架Spring Boot 3.4.x团队主流;原生支持虚拟线程
持久层MyBatis-Plus(主)+ Spring Data JPA(报表查询)租赁业务 SQL 复杂度高(多表关联、核销、账龄),MyBatis 更可控;<br>报表用 JPA/原生 SQL 灵活
数据库PostgreSQL 17(主)已核实本机 17.11;JSONB 支持半结构化、RLS 做多租户、<br>分区表成熟、pgvector 免去独立向量库
分库分表暂不引入 ShardingSphere百万房源单表远未到瓶颈。过早分片是负债:<br>会失去跨分片 join,账单核销这类强关联业务直接崩坏。<br>先用分区表(PARTITION),真到瓶颈再上
缓存Redis(已装 8.10.2)会话、房源详情、分布式锁、库存预扣
消息队列RocketMQ 或 Kafka账单生成、工单派单、IoT 事件异步化。<br>团队若熟 Kafka 优先 Kafka
对象存储RustFS(本机已部署,S3 兼容)图片/视频/AR 模型;S3 协议可随时迁云,无锁定
搜索引擎Elasticsearch房源多条件检索(价格区间+户型+地铁+全文)。<br>⚠️ 不要用 ES 做主数据,只做索引
时序数据库ClickHouseIoT 遥测与行为日志。选它而非 TimescaleDB 的理由见下方对比
向量检索pgvector 0.8.7(已装)知识库十万~百万切片完全够用,省一套中间件的运维
报表/OLAP起步用 PG 只读副本 + 物化视图;<br>数据量起来后接 ClickHouse不要在 PG 主库上跑重报表
工作流/任务XXL-Job 或 PowerJob账单批生成、逾期扫描、催缴触发
API 文档SpringDoc OpenAPI 3—
可观测OpenTelemetry + Prometheus + Grafana租赁业务链路长,必须能追单笔账单的处理路径

时序库:为什么选 ClickHouse 而不是 TimescaleDB

维度ClickHouseTimescaleDB
压缩率✅ 极高(列存,10:1 常见)中(行存 + 压缩)
聚合查询性能✅ 极快(向量化执行)好
单条写入延迟⚠️ 差(不擅长点写)✅ 好
与 PG 生态❌ 独立系统✅ 就是 PG 扩展
运维复杂度⚠️ 需独立集群✅ 复用 PG

定案:ClickHouse。 理由:IoT 遥测是纯追加 + 按时间聚合场景

(「这栋楼上月总用电量」),正是 ClickHouse 的主场;而点写延迟差的缺点

可以用批量写入(每设备攒 5 秒再写)规避。TimescaleDB 与主库同实例会

抢 OLTP 资源,这是更致命的问题。

⚠️ 但 meter_reading(抄表读数)留在 PostgreSQL,不进 ClickHouse。
因为它要参与计费与对账,必须强一致、可事务回滚。只有「原始上报流」进时序库,
这个边界必须划清楚。

一个务实的提醒:不要一次上齐

上表是目标态,不是第一天上线的清单。建议分三期:


三、容量测算(判断「要不要分片」的依据)

按百万房源规模推算:

表行数估算依据处置
room100 万直接给定单表,正常索引
room_calendar100 万 × 365 = 3.65 亿/年短租房源每天一行⚠️ 必须分区(按月/按年)
contract300 万房源周转率 3单表
bill3000 万 ~ 1 亿/年每合同每年 12 张⚠️ 必须分区(按 period_start 月分区)
payment3000 万/年与账单同量级⚠️ 必须分区(按 paid_at)
payment_allocation5000 万/年略多于 payment⚠️ 必须分区
tenant_person1000 万直接给定单表可承受;id_no_hash 唯一索引是热点
device500 万每 5 间房 1 套智能设备单表
device_shadow500 万一设备一行单表(覆盖写,无增长)
agent_message1 亿+/年Agent 高频对话⚠️ 分区 + 定期归档
IoT 遥测100 亿+/年见下❌ 必须出 PG,进 ClickHouse

IoT 遥测的测算(这是最容易被低估的量):

100 万房间 × 每间 10 个设备 = 1000 万设备
每设备每 5 分钟上报 1 次 = 288 次/天
1000 万 × 288 = 28.8 亿条/天  → 每年约 1 万亿条

即使按 10% 设备实际联网并降频到 15 分钟一次,仍是 每天数亿条。

这绝对不可能进 OLTP 主库。必须进 ClickHouse,主库只保留

device_shadow(当前状态,固定行数)。

这个测算是本设计最重要的产出之一:它在架构评审时能直接挡住
「IoT 数据先存 PG 吧,以后再说」这种致命决策。


四、ER 关系主干

tenant ─┬─ property ─ building ─ unit ─ floor ─ room ─┬─ bed
        │                                              ├─ room_calendar  (短租库存)
        │                                              ├─ device_binding ─ device ─ device_shadow
        │                                              ├─ media_binding ─ media_asset
        │                                              └─ ar_scene ─ ar_scene_node ─ ar_hotspot
        ├─ contract ─┬─ contract_party / contract_room / contract_term / contract_version
        │            ├─ room_occupancy  (EXCLUDE 防重复出租)
        │            ├─ contract_deposit
        │            └─ bill ─┬─ bill_item
        │                     └─ payment_allocation ─ payment
        ├─ work_order ─┬─ work_order_flow / work_order_item / dispatch_record
        │              └─ → room / contract / tenant_person
        └─ agent_session ── agent_message ── agent_tool_call

tenant_person  (全局实体,不隔离) ── credit_profile
        └── 通过 contract / bill / work_order 与租户发生关系

五、关键决策速查表

决策点结论一句话理由
多租户隔离共享库共享表 + RLS安全下沉到数据库,不依赖应用代码自觉
租客自然人全局实体,不隔离否则跨公司黑名单失效,风控形同虚设
收租与账单关联payment_allocation 中间表一笔款付多张账单是常态,直连必翻车
防重复出租PostgreSQL EXCLUDE 排他约束应用层校验挡不住时序竞态
防短租超卖单条 UPDATE 条件判断先查后写在并发下必然超卖
合同变更只增不改 + 版本表否则历史对账无法追溯
分库分表一期不做过早分片毁掉跨表 join,负债
时序数据ClickHouse,抄表读数留 PG计费要强一致,遥测不用
向量库pgvector(已装)省一套中间件,百万级够用
AR 锚点数据进 PostgreSQL,不进模型文件要能点热点查真实设备状态

*配套 DDL 见 ../DDL/,已在本机 PostgreSQL 17.11 全部执行通过(85 表 / 245 索引),

并跑通 15 项真实验证与并发压测。*

目录

  1. 一、多租户隔离:三方案对比与定案
  2. 二、技术栈选型(Java 生态)
  3. 三、容量测算(判断「要不要分片」的依据)
  4. 四、ER 关系主干
  5. 五、关键决策速查表