# 明确替代边界

LLMS 索引： [llms.txt](/llms.txt)

---

替代边界不是“PostgreSQL 对阵某产品”的功能打勾表。真正的决策单位是：

```text
某一类数据
在某一类操作下
需要某种一致性/时效/规模/故障语义
由某个团队承担
```

同一个系统可以对一类数据是权威，对另一类数据只是派生副本。

## 18.3.1 缓存、消息、对象存储与离线湖仓 {#item-18-3-1}

### 缓存解决重复读取，不解决权威

缓存适合：

```text
结果计算或读取昂贵
值允许在有限时间内陈旧
miss 可以回到权威
entry 可丢弃并重建
容量与淘汰可接受
```

缓存不适合被默认为：

```text
库存真相
支付状态真相
唯一事件日志
不可恢复业务数据
跨字段事务约束
```

本章 `product-cache-v1` 的 authority：

```json
{
  "product_business_state": "postgresql",
  "cache_entries": "cache"
}
```

这不是文字游戏。假如缓存中 `product.version=8`，PostgreSQL 已是
`version=10`，规则必须保证版本 8 永远不能覆盖版本 10。

### cache-aside 的完整状态机

常见读路径：

```text
read cache
  hit -> verify acceptable version/freshness -> return
  miss -> bounded read from PostgreSQL
       -> write versioned cache entry
       -> return
```

写路径不能只写成“更新 DB 后删缓存”。必须考虑：

```text
DB commit 成功，删除 cache 失败
cache 删除成功，DB transaction 回滚
两个写并发，旧 invalidation 后到
TTL 前后请求同时回源
热点 key 穿透
删除后的旧值复活
cache 整体不可达
```

因此合同包含：

| 字段 | `product-cache-v1` |
|---|---|
| ordering | source version 单调，旧版本不覆盖新版本 |
| idempotency | `product_id + source version` |
| rebuild | 丢弃 namespace，从 PostgreSQL 重建 |
| failure | miss/outage 回退到有界 PostgreSQL 读 |
| reconciliation | 抽样 version + payload checksum |
| deletion | tombstone 或 version bump |
| exit | 关闭 cache 路由并删除派生 namespace |

若无法丢弃 namespace，缓存已经悄悄变成了数据库。

### 消息系统拥有投递，不拥有原事务

事件总线擅长：

```text
异步解耦
一对多消费
保留与重放
按 partition 排序
消费者独立进度
流式处理
```

但“先更新数据库，再发送消息”有经典双写：

```text
DB commit, publish fail   -> 状态变了，没有事件
publish success, DB fail  -> 有事件，没有状态
retry publish             -> 重复事件
```

`order-events-v1` 选择 transactional outbox：

```text
one PostgreSQL transaction
  business state change
  publication intent with stable event_id

external relay
  read pending outbox
  publish at least once
  mark/record progress
```

原子边界只到 outbox。外部投递是 at-least-once，消费者必须幂等。

### “at-least-once”要写清谁至少一次

这几个语义不同：

```text
relay may publish duplicate
broker may redeliver
consumer may process then crash before ack
business side effect may be non-idempotent
```

稳定 `event_id` 是起点，不是终点。消费者需要把“已处理 identity”与业务
效果放在同一原子边界，或使用业务幂等键。

全局顺序通常也不应承诺。本章只承诺：

```text
partition by order_id
no global order
```

否则团队会为一个业务不需要的全序付出吞吐与可用性成本。

### 对象存储拥有大字节，数据库拥有元数据状态

把图片、视频和归档大对象直接塞入 PostgreSQL 并非绝对错误，但常见代价：

```text
database/backup/WAL volume
cache 污染
复制与恢复时间
CDN/分段下载能力
对象生命周期与访问方式
```

`product-media-v1` 采用逐域 authority：

```text
object bytes                         -> object storage
object identity / owner / state /
size / checksum / active generation -> PostgreSQL
```

可靠上传状态机可以是：

```text
reserve metadata row (uploading)
  -> upload immutable object version
  -> verify size + checksum
  -> finalize metadata (ready)
  -> expose signed access
```

失败处理：

