前提: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. 独立库 | 强 | ✅ 好 | ❌ 完全做不了 | ❌ 极高 | ❌ 极高 |
pg_dump、迁移工具与连接池(每个 schema 都要 search_path 管理)。
实际经验:超过 200~500 个 schema 后管理成本急剧上升。
而百万房源量级的 SaaS,租户数大概率在数千家 —— 必然超标。
且每次加字段要跑数千次 DDL 迁移,运维成本不可接受。
共享 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;
🔴 实测踩到的三个坑(全部已验证):
NULLIF(...,'') 容错。若直接写 current_setting('app.tenant_id')::bigint,在未设上下文时
会抛 invalid input syntax for type bigint: "",
导致整个事务崩溃。加了 NULLIF 后行为变为「返回 0 行」——
安全失败(不泄露、不崩),这才是生产可用的行为。
SET LOCAL 只能在事务内使用。实测在事务外执行会报 WARNING: SET LOCAL can only be used in transaction blocks
且不生效,随后查询因上下文为空而失败。
连接池场景下一律用 SET LOCAL(事务结束自动清除);
若误用会话级 SET,租户上下文会串到下一个请求,造成跨租户数据泄露。
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 的核心竞争力。
代价与补偿: 全局表存在隐私合规风险。补偿措施:
id_no_enc)+ 不可逆哈希查重(id_no_hash),数据库层拿不到明文;
选型总原则:优先用团队已掌握的 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 做主数据,只做索引 |
| 时序数据库 | ClickHouse | IoT 遥测与行为日志。选它而非 TimescaleDB 的理由见下方对比 |
| 向量检索 | pgvector 0.8.7(已装) | 知识库十万~百万切片完全够用,省一套中间件的运维 |
| 报表/OLAP | 起步用 PG 只读副本 + 物化视图;<br>数据量起来后接 ClickHouse | 不要在 PG 主库上跑重报表 |
| 工作流/任务 | XXL-Job 或 PowerJob | 账单批生成、逾期扫描、催缴触发 |
| API 文档 | SpringDoc OpenAPI 3 | — |
| 可观测 | OpenTelemetry + Prometheus + Grafana | 租赁业务链路长,必须能追单笔账单的处理路径 |
| 维度 | ClickHouse | TimescaleDB |
|---|---|---|
| 压缩率 | ✅ 极高(列存,10:1 常见) | 中(行存 + 压缩) |
| 聚合查询性能 | ✅ 极快(向量化执行) | 好 |
| 单条写入延迟 | ⚠️ 差(不擅长点写) | ✅ 好 |
| 与 PG 生态 | ❌ 独立系统 | ✅ 就是 PG 扩展 |
| 运维复杂度 | ⚠️ 需独立集群 | ✅ 复用 PG |
定案:ClickHouse。 理由:IoT 遥测是纯追加 + 按时间聚合场景
(「这栋楼上月总用电量」),正是 ClickHouse 的主场;而点写延迟差的缺点
可以用批量写入(每设备攒 5 秒再写)规避。TimescaleDB 与主库同实例会
抢 OLTP 资源,这是更致命的问题。
⚠️ 但
meter_reading(抄表读数)留在 PostgreSQL,不进 ClickHouse。
因为它要参与计费与对账,必须强一致、可事务回滚。只有「原始上报流」进时序库,
这个边界必须划清楚。
上表是目标态,不是第一天上线的清单。建议分三期:
按百万房源规模推算:
| 表 | 行数估算 | 依据 | 处置 |
|---|---|---|---|
room | 100 万 | 直接给定 | 单表,正常索引 |
room_calendar | 100 万 × 365 = 3.65 亿/年 | 短租房源每天一行 | ⚠️ 必须分区(按月/按年) |
contract | 300 万 | 房源周转率 3 | 单表 |
bill | 3000 万 ~ 1 亿/年 | 每合同每年 12 张 | ⚠️ 必须分区(按 period_start 月分区) |
payment | 3000 万/年 | 与账单同量级 | ⚠️ 必须分区(按 paid_at) |
payment_allocation | 5000 万/年 | 略多于 payment | ⚠️ 必须分区 |
tenant_person | 1000 万 | 直接给定 | 单表可承受;id_no_hash 唯一索引是热点 |
device | 500 万 | 每 5 间房 1 套智能设备 | 单表 |
device_shadow | 500 万 | 一设备一行 | 单表(覆盖写,无增长) |
agent_message | 1 亿+/年 | 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 吧,以后再说」这种致命决策。
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 项真实验证与并发压测。*