# 阿里云分布式架构设计

> **前提纠正**：本机仅用于设计验证，**生产部署在阿里云集群**。
> 要求：分布式、可灰度升级、**上线后永不停机维护**。
>
> 本文所有性能数字均来自本机 PostgreSQL 17.11 **实测**（100 万房源/300 万租客/500 万账单），
> 并明确标注到阿里云生产的换算系数。

---

## 一、先回答你的问题：单库上限在哪里，要不要分库

### 1.1 实测数据（可信结论）

测试环境：本机 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 | 全表聚合 |

### 1.2 关键发现：索引列顺序决定生死

**实测踩到并修复的两个真实缺陷（差距达 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 并监控。**

### 1.3 单库上限测算

基于实测，换算到阿里云生产（`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 不是真正的瓶颈，真正的瓶颈是：**

1. **单机故障 = 全站宕机**（你要求永不停机，这一条直接否决单库）
2. **DDL 变更锁表**（你要求灰度升级，ALTER TABLE 会锁表）
3. **备份恢复窗口**（3TB 库的 `pg_dump` 要数小时，停机不可接受）
4. **跨可用区容灾**（单实例无法做）

> ## 🎯 结论：分阶段分库，不是一次性上分库
>
> | 阶段 | 房源量 | 架构 | 何时进入 |
> | --- | --- | --- | --- |
> | **一期** | < 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 |

---

## 三、永不停机的三个支柱

这是你要求的"上线后不能停机维护"，必须从架构层面保证，而不是靠运维小心。

### 支柱 1：数据库变更向后兼容（Expanding / Contracting 模式）

**核心原则：任何一次发布，数据库变更必须是「可被新旧两版代码同时使用」的。**

以「给账单表增加一个状态值 `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 万行，直接被超时中断并回滚。
> 这说明大事务在生产会造成严重后果 —— 必须分批。

### 支柱 2：应用层灰度发布

**灰度维度（按优先级）：**

1. **按租户灰度**（最适合 SaaS）：`tenant_id % 100 < 5` 的先放大客户
2. **按比例灰度**：1% → 5% → 20% → 50% → 100%
3. **按用户/设备**：内部员工先行
4. **按地域**：先一个城市

**实现方式（任选其一，推荐第一个）：**

- **Nacos 配置中心开关**（最简单）：业务代码读开关决定走新逻辑还是旧逻辑
- **Istio/ASM 流量治理**：按 header/权重路由到不同 version 的 Pod
- **Spring Cloud Gateway 路由规则**：按 `tenant_id` hash 分流

**灰度必须能「一键回滚」**，且回滚不需要发版。

### 支柱 3：数据库高可用与零停机运维

| 事项 | 方案 |
| --- | --- |
| 主库故障 | RDS 多可用区，自动主备切换（通常 30~60 秒，有短暂闪断） |
| 计划内维护 | **RDS 有「可维护窗口」但会闪断** → 用**只读副本切换**规避 |
| 版本升级 | 先在只读副本上升级验证 → 再切换 |
| 连接不断 | **应用侧必须实现重连 + 重试**，这是前提 |
| 备份 | RDS 自动备份 + 日志备份（可恢复到任意时间点） |
| 跨地域容灾 | RDS 跨地域备份 + DTS 单向同步到异地只读实例 |

> ⚠️ **诚实提醒**：RDS 主备切换会有 **30~60 秒闪断**，
> 严格意义上不是"零停机"。要真正做到零停机，需要：
> ① 应用层重试 + 幂等（本章后面有专门设计）
> ② 读写分离时，读走副本容忍短暂失败
> ③ 关键写操作走 MQ 异步化，前端只需确认"已受理"

---

## 四、分布式一致性：外部系统集成（本设计的核心难点）

你要接 **第三方签章、第三方 AR、OTA、第三方支付**，未来还有**商城、直播、团购**。
这些集成的共同特征是：**外部系统会超时、会重复回调、会乱序、会不可用**。

> **这是分布式系统最容易出事的地方，也是我第一版设计完全缺失的部分。**

### 4.1 统一设计：集成网关 + 出站事件表 + 幂等