```text
orphan object   -> quarantine + reconciliation
ready row but missing bytes -> do not return success
retry upload    -> same object_id + checksum
replace media   -> new immutable generation, then switch metadata
delete          -> tombstone, retention-aware object deletion
```

这里不存在跨 PostgreSQL 与对象存储的传统 ACID transaction，所以显式状态、
幂等与对账比“调用顺序看起来正确”重要。

### 湖仓拥有分析投影，不拥有在线业务解释

离线湖仓适合：

```text
超长保留
列式大扫描
批处理与多引擎消费
历史快照
低成本冷数据
复杂数据科学管线
```

`analytics-export-v1` 的权威拆分：

```text
operational business state -> PostgreSQL
analytical projection      -> lakehouse
```

每个数据集必须显示：

```text
dataset generation
schema/version
source snapshot identity
source commit position or watermark
complete/incomplete state
freshness
reconciliation result
```

当 CDC 断裂时，正确行为不是悄悄继续展示旧数据，而是标记 last complete
watermark，并根据合同停止或降级消费者。

### snapshot + change stream 的一致性接缝

建立新投影通常需要：

```text
take consistent snapshot at position P
load snapshot
consume changes after P
deduplicate/order by source identity
catch up
publish generation
```

如果 snapshot 与 stream 之间没有稳定接缝，会漏掉或重复一段变化。第 29 章
会把它写成迁移状态机并验收。

### 外置并不自动降低数据库压力

反例：

- cache miss storm 反而打爆 primary；
- CDC slot 积压使 WAL 无法回收；
- 搜索全量重建持续扫描并推高 I/O；
- 湖仓导出占据 replica 和网络；
- object reconciliation 每晚做无界全表 join；
- outbox relay 用无索引轮询。

每个外置系统都要预算它对 PostgreSQL 的读取、WAL、连接与保留压力。

## 18.3.2 超大规模检索、流处理与专用分析 {#item-18-3-2}

### “超大规模”必须换成阈值

以下句子都不能触发架构：

```text
以后数据很多
搜索会很复杂
实时要求很高
分析师越来越多
行业都这么做
```

需要换成可测量的问题：

```text
indexed documents / vectors / bytes
ingest/update/delete rate
query mix and concurrency
P50/P95/P99 latency
quality metrics
language/analyzer needs
faceting/aggregation shape
retention
rebuild time
failure-domain objective
unit cost
team operating capacity
```

当前值可以是 unknown，但触发器不能是形容词。

### 外部检索的真正触发器

PostgreSQL 内搜索常在这些条件下很有优势：

- 数据与交易状态同库，提交即搜索；
- 过滤与关系 join 很重要；
- 规模在单 cluster 预算内；
- 搜索功能较集中；
- 团队不想承担额外索引同步系统；
- 正确性与删除要求高。

专用检索可能在以下证据出现后更合适：

```text
语言分析/相关性能力缺口
索引规模和构建时间越界
高扇出 facet/aggregation 压垮 OLTP
独立扩缩容或故障域是硬目标
多源统一搜索成为主需求
ANN 质量/规模/延迟无法满足
搜索团队能承担独立服务
```

切换前必须用同一 query set 与 relevance golden 比较，不能只比一条延迟。

### 搜索外置仍要保留 PostgreSQL fallback 分类

不是所有查询都能回退：

| 查询类 | 外部搜索故障时 |
|---|---|
| exact product ID | 直接 PostgreSQL |
| 简单关键词 | 可按容量回退 PostgreSQL |
| 复杂 facet | 返回有标签旧 generation 或失败 |
| semantic ANN | 可能没有等价 fallback |
| admin reconciliation | 延后，不影响交易 |

fallback 本身也需要容量演练。平时 1% 回源的 primary，未必承受 100% 搜索
流量。

### 流处理适合持续状态与事件时间计算

PostgreSQL + outbox + worker 可以完成大量异步任务。当需求升级为：

```text
高吞吐多分区事件
大量独立 consumer
长保留重放
event-time window
watermark / late data
stateful join
continuous aggregation
backpressure
```

专用流平台可能更合适。

