18.3 明确替代边界
替代边界不是“PostgreSQL 对阵某产品”的功能打勾表。真正的决策单位是:
某一类数据
在某一类操作下
需要某种一致性/时效/规模/故障语义
由某个团队承担同一个系统可以对一类数据是权威,对另一类数据只是派生副本。
18.3.1 缓存、消息、对象存储与离线湖仓
缓存解决重复读取,不解决权威
缓存适合:
结果计算或读取昂贵
值允许在有限时间内陈旧
miss 可以回到权威
entry 可丢弃并重建
容量与淘汰可接受缓存不适合被默认为:
库存真相
支付状态真相
唯一事件日志
不可恢复业务数据
跨字段事务约束本章 product-cache-v1 的 authority:
{
"product_business_state": "postgresql",
"cache_entries": "cache"
}这不是文字游戏。假如缓存中 product.version=8,PostgreSQL 已是
version=10,规则必须保证版本 8 永远不能覆盖版本 10。
cache-aside 的完整状态机
常见读路径:
read cache
hit -> verify acceptable version/freshness -> return
miss -> bounded read from PostgreSQL
-> write versioned cache entry
-> return写路径不能只写成“更新 DB 后删缓存”。必须考虑:
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,缓存已经悄悄变成了数据库。
消息系统拥有投递,不拥有原事务
事件总线擅长:
异步解耦
一对多消费
保留与重放
按 partition 排序
消费者独立进度
流式处理但“先更新数据库,再发送消息”有经典双写:
DB commit, publish fail -> 状态变了,没有事件
publish success, DB fail -> 有事件,没有状态
retry publish -> 重复事件order-events-v1 选择 transactional outbox:
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”要写清谁至少一次
这几个语义不同:
relay may publish duplicate
broker may redeliver
consumer may process then crash before ack
business side effect may be non-idempotent稳定 event_id 是起点,不是终点。消费者需要把“已处理 identity”与业务
效果放在同一原子边界,或使用业务幂等键。
全局顺序通常也不应承诺。本章只承诺:
partition by order_id
no global order否则团队会为一个业务不需要的全序付出吞吐与可用性成本。
对象存储拥有大字节,数据库拥有元数据状态
把图片、视频和归档大对象直接塞入 PostgreSQL 并非绝对错误,但常见代价:
database/backup/WAL volume
cache 污染
复制与恢复时间
CDN/分段下载能力
对象生命周期与访问方式product-media-v1 采用逐域 authority:
object bytes -> object storage
object identity / owner / state /
size / checksum / active generation -> PostgreSQL可靠上传状态机可以是:
reserve metadata row (uploading)
-> upload immutable object version
-> verify size + checksum
-> finalize metadata (ready)
-> expose signed access失败处理:
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,所以显式状态、 幂等与对账比“调用顺序看起来正确”重要。
湖仓拥有分析投影,不拥有在线业务解释
离线湖仓适合:
超长保留
列式大扫描
批处理与多引擎消费
历史快照
低成本冷数据
复杂数据科学管线analytics-export-v1 的权威拆分:
operational business state -> PostgreSQL
analytical projection -> lakehouse每个数据集必须显示:
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 的一致性接缝
建立新投影通常需要:
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 超大规模检索、流处理与专用分析
“超大规模”必须换成阈值
以下句子都不能触发架构:
以后数据很多
搜索会很复杂
实时要求很高
分析师越来越多
行业都这么做需要换成可测量的问题:
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 预算内;
- 搜索功能较集中;
- 团队不想承担额外索引同步系统;
- 正确性与删除要求高。
专用检索可能在以下证据出现后更合适:
语言分析/相关性能力缺口
索引规模和构建时间越界
高扇出 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 可以完成大量异步任务。当需求升级为:
高吞吐多分区事件
大量独立 consumer
长保留重放
event-time window
watermark / late data
stateful join
continuous aggregation
backpressure专用流平台可能更合适。
但流系统不会自动给出业务 exactly-once。即使框架内部有 exactly-once checkpoint,外部数据库、HTTP 副作用和人工操作仍需幂等与对账。应写:
source delivery semantics
processing state semantics
sink effect semantics
recovery/replay semantics而不是笼统宣布“端到端 exactly once”。
专用分析的触发器是工作负载边界
单节点或 offline replica 先做:
better model and grain
statistics
index/BRIN
partition pruning
parallelism
summary/materialization
batching
resource isolation当代表性证据仍显示:
- scan/compute 越过单节点;
- retention/压缩成本不可接受;
- 并发与查询形状冲突;
- 维护窗口不可满足;
- OLTP 和分析无法充分隔离;
- 横向扩展收益足以覆盖分布代价;
才比较 Citus、兼容分布式 PostgreSQL、列式引擎或湖仓。
第 17 章已经展示分布式新增问题:
distribution key
skew
co-location
pushdown
cross-shard transaction
global uniqueness
rebalance
partial failure
remote authentication
version compatibility专用系统不是“更高级的 PostgreSQL 参数”,而是新的数据与故障模型。
查询兼容和业务兼容要分开
候选声称“PostgreSQL compatible”时,至少核对:
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 只证明一个查询子集。
用两道门防止过早与过晚拆分
进入外置评审:
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:
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 用数据所有权、时效和一致性决定分工
第一步:把名词换成数据域
不要问“谁是 source of truth”,问:
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?同一个订单事件流程可能有三个权威:
order state PostgreSQL
publish intent PostgreSQL outbox
delivery/replay log event bus它们不冲突,因为 authority 的数据域不同。
第二步:为每个读者写时效
“最终一致”没有时钟。改写为:
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还要说明超限:
serve stale with label
fallback to authority
reject request
remove replica from routing
stop dataset publication
page operator第三步:把一致性写成可观察规则
示例:
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,外置系统只是把事故推迟到上线后设计。
第五步:写重建,而不只写备份
派生系统最强的恢复策略通常是从权威重建。但“可重建”必须证明:
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若不能满足,它就不是“随时可丢的副本”。
第六步:删除是一等数据流
创建与更新容易被关注,删除经常遗漏:
cache TTL 让敏感字段继续存在
search rebuild 把已删文档复活
event replay 重新制造旧状态
lakehouse retained versions 留下受监管数据
object storage versioning 保留字节
backup retention 与删除政策冲突每个合同必须有 deletion 字段,并与法律/业务保留策略对齐。删除不能简单理解
为对每个系统发一条 DELETE;它需要 tombstone、generation、审计和最终
验证。
第七步:退出路线与进入路线同时审批
一个可退出设计至少回答:
如何停止新写入/投影
如何 drain 消费者
最后 position/manifest 在哪里
客户端怎样切回或切到替代服务
如何验证没有遗漏
旧数据何时删除
凭据与资源何时回收
谁宣布完成本章蓝图包含五条退出原则。validator 的反例把 exit_paths 清空时必须返回:
E_EXIT_PATH一个通用决策表
| 问题 | 留在 PostgreSQL 倾向 | 外置倾向 |
|---|---|---|
| 是否需与业务写原子提交 | 强 | 需 outbox/saga |
| 是否频繁关系 join/filter | 强 | 需复制/反规范化 |
| 是否可容忍陈旧 | 弱 | 强 |
| 是否可从权威重建 | 非必要 | 必须 |
| 是否需独立扩缩/故障域 | 弱 | 强 |
| 单节点资源是否越界 | 可先优化/隔离 | 证据越界后强 |
| 专用语义是否关键 | 扩展可满足则强 | 缺口明确则强 |
| 团队是否能多系统值班 | 简化更强 | 能力充足才强 |
| 删除/对账是否成熟 | 简单 | 必须成熟 |
| 退出成本是否可接受 | 低 | 必须明示 |
表不会自动给出答案;它强迫评审把隐性成本说出来。
用合同 ID 连接能力
本章九项 capability 中,外置或可能外置的能力引用:
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
会拒绝悬空引用,但业务评审仍要判断合同内容是否真实可行。
不选择具体产品也是有效决定
本章故意不为 cache、event bus、object storage、search 或 lakehouse 指定 品牌。原因不是逃避,而是先稳定更长寿的合同:
authority
delivery semantics
freshness
rebuild
security
exit候选产品要在这些合同下比较。若先选产品,团队很容易把产品默认行为冒充业务 需求。
最终判断句式
一项成熟决定应能写成:
对数据域 X,由系统 A 保持权威;系统 B 以语义 Y 持有派生投影,目标新鲜度 为 Z,失败时按 F 退化,以 identity I 幂等并按 R 对账,可通过步骤 E 重建 和退出。只有触发器 T 出现且 gate G 通过后,B 才进入生产。
能写清这句话,才算划定边界。
上一节:PostgreSQL 的强项与代价 · 返回本章目录 · 下一节:平台服务目录与多租户 · 查看全书目录 · 查看索引中心