前提纠正:本机仅用于设计验证,生产部署在阿里云集群。
要求:分布式、可灰度升级、上线后永不停机维护。
本文所有性能数字均来自本机 PostgreSQL 17.11 实测(100 万房源/300 万租客/500 万账单),
并明确标注到阿里云生产的换算系数。
测试环境:本机 32 核 / PostgreSQL 17.11 默认配置(shared_buffers=128MB)
数据规模:100 万房源、300 万租客、500 万账单(3.2 GB)、100 个租户
测试工具:pgbench(PostgreSQL 官方)
| 查询场景 | 实测延迟 | 实测 TPS(16并发) | 扫描方式 |
|---|---|---|---|
| 主键点查房源 | 0.076 ms | 211,409 | Index Scan |
| 房源列表页(租户+状态+LIMIT20) | 0.078 ms | 203,941 | Index Scan |
| 逾期账单统计 | 1.4 ms | ~16,000 | Index Only Scan(0 回表) |
| 组合业务(3 查询/事务) | 0.387 ms | 41,318 | — |
| 混合读写 9:1 | 0.667 ms | 23,882 | — |
| 纯写 UPDATE | 0.423 ms | 37,788 | — |
| 出租率聚合看板 | 66 ms | 241 | 全表聚合 |
实测踩到并修复的两个真实缺陷(差距达 1250 倍):
| 问题 | 修复前 | 修复后 | 提升 |
|---|---|---|---|
按房号查房源(缺 tenant_id 前导索引) | 72.8 ms | 0.042 ms | 1733× |
房源列表页(原索引是部分索引,查询未带 deleted_at IS NULL 用不上) | 43.7 ms (366 TPS) | 0.035 ms (20万 TPS) | 1250× |
结论:在 RLS 架构下,「每个高频查询都必须有以
tenant_id打头的索引」是硬约束。
因为 RLS 会把tenant_id条件加到所有查询上,
任何不以tenant_id为前导列的索引都会退化为「扫描后过滤」。
另一个教训(280 倍):统计信息滞后比索引设计更致命。
同一查询,ANALYZE 前 396 ms,ANALYZE 后 1.4 ms。
优化器选错执行计划会导致灾难性性能退化。生产必须调优 autovacuum 并监控。
基于实测,换算到阿里云生产(shared_buffers=8GB,NVMe SSD,预计 2-5 倍提升):
| 指标 | 本机实测(默认配置) | 阿里云生产估算 | 说明 |
|---|---|---|---|
| 点查 TPS | 211,000 | 40~80 万 | 全内存命中 |
| 混合读写 TPS | 23,882 | 5~10 万 | 更贴近真实 |
| 单表数据量 | 500 万账单 = 3.5GB | 1 亿行 ≈ 70GB 仍可接受 | 但需分区 |
| 存储上限 | — | 单实例 3~5 TB 舒适区 | 超过需分库 |
⚠️ 但 TPS 不是真正的瓶颈,真正的瓶颈是:
pg_dump 要数小时,停机不可接受)## 🎯 结论:分阶段分库,不是一次性上分库
| 阶段 | 房源量 | 架构 | 何时进入 |
| --- | --- | --- | --- |
| 一期 | < 30 万 | 单主库 + 1 只读副本 + 分区 | 上线即用 |
| 二期 | 30~100 万 | 1 主 2 从(读写分离)+ 分区 + 连接池 | 主库 CPU > 50% 或 副本延迟告警 |
| 三期 | > 100 万 或 单库 > 2TB | 按tenant_id分库(ShardingSphere) | 单库存储 > 2TB 或写入 TPS > 5 万 |
这个判断标准是量化的,不是拍脑袋。 一期不做分库的理由:
分库会失去跨库 JOIN 与事务,账单核销这类强关联业务直接崩坏。
在没到瓶颈前分库,是纯负债。
┌─────────────────────┐
│ SLB / ALB(四层) │
└──────────┬──────────┘
│
┌────────────────┴────────────────┐
│ WAF + CDN(静态/图片) │
└────────────────┬────────────────┘
│
┌──────────────────────┴───────────────────────┐
│ ACK(Kubernetes 集群) │
│ ┌────────────────────────────────────────┐ │
│ │ Ingress(Nginx/ALB Ingress) │ │
│ └───────────────┬────────────────────────┘ │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ │ 网关层: Spring Cloud Gateway │ │
│ │ (鉴权/限流/租户路由/灰度) │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ┌───────────────┴────────────────┐ │
│ │ 业务微服务群(灰度可控) │ │
│ ├────────────────────────────────┤ │
│ │ 房源服务 │ 合同服务 │ 计费服务 │ │
│ │ 工单服务 │ IoT服务 │ 媒体服务 │ │
│ │ Agent服务│ 用户服务│ 商城服务 │ │
│ └───────────────┬────────────────┘ │
└──────────────────┼───────────────────────────┘
│
┌────────────────────────┼────────────────────────────┐
│ │ │
┌────┴─────┐ ┌───────┴──────┐ ┌────────┴────────┐
│ RDS PG │ │ Redis 集群 │ │ 消息队列 │
│ 主+只读 │ │ (主从+哨兵) │ │ RocketMQ/Kafka │
│ 多可用区 │ └──────────────┘ └────────┬────────┘
└────┬─────┘ │
│ ┌──────┴──────┐
│ │ OSS 对象存储│
┌────┴──────────┐ └─────────────┘
│ ClickHouse │ ┌─────────────┐ ┌──────────────┐
│ (IoT/行为) │ │ ES 搜索 │ │ AnalyticDB │
└───────────────┘ └─────────────┘ │ (报表可选) │
└──────────────┘
| 层次 | 阿里云产品 | 选型理由 |
|---|---|---|
| 负载均衡 | ALB / CLB | 支持四层七层,健康检查与灰度 |
| 容器编排 | ACK(Kubernetes) | 灰度发布的基础设施,滚动/金丝雀必需 |
| 应用防火墙 | WAF 3.0 | C 端业务必需,防刷防爬(房源爬虫是行业常态) |
| CDN | 阿里云 CDN | 房源图片/AR 全景图分发,必须用 |
| 数据库 | RDS PostgreSQL(高可用版,多可用区) | 主备自动切换、秒级 RPO |
| 只读副本 | RDS 只读实例 × 2 | 报表/C 端查询隔离 |
| 缓存 | 云数据库 Redis(集群版) | 会话、房源缓存、分布式锁 |
| 消息队列 | RocketMQ 5.x | 事务消息(支付/OTA 场景必需) |
| 对象存储 | OSS | 图片/视频/AR 模型,配 CDN |
| 搜索 | Elasticsearch(阿里云 ES) | 房源多条件检索 + 地图附近搜索 |
| 时序/OLAP | ClickHouse(阿里云或自建) | IoT 遥测、行为日志、报表 |
| 数据同步 | DTS | 主从/跨库同步、增量订阅(binlog) |
| 容器镜像 | ACR | 镜像仓库 |
| 配置/注册 | MSE Nacos | 服务发现 + 配置中心(灰度开关) |
| 链路追踪 | ARMS | 长链路排查(一笔账单跨多个服务) |
| 日志 | SLS 日志服务 | 集中日志 + 审计 |
| 密钥管理 | KMS | 证件号加密、支付密钥托管 |
| 密钥/证书 | SSL 证书服务 | HTTPS |
这是你要求的"上线后不能停机维护",必须从架构层面保证,而不是靠运维小心。
核心原则:任何一次发布,数据库变更必须是「可被新旧两版代码同时使用」的。
以「给账单表增加一个状态值 refunding」为例:
| 步骤 | 动作 | 说明 |
|---|---|---|
| 1. Expand | 只放宽约束,不加新列 | CHECK 约束先加上新值 |
| 2. 发布代码 A | 新代码能写 refunding,旧代码能读(当作 pending 处理) | 灰度期间两版共存 |
| 3. 数据迁移 | 后台批次把存量数据规范化 | 不停写 |
| 4. 发布代码 B | 移除对旧状态的兼容 | — |
| 5. Contract | 收紧约束、删除废弃列 | 必须等所有旧实例下线 |
❌ 绝对禁止的 DDL 操作(会锁表或破坏旧代码):
| 危险操作 | 后果 | 正确做法 |
|---|---|---|
ALTER TABLE ... ADD COLUMN NOT NULL(无默认值) | 锁表 + 旧代码 INSERT 失败 | 先加 nullable,回填,再加约束 |
ALTER TABLE ... RENAME COLUMN | 旧代码立即崩溃 | 加新列 + 双写 + 迁移 + 删旧列 |
ALTER TABLE ... ALTER COLUMN TYPE | 全表重写,长时间锁 | 加新列 + 双写 + 迁移 |
DROP COLUMN | 旧代码崩溃 | 至少等 2 个发布周期 |
CREATE INDEX(非并发) | 阻塞所有写入 | 必须 CREATE INDEX CONCURRENTLY |
大表 UPDATE 全表 | 长事务 + 膨胀 | 分批(每批 5000 行) |
本机实测佐证:我在测试时一次性插入 2500 万行,直接被超时中断并回滚。
这说明大事务在生产会造成严重后果 —— 必须分批。
灰度维度(按优先级):
tenant_id % 100 < 5 的先放大客户实现方式(任选其一,推荐第一个):
tenant_id hash 分流灰度必须能「一键回滚」,且回滚不需要发版。
| 事项 | 方案 |
|---|---|
| 主库故障 | RDS 多可用区,自动主备切换(通常 30~60 秒,有短暂闪断) |
| 计划内维护 | RDS 有「可维护窗口」但会闪断 → 用只读副本切换规避 |
| 版本升级 | 先在只读副本上升级验证 → 再切换 |
| 连接不断 | 应用侧必须实现重连 + 重试,这是前提 |
| 备份 | RDS 自动备份 + 日志备份(可恢复到任意时间点) |
| 跨地域容灾 | RDS 跨地域备份 + DTS 单向同步到异地只读实例 |
⚠️ 诚实提醒:RDS 主备切换会有 30~60 秒闪断,
严格意义上不是"零停机"。要真正做到零停机,需要:
① 应用层重试 + 幂等(本章后面有专门设计)
② 读写分离时,读走副本容忍短暂失败
③ 关键写操作走 MQ 异步化,前端只需确认"已受理"
你要接 第三方签章、第三方 AR、OTA、第三方支付,未来还有商城、直播、团购。
这些集成的共同特征是:外部系统会超时、会重复回调、会乱序、会不可用。
这是分布式系统最容易出事的地方,也是我第一版设计完全缺失的部分。
业务服务 ──写入──> outbox_event 表(本地事务,与业务同库同事务)
│
│ 定时扫描(或 CDC 订阅 binlog)
▼
集成网关(统一处理重试/限流/降级/签名)
│
▼
外部系统(支付/OTA/签章/AR)
│
│ 异步回调
▼
inbound_callback 表(先落库,再处理)
│
▼
幂等处理器 → 业务更新
三个关键组件:
outbox_event(出站事件表)解决"业务改了但消息没发出去"的问题。本地事务保证业务与事件同生共死,
再由后台任务投递(至少一次语义)。这比"先发消息再改库"可靠得多。
inbound_callback(入站回调表)第三方回调先落库再处理,杜绝"处理到一半崩溃导致状态不一致"。
记录原始报文,便于对账与追查。
idempotency_record所有外部请求带唯一业务号,处理前先查幂等表。
支付的三条铁律:
必须有主动查询兜底(定时查单)。
发现差异自动生成差错单。
支付状态机(必须严格执行):
待支付 ──支付成功──> 已支付 ──退款──> 已退款
│ │
└──超时/取消──> 已关闭 └──部分退款──> 部分退款
关键:支付成功与账单核销必须幂等。
同一笔支付回调重复到达 10 次,只能核销 1 次。
本设计已用 uq_payment_trade_no 唯一索引 + payment_allocation 唯一约束保证(已验证)。
这是最容易翻车的地方。 携程、美团、飞猪各有自己的库存与价格体系。
核心问题:多渠道同时卖同一间房 → 必然超卖。
解决方案:
平台库存(唯一真相)
│
├── 推送库存到 OTA A(异步,可能失败 → 重试队列)
├── 推送库存到 OTA B
└── 接受 OTA 订单(异步回调)→ 原子扣减本地库存 → 成功则确认,失败则拒单
关键设计:
⚠️ 这个场景下,拒单是正常的、不可避免的。
产品层面要提前准备拒单补偿方案(升级房型/送券),而不是追求 100% 不出错。
ar_scene.external_provider / external_scene_id 记录映射换供应商时只需替换媒体资源,业务数据不动
你提到未来要接商城、直播、团购。这些与租赁主业务的负载特征完全不同:
| 业务 | 特征 | 架构要求 |
|---|---|---|
| 租赁主业务(B 端为主) | 写多、强一致、低并发 | 事务型数据库 |
| 商城 | 读多、库存扣减、秒杀 | 独立库 + Redis 库存 + 限流 |
| 直播 | 高并发读、低延迟 | 独立服务 + CDN + WebRTC |
| 团购 | 短时脉冲流量 | 独立集群 + 弹性伸缩 + 排队 |
核心原则:C 端与 B 端物理隔离。
B 端(运营后台/管家/房东) → 业务库(写主库)
C 端(租客 App/小程序) → 读副本 + Redis + ES,绝不直连主库
商城/直播/团购 → 独立库 + 独立集群
为什么必须隔离?
一次直播带货的脉冲流量可以瞬间打满连接池,
如果和租赁主业务共用数据库,会导致收租、签合同全部不可用。
这种事故在行业内非常常见。
预留设计(本设计已考虑):
tenant 表已有 isolation_mode 字段,支持大租户独立库user_account 绑定多个租户(商城和租赁可共用账号)outbox_event 统一对外集成,新增业务只需加事件类型| 项目 | 本机实测 | 换算说明 |
|---|---|---|
| 点查延迟 | 0.076 ms | 本机全内存;阿里云 RDS + NVMe 接近 |
| 混合 TPS | 23,882 | 生产估算 5~10 万(shared_buffers 128MB→8GB) |
| 100 万房源表大小 | 301 MB | 线性外推,1000 万房源约 3 GB |
| 500 万账单表大小 | 3.5 GB | 1 亿账单约 70 GB,必须分区 |
| 出租率聚合 | 66 ms | 这是唯一需要优化到 OLAP 的场景 |
必须说明的口径问题:本机
shared_buffers只有 128MB(默认值),
阿里云生产会设为物理内存 25%(8C32G → 8GB),
但不要把换算系数当成精确值,它是量级参考。
上线前必须在阿里云环境用真实数据重新压测。
唯一确定需要 OLAP 的场景:出租率/收入看板。
66ms × 100 个并发用户 = 6.6 秒,会拖垮数据库。
方案:看板数据走 ClickHouse 或每日物化视图预聚合,绝不在主库实时算。
| 决策点 | 结论 | 依据 |
|---|---|---|
| 一期是否分库 | 不分 | 百万房源单库可扛;分库会毁掉跨表事务 |
| 何时分库 | 单库 > 2TB 或写 TPS > 5 万 | 量化门槛,非感觉 |
| 数据库高可用 | RDS 多可用区 + 2 只读副本 | 永不停机的底线 |
| 永不停机怎么做 | Expanding/Contracting + 灰度 + 应用重试 | 三支柱缺一不可 |
| 外部集成 | outbox + inbound 回调表 + 幂等表 | 分布式一致性唯一可靠做法 |
| 支付对账 | 必须做日终对账 | 回调会丢,不能只信回调 |
| OTA 防超卖 | 本地库存为唯一真相 + 允许拒单 | 多渠道必然不一致 |
| C 端隔离 | 必须物理隔离 | 直播脉冲会打垮主业务 |
| 看板 | ClickHouse / 物化视图 | 实测 66ms 实时聚合不可接受 |
| 索引硬约束 | 高频查询必须有 tenant_id 前导 | 实测差距 1250 倍 |
配套:02-分布式数据一致性设计.md(外部集成表结构)、../阿里云部署/(部署与灰度方案)