但流系统不会自动给出业务 exactly-once。即使框架内部有 exactly-once
checkpoint，外部数据库、HTTP 副作用和人工操作仍需幂等与对账。应写：

```text
source delivery semantics
processing state semantics
sink effect semantics
recovery/replay semantics
```

而不是笼统宣布“端到端 exactly once”。

### 专用分析的触发器是工作负载边界

单节点或 offline replica 先做：

```text
better model and grain
statistics
index/BRIN
partition pruning
parallelism
summary/materialization
batching
resource isolation
```

当代表性证据仍显示：

- scan/compute 越过单节点；
- retention/压缩成本不可接受；
- 并发与查询形状冲突；
- 维护窗口不可满足；
- OLTP 和分析无法充分隔离；
- 横向扩展收益足以覆盖分布代价；

才比较 Citus、兼容分布式 PostgreSQL、列式引擎或湖仓。

第 17 章已经展示分布式新增问题：

```text
distribution key
skew
co-location
pushdown
cross-shard transaction
global uniqueness
rebalance
partial failure
remote authentication
version compatibility
```

专用系统不是“更高级的 PostgreSQL 参数”，而是新的数据与故障模型。

### 查询兼容和业务兼容要分开

候选声称“PostgreSQL compatible”时，至少核对：

```text
wire protocol
SQL grammar
type semantics
transaction/isolation
constraints
extensions
system catalogs
planner/explain
backup/restore
HA/failure behavior
driver/tooling
operational ownership
```

能连上 `psql` 只证明协议入口；能跑 SELECT 只证明一个查询子集。

### 用两道门防止过早与过晚拆分

进入外置评审：

```text
measured problem
representative workload
PostgreSQL baseline already optimized appropriately
service objective still missed
candidate benefit is causal
owner and budget exist
data contract and exit path written
```

继续留在 PostgreSQL：

```text
objective met with safe headroom
resource competition bounded
recovery/upgrade path passes
team can operate it
externalization tax exceeds measured benefit
```

门是双向的。不能因为过去留在 PostgreSQL，就永远拒绝新证据。

## 18.3.3 用数据所有权、时效和一致性决定分工 {#item-18-3-3}

### 第一步：把名词换成数据域

不要问“谁是 source of truth”，问：

```text
order business state?
payment settlement state?
publication intent?
message replay log?
product description?
search ranking projection?
media bytes?
media lifecycle metadata?
analytical dataset?
cache entry?
```

同一个订单事件流程可能有三个权威：

```text
order state          PostgreSQL
publish intent       PostgreSQL outbox
delivery/replay log  event bus
```

它们不冲突，因为 authority 的数据域不同。

### 第二步：为每个读者写时效

“最终一致”没有时钟。改写为：

```text
product cache TTL <= 300s, plus versioned invalidation
offline replica replay lag P99 <= proposed 60s
search indexing lag <= target before activation
analytics dataset labels its last complete watermark
order event publish P99 target set in chapter 24
```

还要说明超限：

```text
serve stale with label
fallback to authority
reject request
remove replica from routing
stop dataset publication
page operator
```

### 第三步：把一致性写成可观察规则

示例：

```text
cache version never replaces a newer PostgreSQL version
product deletion tombstone survives retry and generation swap
event_id stable across relay retry
consumer business effect deduplicated by event_id
analytics latest row selected by source commit position
object ready state requires matching immutable checksum
```

这些规则可以生成 negative test；“保持一致”无法测试。

### 第四步：定义失败矩阵

对每个跨系统合同列：

| 故障 | 写入结果 | 读取结果 | 重试 | 告警/对账 |
|---|---|---|---|---|
| PostgreSQL down | 是否拒绝 | cache 是否可陈旧读 | 谁重试 | 哪个 owner |
| external sink down | source 是否可提交 | 是否 fallback | backlog 边界 | lag |
| network partition | 双方如何判定 | 是否可能分歧 | 幂等 identity | reconciliation |
| projection corrupt | source 不受影响 | 停止/旧 generation | rebuild | checksum |
| credential expired | source 不受影响 | 外置路径失败 | 更新 secret | auth alert |