```
业务服务 ──写入──> outbox_event 表（本地事务，与业务同库同事务）
                        │
                        │ 定时扫描（或 CDC 订阅 binlog）
                        ▼
                  集成网关（统一处理重试/限流/降级/签名）
                        │
                        ▼
              外部系统（支付/OTA/签章/AR）
                        │
                        │ 异步回调
                        ▼
                  inbound_callback 表（先落库，再处理）
                        │
                        ▼
                  幂等处理器 → 业务更新
```

**三个关键组件：**

1. **`outbox_event`（出站事件表）**
   解决"业务改了但消息没发出去"的问题。**本地事务保证业务与事件同生共死**，
   再由后台任务投递（至少一次语义）。这比"先发消息再改库"可靠得多。
2. **`inbound_callback`（入站回调表）**
   第三方回调**先落库再处理**，杜绝"处理到一半崩溃导致状态不一致"。
   记录原始报文，便于对账与追查。
3. **幂等表 `idempotency_record`**
   所有外部请求带唯一业务号，处理前先查幂等表。

### 4.2 支付集成（最复杂，必须有对账）

**支付的三条铁律：**

1. **不要把"支付成功"完全寄托于回调。** 回调可能丢失。
   必须有**主动查询**兜底（定时查单）。
2. **必须有日终对账。** 与支付宝/微信/银联下载对账单逐笔核对，
   发现差异自动生成差错单。
3. **金额一律用「分」为单位的整数**存储和传输，**绝不用浮点**。

**支付状态机（必须严格执行）：**

```
待支付 ──支付成功──> 已支付 ──退款──> 已退款
   │                    │
   └──超时/取消──> 已关闭  └──部分退款──> 部分退款
```

**关键：支付成功与账单核销必须幂等。**
同一笔支付回调重复到达 10 次，只能核销 1 次。
本设计已用 `uq_payment_trade_no` 唯一索引 + `payment_allocation` 唯一约束保证（已验证）。

### 4.3 OTA 集成（库存超卖的最大风险源）

**这是最容易翻车的地方。** 携程、美团、飞猪各有自己的库存与价格体系。

**核心问题：多渠道同时卖同一间房 → 必然超卖。**

**解决方案：**

```
平台库存（唯一真相）
     │
     ├── 推送库存到 OTA A（异步，可能失败 → 重试队列）
     ├── 推送库存到 OTA B
     └── 接受 OTA 订单（异步回调）→ 原子扣减本地库存 → 成功则确认，失败则拒单
```

**关键设计：**
- **本地库存是唯一真相**，OTA 上的库存只是"副本"
- OTA 下单回调到达时，**先原子扣减本地库存**，失败则调用 OTA 拒单接口
- 库存同步用**版本号/时间戳**防止旧数据覆盖新数据
- 必须接受"短暂不一致"：OTA 显示有房但实际没了 → 拒单 + 补偿

> ⚠️ 这个场景下，**拒单是正常的、不可避免的**。
> 产品层面要提前准备拒单补偿方案（升级房型/送券），而不是追求 100% 不出错。

### 4.4 电子签章集成

- 签章流程是**异步**的：发起签署 → 用户签署 → 回调通知结果
- 必须保存**签署流水号**与**最终合同文件哈希**
- **关键合规点**：合同原文、签署过程、时间戳必须固化存档（司法举证需要）
- 建议接**法大大 / e签宝 / 上上签**，自建电子签法律效力难保证

### 4.5 第三方 AR 集成

- AR 内容生产（拍摄/建模）在第三方，**但锚点数据必须存自己库**
- 用 `ar_scene.external_provider` / `external_scene_id` 记录映射
- **防厂商锁定**：所有业务关联（热点挂设备、热点挂房间）存在自己的表里，
  换供应商时只需替换媒体资源，业务数据不动

---

## 五、C 端商城/直播/团购的架构预留

你提到未来要接商城、直播、团购。这些与租赁主业务的**负载特征完全不同**：

| 业务 | 特征 | 架构要求 |
| --- | --- | --- |
| 租赁主业务（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`（外部集成表结构）、`../阿里云部署/`（部署与灰度方案）*