如果团队只讨论 happy path，外置系统只是把事故推迟到上线后设计。

### 第五步：写重建，而不只写备份

派生系统最强的恢复策略通常是从权威重建。但“可重建”必须证明：

```text
source retains enough history
consistent snapshot can be taken
change position can be bridged
schema/version transformation is reproducible
capacity allows rebuild within objective
live changes do not get lost
quality/checksum validation exists
generation can be atomically published
old generation can be rolled back
```

若不能满足，它就不是“随时可丢的副本”。

### 第六步：删除是一等数据流

创建与更新容易被关注，删除经常遗漏：

```text
cache TTL 让敏感字段继续存在
search rebuild 把已删文档复活
event replay 重新制造旧状态
lakehouse retained versions 留下受监管数据
object storage versioning 保留字节
backup retention 与删除政策冲突
```

每个合同必须有 deletion 字段，并与法律/业务保留策略对齐。删除不能简单理解
为对每个系统发一条 `DELETE`；它需要 tombstone、generation、审计和最终
验证。

### 第七步：退出路线与进入路线同时审批

一个可退出设计至少回答：

```text
如何停止新写入/投影
如何 drain 消费者
最后 position/manifest 在哪里
客户端怎样切回或切到替代服务
如何验证没有遗漏
旧数据何时删除
凭据与资源何时回收
谁宣布完成
```

本章蓝图包含五条退出原则。validator 的反例把 `exit_paths` 清空时必须返回：

```text
E_EXIT_PATH
```

### 一个通用决策表

| 问题 | 留在 PostgreSQL 倾向 | 外置倾向 |
|---|---|---|
| 是否需与业务写原子提交 | 强 | 需 outbox/saga |
| 是否频繁关系 join/filter | 强 | 需复制/反规范化 |
| 是否可容忍陈旧 | 弱 | 强 |
| 是否可从权威重建 | 非必要 | 必须 |
| 是否需独立扩缩/故障域 | 弱 | 强 |
| 单节点资源是否越界 | 可先优化/隔离 | 证据越界后强 |
| 专用语义是否关键 | 扩展可满足则强 | 缺口明确则强 |
| 团队是否能多系统值班 | 简化更强 | 能力充足才强 |
| 删除/对账是否成熟 | 简单 | 必须成熟 |
| 退出成本是否可接受 | 低 | 必须明示 |

表不会自动给出答案；它强迫评审把隐性成本说出来。

### 用合同 ID 连接能力

本章九项 capability 中，外置或可能外置的能力引用：

```text
lexical search        -> external-search-projection-v1
semantic search       -> external-search-projection-v1
spatiotemporal        -> analytics-export-v1
operational analytics -> analytics-export-v1
product cache         -> product-cache-v1
order delivery        -> order-events-v1
media bytes           -> product-media-v1
```

引用让我们可以回答：

- 某合同被哪些能力使用；
- 某合同删除后哪些蓝图断裂；
- 是否有无人引用的外置系统；
- 是否所有外置能力都有 owner；
- 外置触发器是否存在。

[`validate.py`](/labs/ch18/validate.py)
会拒绝悬空引用，但业务评审仍要判断合同内容是否真实可行。

### 不选择具体产品也是有效决定

本章故意不为 cache、event bus、object storage、search 或 lakehouse 指定
品牌。原因不是逃避，而是先稳定更长寿的合同：

```text
authority
delivery semantics
freshness
rebuild
security
exit
```

候选产品要在这些合同下比较。若先选产品，团队很容易把产品默认行为冒充业务
需求。

### 最终判断句式

一项成熟决定应能写成：

> 对数据域 X，由系统 A 保持权威；系统 B 以语义 Y 持有派生投影，目标新鲜度
> 为 Z，失败时按 F 退化，以 identity I 幂等并按 R 对账，可通过步骤 E 重建
> 和退出。只有触发器 T 出现且 gate G 通过后，B 才进入生产。

能写清这句话，才算划定边界。

---

[上一节：PostgreSQL 的强项与代价](../02/) · [返回本章目录](../) · [下一节：平台服务目录与多租户](../04/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
