惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Heimdal Security Blog
A
Arctic Wolf
K
Kaspersky official blog
V
Vulnerabilities – Threatpost
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Simon Willison's Weblog
Simon Willison's Weblog
L
LINUX DO - 热门话题
MongoDB | Blog
MongoDB | Blog
T
Threat Research - Cisco Blogs
D
Docker
爱范儿
爱范儿
T
Tenable Blog
C
Check Point Blog
B
Blog
C
Cisco Blogs
Vercel News
Vercel News
The Cloudflare Blog
T
Threatpost
NISL@THU
NISL@THU
T
Tor Project blog
V2EX - 技术
V2EX - 技术
P
Palo Alto Networks Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tailwind CSS Blog
G
GRAHAM CLULEY
P
Privacy & Cybersecurity Law Blog
SecWiki News
SecWiki News
博客园 - 司徒正美
S
Security @ Cisco Blogs
GbyAI
GbyAI
S
Secure Thoughts
Microsoft Security Blog
Microsoft Security Blog
The Register - Security
The Register - Security
Recorded Future
Recorded Future
Cloudbric
Cloudbric
Webroot Blog
Webroot Blog
N
News and Events Feed by Topic
Y
Y Combinator Blog
博客园_首页
T
Troy Hunt's Blog
The Hacker News
The Hacker News
雷峰网
雷峰网
Google DeepMind News
Google DeepMind News
U
Unit 42
AWS News Blog
AWS News Blog
PCI Perspectives
PCI Perspectives
V
Visual Studio Blog
博客园 - 聂微东
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 【Linux 网络子系统深度拆解】内核网络调优方法论:从基准测试到生产验证 排序基准测试:用数据说话
【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践
Liao Tonglang · 2026-04-13 · via 土法炼钢兴趣小组的算法知识备份

当一个电商系统同时需要处理用户下单(强一致事务)、商品搜索(全文检索)、用户关系推荐(图遍历)、实时监控指标(时序写入)以及运营日志分析(列式扫描)时,试图用一种数据库满足所有需求,几乎注定走向性能瓶颈或架构畸形。这正是多模持久化(Polyglot Persistence)要解决的核心命题:为不同的数据需求选择最合适的存储引擎,让每一类数据都跑在它最擅长的赛道上。

这个概念最早由 Martin Fowler 和 Pramod Sadalage 在 2011 年明确提出。其核心观点并不复杂——没有银弹数据库,不同数据模型天然适配不同的访问模式。然而,从理论到工程实践之间横亘着巨大的鸿沟:多套存储之间的数据一致性如何保证?运维复杂度如何控制?选型决策的依据到底是什么?

本文将从 CAP 定理与 PACELC 模型的实际应用出发,逐一分析五种主流存储引擎的适用场景,深入探讨多存储间的数据一致性挑战,并以 Uber 从 Postgres 迁移到 MySQL+Schemaless 的真实案例作为复盘素材,最终给出一套可落地的选型决策框架。

1.1 单一数据库的天花板

在系统规模较小的阶段,一个关系型数据库(Relational Database)往往足以支撑所有业务。用户表、订单表、商品表全部放在同一个 PostgreSQL 或 MySQL 实例中,通过 JOIN 完成各种查询,事务一致性由数据库引擎保证。这种架构简单、可靠、易于理解。

但随着业务增长,问题逐渐浮现:

  • 全文搜索:关系型数据库的 LIKE '%keyword%' 在百万级数据量下性能急剧恶化,即便加上全文索引,其分词能力和相关性排序也远不如专用搜索引擎。
  • 社交关系:用户之间的关注、好友、推荐关系本质上是图结构,用关系表做多级遍历需要大量递归 JOIN,性能呈指数级下降。
  • 时序数据:监控指标、IoT 传感器数据的写入量可达每秒数十万条,关系型数据库的行存储格式在这种场景下写入放大严重,且时间范围聚合查询效率低下。
  • 缓存热点:高并发读场景下,即便有读副本,数据库连接池仍然是瓶颈,需要一层内存缓存来吸收热点流量。
  • 会话与配置:结构松散、字段频繁变化的数据用固定 Schema 管理反而增加了迁移成本。

1.2 Polyglot Persistence 的核心思想

多模持久化的本质是一种架构策略:根据数据的访问模式(Access Pattern)、一致性需求(Consistency Requirement)和性能特征(Performance Profile),将不同类型的数据分配到最适合的存储引擎。

graph TB
    Client[客户端请求] --> Gateway[API Gateway]

    Gateway --> OrderService[订单服务]
    Gateway --> SearchService[搜索服务]
    Gateway --> RecommendService[推荐服务]
    Gateway --> MonitorService[监控服务]
    Gateway --> SessionService[会话服务]

    OrderService --> PostgreSQL[(PostgreSQL<br/>事务型数据)]
    SearchService --> Elasticsearch[(Elasticsearch<br/>全文检索)]
    RecommendService --> Neo4j[(Neo4j<br/>图数据)]
    MonitorService --> InfluxDB[(InfluxDB<br/>时序数据)]
    SessionService --> Redis[(Redis<br/>KV 缓存)]

    PostgreSQL -.->|CDC| Elasticsearch
    PostgreSQL -.->|CDC| Neo4j

    style PostgreSQL fill:#336791,color:#fff
    style Elasticsearch fill:#005571,color:#fff
    style Neo4j fill:#008CC1,color:#fff
    style InfluxDB fill:#22ADF6,color:#fff
    style Redis fill:#DC382D,color:#fff

上图展示了一个典型的多模持久化架构:订单数据存储在 PostgreSQL 中以保证 ACID 事务;商品搜索由 Elasticsearch 支撑;用户社交关系用 Neo4j 做图遍历;监控指标写入 InfluxDB;会话与热点缓存放在 Redis。不同存储之间通过变更数据捕获(Change Data Capture,CDC)进行数据同步。

1.3 收益与代价

收益显而易见:

  1. 每种数据都在最适合它的引擎上运行,性能最优化。
  2. 不同存储可以独立扩缩容,避免”木桶效应”。
  3. 技术栈灵活,可以引入领域最优的解决方案。

代价同样不可忽视:

  1. 运维复杂度倍增——需要维护多种数据库的监控、备份、升级流程。
  2. 数据一致性成为显式挑战——不再有单一事务边界保护。
  3. 团队技能要求提高——工程师需要理解不同存储引擎的特性与陷阱。
  4. 调试困难——一个业务流程可能跨越三四种存储,定位问题的链路更长。

这些代价是否值得,取决于业务规模和复杂度。对于初创公司的 MVP,单一数据库几乎总是正确的起点。但当系统演进到一定阶段,Polyglot Persistence 往往不是一个选择,而是一个必然。

二、CAP 定理在实际选型中的应用

2.1 CAP 的本质含义

CAP 定理由 Eric Brewer 在 2000 年提出,后由 Seth Gilbert 和 Nancy Lynch 在 2002 年给出形式化证明。其核心主张是:在一个分布式系统中,以下三个属性不可能同时完全满足,最多只能同时满足其中两个——

  • 一致性(Consistency):每次读操作都能返回最近一次写操作的结果,或者返回一个错误。这里的一致性特指线性一致性(Linearizability),比通常讨论的”最终一致性”要严格得多。
  • 可用性(Availability):每个请求都能在有限时间内收到一个非错误的响应——不保证返回的是最新数据。
  • 分区容忍性(Partition Tolerance):系统在网络分区(部分节点间的通信中断)发生时仍能继续运作。

2.2 常见误解

CAP 定理在工程实践中被广泛误用。以下几点需要澄清:

第一,CAP 不是一个静态的三选二。网络分区并非随时发生。在没有分区的正常运行状态下,系统可以同时提供一致性和可用性。CAP 定理只在分区真正发生时才强制你做出选择:是牺牲一致性(AP)还是牺牲可用性(CP)。

第二,CAP 中的”一致性”是线性一致性,这是最强的一致性模型。实际系统中使用的因果一致性(Causal Consistency)、读己之写(Read-your-writes)、最终一致性(Eventual Consistency)等弱一致性模型并不在 CAP 定理的讨论范围内。

第三,CAP 中的”可用性”要求每个节点都能响应——即使是少数派分区中的节点。这在实际系统中几乎不会被追求,因为我们通常接受少数派分区不可用。

2.3 CAP 对选型的实际影响

理解了这些细节之后,CAP 对数据库选型的指导意义可以归纳为以下问题框架:

问题 1:你的数据能否容忍在网络分区期间返回旧值?
  - 能 → 可以考虑 AP 系统(如 Cassandra、DynamoDB)
  - 不能 → 需要 CP 系统(如 ZooKeeper、etcd、Spanner)

问题 2:你的数据能否容忍在网络分区期间部分请求失败?
  - 能 → CP 系统可以满足需求
  - 不能 → 需要 AP 系统,但必须在应用层处理数据冲突

问题 3:网络分区在你的部署环境中有多常见?
  - 极少(单数据中心内部)→ CAP 的约束几乎不构成实际限制
  - 较多(跨数据中心、跨区域)→ 分区处理策略是架构设计的核心问题

2.4 实际系统中的 CAP 定位

下面这张表梳理了常见数据库在 CAP 维度上的定位:

数据库 CAP 定位 分区时的行为 适用场景
PostgreSQL(单节点) CA(无分布式) 不涉及分区 强一致事务
MySQL + InnoDB Cluster CP 少数派不可写 OLTP 业务
Cassandra AP 返回可能过期的数据 高写入、可容忍短暂不一致
DynamoDB AP(可调) 最终一致读/强一致读可选 弹性伸缩的 KV/Document
MongoDB(复制集) CP 主节点不可达时选举新主 文档型、灵活 Schema
etcd / ZooKeeper CP 少数派只读 配置中心、分布式锁
CockroachDB CP 少数派不可用 分布式强一致 SQL
Redis Cluster AP 异步复制可能丢数据 缓存、会话

需要注意的是,很多数据库提供了可调的一致性级别(Tunable Consistency)。例如 Cassandra 的读写一致性级别(ONEQUORUMALL)允许在同一个集群中为不同的查询选择不同的一致性保证。DynamoDB 的 ConsistentRead 参数也允许在最终一致读和强一致读之间切换。

// Cassandra:使用 QUORUM 级别保证强一致读
SimpleStatement statement = SimpleStatement.builder("SELECT * FROM orders WHERE order_id = ?")
    .addPositionalValues(orderId)
    .setConsistencyLevel(ConsistencyLevel.QUORUM)
    .build();
ResultSet rs = session.execute(statement);

// Cassandra:使用 ONE 级别获得最低延迟(可能读到旧数据)
SimpleStatement fastRead = SimpleStatement.builder("SELECT * FROM product_views WHERE product_id = ?")
    .addPositionalValues(productId)
    .setConsistencyLevel(ConsistencyLevel.ONE)
    .build();
# DynamoDB:强一致读
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Orders')

# 强一致读——保证返回最新写入的值
response = table.get_item(
    Key={'order_id': '12345'},
    ConsistentRead=True
)

# 最终一致读——延迟更低,但可能返回稍旧的数据
response = table.get_item(
    Key={'order_id': '12345'},
    ConsistentRead=False  # 默认值
)

三、PACELC 模型——比 CAP 更实用的选型框架

3.1 CAP 的不足

CAP 定理在工程选型中的最大局限是:它只描述了分区发生时的权衡,而现实中网络分区是小概率事件。在网络正常运行的绝大部分时间里,系统面临的核心权衡是延迟(Latency)与一致性(Consistency)之间的取舍。CAP 对此没有给出任何指导。

3.2 PACELC 的定义

Daniel Abadi 在 2012 年提出了 PACELC 模型来弥补这一不足。PACELC 的含义是:

如果发生了分区(Partition,P),系统需要在可用性(Availability,A)和一致性(Consistency,C)之间做出选择;否则(Else,E),在正常运行时,系统需要在延迟(Latency,L)和一致性(Consistency,C)之间做出选择。

用公式表达就是:

IF Partition THEN {Availability vs Consistency}
ELSE            {Latency vs Consistency}

这个模型的价值在于:它承认了在没有分区的情况下,保证强一致性通常需要更多的网络往返(例如多副本之间的同步确认),从而带来更高的延迟。这个权衡在日常运行中比分区时的权衡更常被触发,因此对选型决策的影响更大。

3.3 常见系统的 PACELC 分类

数据库 P 时选择(A/C) E 时选择(L/C) PACELC 分类 说明
Cassandra A L PA/EL 默认优先可用和低延迟
DynamoDB A L PA/EL 类似 Cassandra,可调
MongoDB C C PC/EC 默认强一致(可配置)
PostgreSQL(流复制) C C PC/EC 同步复制保一致性
CockroachDB C C PC/EC Raft 协议保强一致
Cosmos DB A(可调) L(可调) 可调 五种一致性级别
CRDT 系统 A L PA/EL 无冲突数据类型

3.4 PACELC 对选型的指导

PACELC 模型给出了一个更精确的选型框架。在实际项目中,可以按以下步骤使用:

flowchart TD
    Start[开始选型] --> Q1{数据是否需要<br/>跨区域部署?}
    Q1 -->|否| Q2{是否需要<br/>强一致事务?}
    Q1 -->|是| Q3{分区时优先<br/>可用性还是一致性?}

    Q2 -->|是| R1[单区域 CP 系统<br/>PostgreSQL / MySQL]
    Q2 -->|否| Q4{读延迟是否<br/>关键指标?}

    Q3 -->|可用性 A| Q5{正常时优先<br/>低延迟还是一致性?}
    Q3 -->|一致性 C| R2[跨区域 CP 系统<br/>Spanner / CockroachDB]

    Q4 -->|是| R3[内存 KV 存储<br/>Redis / Memcached]
    Q4 -->|否| R4[文档存储<br/>MongoDB / DynamoDB]

    Q5 -->|低延迟 L| R5[PA/EL 系统<br/>Cassandra / DynamoDB]
    Q5 -->|一致性 C| R6[PA/EC 系统<br/>较少见的组合]

    style Start fill:#2d3748,color:#fff
    style R1 fill:#38a169,color:#fff
    style R2 fill:#38a169,color:#fff
    style R3 fill:#38a169,color:#fff
    style R4 fill:#38a169,color:#fff
    style R5 fill:#38a169,color:#fff
    style R6 fill:#38a169,color:#fff

关键原则是:先确定部署拓扑(单区域还是跨区域),再确定一致性要求,最后在候选系统中根据延迟和吞吐需求做最终选择。跨区域部署的系统必须面对分区问题,因此 P 时的选择至关重要;单区域部署的系统主要面对的是 E 分支的延迟-一致性权衡。

3.5 一致性级别的光谱

现代分布式数据库很少提供非此即彼的选择。从强到弱,常见的一致性级别排列如下:

严格可序列化(Strict Serializable)
  ↓
线性一致性(Linearizable)
  ↓
顺序一致性(Sequential Consistency)
  ↓
因果一致性(Causal Consistency)
  ↓
读己之写(Read-your-writes)
  ↓
单调读(Monotonic Reads)
  ↓
最终一致性(Eventual Consistency)

Azure Cosmos DB 是在这方面做得最显式的系统,它提供了五种可选的一致性级别:强一致性(Strong)、有界陈旧性(Bounded Staleness)、会话一致性(Session)、一致前缀(Consistent Prefix)和最终一致性(Eventual)。每种级别在延迟、可用性和一致性保证之间提供不同的权衡点。

// Azure Cosmos DB:按操作级别配置一致性
const { CosmosClient } = require("@azure/cosmos");

const client = new CosmosClient({
    endpoint: process.env.COSMOS_ENDPOINT,
    key: process.env.COSMOS_KEY,
    // 账户级别默认一致性为 Session
    consistencyLevel: "Session"
});

const container = client.database("ecommerce").container("orders");

// 对于订单创建——使用强一致性
const { resource: order } = await container.items.create(
    { id: orderId, status: "created", items: cartItems },
    { consistencyLevel: "Strong" }
);

// 对于商品浏览计数——使用最终一致性以获得最低延迟
const { resource: viewCount } = await container.item(productId, productId).read(
    { consistencyLevel: "Eventual" }
);

四、五种存储引擎的适用场景与局限

4.1 关系型数据库(SQL)

关系型数据库是最成熟的存储引擎类别,以 PostgreSQL、MySQL、Oracle、SQL Server 为代表。其核心优势在于:

  • ACID 事务保证——适合金融、订单等对一致性要求极高的场景。
  • 丰富的 SQL 表达能力——复杂查询、多表 JOIN、窗口函数、CTE 递归查询等。
  • 强大的生态——ORM 框架、迁移工具、监控方案、DBA 人才储备。
  • Schema 约束——通过外键、检查约束等机制在数据库层面保证数据完整性。

局限性同样明显:

  • 水平扩展困难——分库分表(Sharding)是关系型数据库最大的痛点。虽然有 Vitess、Citus 等方案,但都带来了显著的架构复杂度。
  • 半结构化数据处理较弱——虽然 PostgreSQL 的 JSONB 和 MySQL 的 JSON 类型提供了一定的文档能力,但相比原生文档数据库仍有差距。
  • 写入吞吐有上限——单节点写入能力受限于磁盘 I/O 和锁竞争。
-- PostgreSQL:利用 CTE 和窗口函数做复杂分析
-- 查询每个用户最近 30 天内的订单金额排名
WITH recent_orders AS (
    SELECT
        user_id,
        order_id,
        total_amount,
        created_at,
        ROW_NUMBER() OVER (
            PARTITION BY user_id
            ORDER BY total_amount DESC
        ) AS amount_rank
    FROM orders
    WHERE created_at >= NOW() - INTERVAL '30 days'
      AND status = 'completed'
)
SELECT
    u.username,
    ro.order_id,
    ro.total_amount,
    ro.amount_rank
FROM recent_orders ro
JOIN users u ON u.id = ro.user_id
WHERE ro.amount_rank <= 3
ORDER BY u.username, ro.amount_rank;

4.2 键值存储(KV)

键值存储以 Redis、Memcached、Amazon DynamoDB(KV 模式)、etcd 为代表。其数据模型最为简单:一个键(Key)对应一个值(Value),支持的操作主要是 GET、SET、DELETE。

适用场景:

  • 缓存层:热点数据缓存、页面片段缓存、API 响应缓存。
  • 会话管理:用户登录会话、购物车临时数据。
  • 分布式锁:利用原子操作实现互斥访问。
  • 计数器与限流:原子递增操作实现精确计数和 Rate Limiting。
  • 特征标志(Feature Flag)存储:低延迟读取开关配置。

局限性:

  • 无法进行复杂查询——不支持 JOIN、聚合、范围扫描(部分系统如 Redis 的 Sorted Set 支持有限的范围操作)。
  • 数据模型过于简单——业务逻辑中的关系和约束需要在应用层实现。
  • 内存成本——纯内存 KV(如 Redis)的存储成本远高于磁盘存储。
# Redis:典型的缓存-穿透模式
import redis
import json
import hashlib

r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

def get_product(product_id: str) -> dict:
    cache_key = f"product:{product_id}"

    # 先查缓存
    cached = r.get(cache_key)
    if cached is not None:
        return json.loads(cached)

    # 缓存未命中,查数据库
    product = db.query("SELECT * FROM products WHERE id = %s", product_id)
    if product is None:
        # 缓存空值,防止缓存穿透
        r.setex(cache_key, 60, json.dumps(None))
        return None

    # 写入缓存,设置 TTL
    r.setex(cache_key, 3600, json.dumps(product))
    return product


def invalidate_product_cache(product_id: str):
    """当商品信息更新时,主动失效缓存"""
    cache_key = f"product:{product_id}"
    r.delete(cache_key)


# Redis:分布式锁实现
def acquire_lock(lock_name: str, timeout: int = 10) -> str | None:
    """获取分布式锁,返回锁 token"""
    token = hashlib.sha256(f"{lock_name}:{time.time()}".encode()).hexdigest()
    acquired = r.set(
        f"lock:{lock_name}",
        token,
        nx=True,     # 仅在不存在时设置
        ex=timeout    # 自动过期,防止死锁
    )
    return token if acquired else None


def release_lock(lock_name: str, token: str) -> bool:
    """释放分布式锁,使用 Lua 脚本保证原子性"""
    lua_script = """
    if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
    else
        return 0
    end
    """
    result = r.eval(lua_script, 1, f"lock:{lock_name}", token)
    return result == 1

4.3 文档数据库(Document)

文档数据库以 MongoDB、CouchDB、Amazon DynamoDB(文档模式)、Firestore 为代表。其核心特征是以 JSON/BSON 格式的文档作为基本存储单元,同一集合中的文档不需要遵循相同的 Schema。

适用场景:

  • 内容管理系统(CMS):文章、评论、用户生成内容等半结构化数据。
  • 产品目录:不同品类的商品有不同的属性集合(手机有屏幕尺寸,服装有尺码颜色)。
  • 用户画像:每个用户的属性集合可能不同,且频繁新增。
  • 事件溯源(Event Sourcing):事件的负载结构随版本演化。

局限性:

  • 多文档事务支持较晚且性能较差——MongoDB 从 4.0 版本开始支持多文档 ACID 事务,但其性能和成熟度仍不如关系型数据库。
  • JOIN 能力有限——$lookup 聚合操作相比 SQL JOIN 在表达能力和性能上都有差距。
  • 数据冗余——为了避免 JOIN,文档数据库鼓励数据嵌套和反范式化(Denormalization),这导致更新一致性成为挑战。
// MongoDB:利用灵活 Schema 存储不同品类的商品
db.products.insertMany([
    {
        _id: "prod_001",
        name: "iPhone 15 Pro",
        category: "smartphone",
        price: 7999,
        // 手机特有属性
        specs: {
            screenSize: "6.1 inch",
            chipset: "A17 Pro",
            storage: ["128GB", "256GB", "512GB", "1TB"],
            camera: {
                main: "48MP",
                ultrawide: "12MP",
                telephoto: "12MP"
            }
        },
        reviews: [
            { userId: "u_100", rating: 5, text: "非常好用", createdAt: new Date() }
        ]
    },
    {
        _id: "prod_002",
        name: "Patagonia 冲锋衣",
        category: "outdoor_clothing",
        price: 2499,
        // 服装特有属性
        specs: {
            material: "GORE-TEX",
            waterproof: true,
            breathability: "25000g/m2/24hr",
            sizes: ["S", "M", "L", "XL"],
            colors: ["黑色", "藏青", "橄榄绿"]
        },
        // 服装有尺码表,手机没有
        sizeChart: {
            S: { chest: "96cm", length: "68cm" },
            M: { chest: "100cm", length: "70cm" },
            L: { chest: "104cm", length: "72cm" },
            XL: { chest: "108cm", length: "74cm" }
        }
    }
]);

// 聚合管道:按品类统计平均评分和商品数量
db.products.aggregate([
    { $unwind: "$reviews" },
    { $group: {
        _id: "$category",
        avgRating: { $avg: "$reviews.rating" },
        productCount: { $addToSet: "$_id" },
        totalReviews: { $sum: 1 }
    }},
    { $project: {
        category: "$_id",
        avgRating: { $round: ["$avgRating", 1] },
        productCount: { $size: "$productCount" },
        totalReviews: 1
    }},
    { $sort: { avgRating: -1 } }
]);

4.4 图数据库(Graph)

图数据库以 Neo4j、Amazon Neptune、JanusGraph、TigerGraph 为代表。其数据模型由节点(Node)、边(Edge)和属性(Property)组成,天然适合表达实体之间的关系。

适用场景:

  • 社交网络:好友关系、关注链、共同好友推荐。
  • 知识图谱:实体关系建模、语义搜索。
  • 欺诈检测:通过关联分析发现可疑的资金流转路径。
  • 权限管理:基于角色的访问控制(RBAC),用户-角色-权限的继承关系。
  • 推荐引擎:基于协同过滤的”购买了这个商品的人还购买了”。

局限性:

  • 水平扩展挑战——图的遍历操作天然需要跨节点通信,分布式图数据库的性能远不如单机。
  • 不适合大批量写入——图数据库的写入通常需要维护索引和邻接信息,吞吐不如 KV 或列式存储。
  • 生态和人才相对小众——图查询语言(Cypher、Gremlin)的学习曲线较陡。
  • 全量扫描性能差——适合局部遍历,不适合全表聚合。
// Neo4j Cypher:社交推荐——找到二度好友中共同好友最多的人
MATCH (me:User {id: 'user_123'})-[:FOLLOWS]->(friend:User)-[:FOLLOWS]->(fof:User)
WHERE NOT (me)-[:FOLLOWS]->(fof)
  AND me <> fof
WITH fof, COUNT(DISTINCT friend) AS mutual_friends
ORDER BY mutual_friends DESC
LIMIT 10
RETURN fof.name AS recommended_user,
       fof.id AS user_id,
       mutual_friends

// 欺诈检测:找到资金在 3 步内形成环路的账户
MATCH path = (a:Account)-[:TRANSFER*3..5]->(a)
WHERE ALL(r IN relationships(path) WHERE r.amount > 10000)
  AND ALL(r IN relationships(path)
      WHERE r.timestamp > datetime() - duration('P7D'))
WITH a, path,
     REDUCE(total = 0, r IN relationships(path) | total + r.amount) AS total_amount
WHERE total_amount > 100000
RETURN a.account_number AS suspicious_account,
       length(path) AS cycle_length,
       total_amount,
       [n IN nodes(path) | n.account_number] AS cycle_path
ORDER BY total_amount DESC

4.5 时序数据库(Time-series)

时序数据库以 InfluxDB、TimescaleDB、Prometheus、QuestDB、TDengine 为代表。其核心特征是为带时间戳的数据优化了写入和时间范围查询性能。

适用场景:

  • 基础设施监控:CPU、内存、磁盘、网络等指标。
  • 应用性能监控(APM):请求延迟、错误率、吞吐量。
  • IoT 数据采集:传感器读数、设备状态。
  • 金融行情:股票价格、交易量的时间序列。
  • 业务指标:DAU/MAU、转化率、收入等运营数据的时间维度分析。

局限性:

  • 不适合频繁更新——时序数据通常是追加写入(Append-only),修改历史数据的性能较差。
  • 查询模式有限——主要优化时间范围聚合,不支持复杂的关联查询。
  • 数据生命周期管理复杂——需要配置保留策略(Retention Policy)和降采样(Downsampling)来控制存储成本。
-- TimescaleDB:利用 PostgreSQL 扩展实现时序数据存储
-- TimescaleDB 的优势在于完全兼容 SQL 生态

-- 创建普通表
CREATE TABLE metrics (
    time        TIMESTAMPTZ NOT NULL,
    host        TEXT        NOT NULL,
    metric_name TEXT        NOT NULL,
    value       DOUBLE PRECISION,
    tags        JSONB
);

-- 转换为超表(Hypertable),按时间自动分片
SELECT create_hypertable('metrics', 'time');

-- 创建复合索引
CREATE INDEX idx_metrics_host_time ON metrics (host, time DESC);

-- 写入指标数据
INSERT INTO metrics (time, host, metric_name, value, tags)
VALUES
    (NOW(), 'web-01', 'cpu_usage', 72.5, '{"region": "us-east-1"}'),
    (NOW(), 'web-01', 'mem_usage', 85.3, '{"region": "us-east-1"}'),
    (NOW(), 'web-02', 'cpu_usage', 45.1, '{"region": "us-west-2"}');

-- 时间桶聚合:每 5 分钟的平均 CPU 使用率
SELECT
    time_bucket('5 minutes', time) AS bucket,
    host,
    AVG(value) AS avg_cpu,
    MAX(value) AS max_cpu,
    MIN(value) AS min_cpu
FROM metrics
WHERE metric_name = 'cpu_usage'
  AND time > NOW() - INTERVAL '1 hour'
GROUP BY bucket, host
ORDER BY bucket DESC, host;

-- 配置保留策略:自动删除 30 天前的数据
SELECT add_retention_policy('metrics', INTERVAL '30 days');

-- 配置连续聚合:自动维护按小时聚合的物化视图
CREATE MATERIALIZED VIEW metrics_hourly
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', time) AS bucket,
    host,
    metric_name,
    AVG(value) AS avg_value,
    MAX(value) AS max_value,
    COUNT(*) AS sample_count
FROM metrics
GROUP BY bucket, host, metric_name;

4.6 五种引擎的综合对比

维度 SQL KV Document Graph Time-series
数据模型 表/行/列 键-值对 JSON/BSON 文档 节点/边/属性 时间戳+指标+标签
典型查询 复杂 JOIN,聚合 点查 GET/SET 文档内嵌套查询 图遍历,路径查找 时间范围聚合
写入吞吐 中等 极高 中低 极高
读取延迟 中等(取决于索引) 极低(亚毫秒) 低(局部遍历) 低(时间范围)
事务支持 完整 ACID 单键原子 有限多文档事务 有限 无/弱
水平扩展 困难 容易 较容易 困难 较容易
Schema 灵活性 低(固定 Schema) 无 Schema 高(动态 Schema) 中等 低(固定指标结构)
生态成熟度 最高 中等 中等
运维复杂度 中等 中等 中等
代表产品 PostgreSQL, MySQL Redis, etcd MongoDB, DynamoDB Neo4j, Neptune InfluxDB, TimescaleDB

五、多存储间的数据一致性挑战

当系统采用 Polyglot Persistence 架构后,最棘手的工程问题之一就是多存储间的数据一致性。一条订单数据可能需要同时存在于 PostgreSQL(主存储)、Elasticsearch(搜索索引)和 Redis(缓存)中。如何保证这三个副本之间的一致性?

5.1 双写问题(Dual Write Problem)

最直觉的方案是在应用层依次写入多个存储:

// 反模式:应用层双写
@Transactional
public void createOrder(Order order) {
    // 步骤 1:写入 PostgreSQL
    orderRepository.save(order);

    // 步骤 2:写入 Elasticsearch
    elasticsearchClient.index(order);  // 如果这里失败了呢?

    // 步骤 3:写入 Redis 缓存
    redisTemplate.opsForValue().set(
        "order:" + order.getId(),
        objectMapper.writeValueAsString(order)
    );
}

这段代码存在严重的一致性风险:

  1. 如果步骤 2 失败,PostgreSQL 中有数据但 Elasticsearch 中没有——用户搜索不到刚创建的订单。
  2. 如果步骤 2 成功但步骤 3 失败,Redis 缓存中没有最新数据——后续读取可能命中旧缓存。
  3. 即使加上重试逻辑,两个写操作之间也存在时间窗口——在这个窗口内数据是不一致的。
  4. @Transactional 注解只能保护 PostgreSQL 的事务,无法将 Elasticsearch 和 Redis 纳入事务边界。

5.2 变更数据捕获(Change Data Capture,CDC)

CDC 是解决双写问题的核心方案。其思想是:只写入主存储(Source of Truth),然后通过捕获主存储的变更日志(如 MySQL 的 binlog、PostgreSQL 的 WAL)来驱动下游存储的更新。

应用层 → 只写 PostgreSQL → PostgreSQL WAL
                                    ↓
                            Debezium(CDC 工具)
                                    ↓
                              Kafka Topic
                             ↙         ↘
                    Elasticsearch      Redis
                    Consumer          Consumer

这种架构的优势在于:

  • 应用层只需要关心主存储的写入,逻辑简单。
  • 下游消费者可以独立部署、独立扩缩容、独立重试。
  • 基于日志的同步天然保证了事件的顺序性。
  • 如果下游消费者暂时不可用,事件会在 Kafka 中积压,待恢复后自动追赶。

以 Debezium 为例的 CDC 配置:

{
    "name": "postgres-connector",
    "config": {
        "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
        "database.hostname": "postgres-primary",
        "database.port": "5432",
        "database.user": "debezium",
        "database.password": "${DEBEZIUM_PASSWORD}",
        "database.dbname": "ecommerce",
        "database.server.name": "ecommerce-db",
        "table.include.list": "public.orders,public.products,public.users",
        "plugin.name": "pgoutput",
        "slot.name": "debezium_slot",
        "publication.name": "debezium_publication",
        "tombstones.on.delete": "true",
        "key.converter": "org.apache.kafka.connect.json.JsonConverter",
        "value.converter": "org.apache.kafka.connect.json.JsonConverter"
    }
}

下游 Elasticsearch 的消费者实现:

# Kafka 消费者:将 CDC 事件同步到 Elasticsearch
from kafka import KafkaConsumer
from elasticsearch import Elasticsearch
import json

consumer = KafkaConsumer(
    'ecommerce-db.public.orders',
    bootstrap_servers=['kafka-1:9092', 'kafka-2:9092'],
    group_id='es-sync-orders',
    auto_offset_reset='earliest',
    enable_auto_commit=False,
    value_deserializer=lambda m: json.loads(m.decode('utf-8'))
)

es = Elasticsearch(['http://es-node-1:9200'])

for message in consumer:
    payload = message.value['payload']
    op = payload['op']  # c=create, u=update, d=delete

    if op in ('c', 'u'):
        after = payload['after']
        es.index(
            index='orders',
            id=str(after['id']),
            document={
                'order_id': after['id'],
                'user_id': after['user_id'],
                'status': after['status'],
                'total_amount': after['total_amount'],
                'created_at': after['created_at'],
                'updated_at': after['updated_at']
            }
        )
    elif op == 'd':
        before = payload['before']
        es.delete(index='orders', id=str(before['id']), ignore=[404])

    consumer.commit()

5.3 事务发件箱模式(Transactional Outbox Pattern)

CDC 基于数据库日志的方案虽然可靠,但配置和运维成本较高。事务发件箱模式(Transactional Outbox)是一种更轻量的替代方案,其思想是:在同一个数据库事务中,既写入业务数据,也写入一条”待发送事件”记录到发件箱表。然后由一个后台轮询进程(或 CDC)读取发件箱表,将事件发布到消息队列。

-- 发件箱表结构
CREATE TABLE outbox_events (
    id              BIGSERIAL PRIMARY KEY,
    aggregate_type  TEXT NOT NULL,
    aggregate_id    TEXT NOT NULL,
    event_type      TEXT NOT NULL,
    payload         JSONB NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    published_at    TIMESTAMPTZ,
    retry_count     INT NOT NULL DEFAULT 0
);

CREATE INDEX idx_outbox_unpublished ON outbox_events (created_at)
    WHERE published_at IS NULL;
// 在同一个事务中写入业务数据和发件箱事件
@Transactional
public Order createOrder(CreateOrderRequest request) {
    // 写入订单
    Order order = new Order();
    order.setUserId(request.getUserId());
    order.setItems(request.getItems());
    order.setTotalAmount(calculateTotal(request.getItems()));
    order.setStatus(OrderStatus.CREATED);
    orderRepository.save(order);

    // 在同一个事务中写入发件箱
    OutboxEvent event = new OutboxEvent();
    event.setAggregateType("Order");
    event.setAggregateId(order.getId().toString());
    event.setEventType("OrderCreated");
    event.setPayload(objectMapper.valueToTree(order));
    outboxRepository.save(event);

    return order;
}
# 后台发件箱轮询器
import time
import psycopg2
from kafka import KafkaProducer
import json

producer = KafkaProducer(
    bootstrap_servers=['kafka-1:9092'],
    value_serializer=lambda v: json.dumps(v).encode('utf-8'),
    acks='all'
)

def poll_outbox():
    conn = psycopg2.connect(dsn="postgresql://...")
    while True:
        with conn.cursor() as cur:
            # 获取未发布的事件(加锁防止并发)
            cur.execute("""
                SELECT id, aggregate_type, aggregate_id, event_type, payload
                FROM outbox_events
                WHERE published_at IS NULL
                ORDER BY created_at
                LIMIT 100
                FOR UPDATE SKIP LOCKED
            """)
            rows = cur.fetchall()

            for row in rows:
                event_id, agg_type, agg_id, event_type, payload = row
                topic = f"events.{agg_type.lower()}"

                # 发送到 Kafka
                future = producer.send(
                    topic,
                    key=agg_id.encode('utf-8'),
                    value={
                        'event_id': event_id,
                        'event_type': event_type,
                        'aggregate_type': agg_type,
                        'aggregate_id': agg_id,
                        'payload': payload
                    }
                )
                future.get(timeout=10)

                # 标记为已发布
                cur.execute(
                    "UPDATE outbox_events SET published_at = NOW() WHERE id = %s",
                    (event_id,)
                )

            conn.commit()
        time.sleep(1)  # 轮询间隔

5.4 Saga 模式与最终一致性

当多个存储的写入需要形成一个逻辑事务时(例如下单时需要扣库存、创建支付单、发送通知),Saga 模式提供了一种基于补偿操作的最终一致性方案。

Saga 有两种实现方式:

  • 编排式(Choreography):每个服务监听前一步的事件,自行决定是否执行和补偿。去中心化,但流程难以追踪。
  • 协调式(Orchestration):由一个 Saga 协调器(Orchestrator)统一管理流程,按顺序调用各步骤,失败时按逆序执行补偿。集中化,流程清晰但协调器本身成为单点。
// Go:Saga 协调器的简化实现
package saga

import (
    "context"
    "fmt"
    "log"
)

type Step struct {
    Name       string
    Execute    func(ctx context.Context, state map[string]any) error
    Compensate func(ctx context.Context, state map[string]any) error
}

type Saga struct {
    Steps []Step
}

func (s *Saga) Run(ctx context.Context, state map[string]any) error {
    var completedSteps []Step

    for _, step := range s.Steps {
        log.Printf("执行步骤: %s", step.Name)
        if err := step.Execute(ctx, state); err != nil {
            log.Printf("步骤 %s 失败: %v,开始补偿", step.Name, err)
            // 按逆序执行补偿
            for i := len(completedSteps) - 1; i >= 0; i-- {
                cs := completedSteps[i]
                log.Printf("补偿步骤: %s", cs.Name)
                if compErr := cs.Compensate(ctx, state); compErr != nil {
                    log.Printf("补偿步骤 %s 失败: %v", cs.Name, compErr)
                    return fmt.Errorf("saga 补偿失败于步骤 %s: %w", cs.Name, compErr)
                }
            }
            return fmt.Errorf("saga 在步骤 %s 中止: %w", step.Name, err)
        }
        completedSteps = append(completedSteps, step)
    }

    log.Println("saga 全部步骤执行成功")
    return nil
}

// 使用示例:创建订单的 Saga
func NewCreateOrderSaga(
    orderSvc OrderService,
    inventorySvc InventoryService,
    paymentSvc PaymentService,
) *Saga {
    return &Saga{
        Steps: []Step{
            {
                Name: "创建订单",
                Execute: func(ctx context.Context, state map[string]any) error {
                    orderID, err := orderSvc.Create(ctx, state["items"].([]Item))
                    if err != nil {
                        return err
                    }
                    state["order_id"] = orderID
                    return nil
                },
                Compensate: func(ctx context.Context, state map[string]any) error {
                    return orderSvc.Cancel(ctx, state["order_id"].(string))
                },
            },
            {
                Name: "扣减库存",
                Execute: func(ctx context.Context, state map[string]any) error {
                    return inventorySvc.Reserve(ctx, state["items"].([]Item))
                },
                Compensate: func(ctx context.Context, state map[string]any) error {
                    return inventorySvc.Release(ctx, state["items"].([]Item))
                },
            },
            {
                Name: "创建支付单",
                Execute: func(ctx context.Context, state map[string]any) error {
                    paymentID, err := paymentSvc.Charge(ctx, state["order_id"].(string), state["amount"].(float64))
                    if err != nil {
                        return err
                    }
                    state["payment_id"] = paymentID
                    return nil
                },
                Compensate: func(ctx context.Context, state map[string]any) error {
                    return paymentSvc.Refund(ctx, state["payment_id"].(string))
                },
            },
        },
    }
}

六、案例分析:Uber 从 Postgres 到 MySQL+Schemaless

6.1 背景

2016 年 7 月,Uber 工程团队发表了一篇引发广泛讨论的文章——“Why Uber Engineering Switched from Postgres to MySQL”。这篇文章详细阐述了 Uber 在早期使用 PostgreSQL 时遇到的痛点,以及最终迁移到 MySQL+Schemaless 架构的过程。这个案例是 Polyglot Persistence 思想在超大规模系统中的一次深刻实践。

6.2 PostgreSQL 时代的痛点

Uber 的后端系统最初建立在 Python + PostgreSQL 的技术栈之上。随着业务的爆发式增长,他们遇到了以下关键问题:

第一,写放大(Write Amplification)。PostgreSQL 使用多版本并发控制(MVCC),每次 UPDATE 操作不是原地修改,而是创建一条新的行版本(Tuple),旧版本标记为”死元组”(Dead Tuple)。这意味着一次逻辑更新可能导致多次物理写入——不仅要写入新数据行,还要更新所有引用该行的索引。

在 Uber 的行程(Trip)表中,一条行程记录有大量索引(按用户、按司机、按城市、按时间等),每次更新行程状态都触发所有索引的更新。这在高峰期导致了严重的 I/O 瓶颈。

第二,复制效率问题。PostgreSQL 的流复制(Streaming Replication)传输的是 WAL(Write-Ahead Log)记录。WAL 记录包含物理级别的变更信息(哪个页面的哪个偏移量被修改了),这意味着:

  • 主从之间必须运行完全相同的 PostgreSQL 大版本。
  • 无法跨大版本进行滚动升级——必须停机或使用逻辑复制(当时不够成熟)。
  • 从库需要回放所有的物理变更,包括前面提到的写放大带来的额外写入。

第三,表膨胀与 VACUUM 压力。死元组需要由 VACUUM 进程回收。在高写入场景下,VACUUM 可能跟不上死元组的生成速度,导致表和索引不断膨胀。膨胀的表意味着更多的磁盘 I/O、更大的缓存占用和更差的查询性能。autovacuum 的调优本身就是一个复杂的运维课题。

第四,数据损坏问题。Uber 的文章还提到了在 PostgreSQL 9.2 版本上遇到的一些数据损坏事件,特别是在主从切换(Failover)过程中。虽然这些问题在后续版本中已经修复,但在当时给团队留下了深刻印象。

6.3 选择 MySQL+InnoDB 的理由

经过评估,Uber 决定将核心存储迁移到 MySQL+InnoDB。选择 MySQL 的关键原因包括:

第一,InnoDB 的更新机制不同。InnoDB 使用聚簇索引(Clustered Index),数据按主键顺序存储。UPDATE 操作是原地修改(In-place Update),旧版本数据写入 UNDO 日志而不是创建新的行版本。二级索引(Secondary Index)存储的是主键值而不是行指针,因此当非索引列更新时,二级索引不需要变动。

这一点对 Uber 的场景至关重要:行程记录的大多数更新只涉及状态字段的变化(如从”进行中”变为”已完成”),而不涉及索引列。在 InnoDB 中,这种更新只需要修改数据页和 UNDO 日志,不需要触动二级索引。

第二,MySQL 的复制基于 binlog(逻辑日志),包含行级别的变更信息(而不是物理页面变更)。这带来了几个优势:

  • 从库可以运行不同的 MySQL 版本,方便滚动升级。
  • binlog 可以被外部系统消费(正是后来 CDC 架构的基础)。
  • 复制效率更高,因为只传输逻辑变更而不是物理变更。

第三,MySQL 在大规模运维方面有更成熟的工具链——pt-online-schema-change(在线 DDL)、Orchestrator(自动故障切换)、gh-ost(GitHub 的在线迁移工具)等。

6.4 Schemaless:Uber 自研的文档层

仅仅迁移到 MySQL 并不能解决所有问题。Uber 的业务数据结构变化频繁,且不同城市、不同业务线的数据字段可能不同。为此,Uber 在 MySQL 之上构建了一层名为 Schemaless 的存储抽象层。

Schemaless 的核心设计理念:

  • 每条记录存储为一个不可变的 JSON 文档单元(Cell),由 (row_key, column_name, ref_key) 三元组唯一标识。
  • row_key 是行的唯一标识(通常是 UUID)。
  • column_name 是列族的名称(类似 HBase 的 Column Family)。
  • ref_key 是版本号,单调递增,每次更新都生成一个新的 Cell 而不是覆盖旧的。
  • 底层数据存储在 MySQL 的宽表中,每一行对应一个 Cell。
-- Schemaless 底层 MySQL 表的简化结构
CREATE TABLE cells (
    row_key     VARBINARY(36) NOT NULL,
    column_name VARCHAR(64)   NOT NULL,
    ref_key     BIGINT        NOT NULL,
    body        MEDIUMBLOB    NOT NULL,  -- JSON 文档,GZIP 压缩
    created_at  DATETIME(6)   NOT NULL,
    PRIMARY KEY (row_key, column_name, ref_key),
    INDEX idx_column_ref (column_name, ref_key)
) ENGINE=InnoDB;

这种设计带来了几个关键好处:

  1. Schema 演化无痛——新增字段只需要在 JSON 文档中加入,不需要 ALTER TABLE。
  2. 天然的事件溯源——每次变更都产生新的 Cell,历史版本自动保留。
  3. 基于 MySQL 的成熟生态——利用 MySQL 的复制、备份、监控等工具链。
  4. 水平分片——通过 row_key 的哈希实现数据在多个 MySQL 实例间的均匀分布。

6.5 迁移后的架构

迁移后,Uber 的数据层架构演变为一个典型的 Polyglot Persistence 系统:

Uber 后端服务(Go / Java)
        |
        ├── Schemaless(MySQL)── 核心业务数据(行程、用户、支付)
        |       |
        |       └── CDC → Kafka → 下游消费者
        |
        ├── Redis ── 缓存热点数据、会话、限流
        |
        ├── Elasticsearch ── 搜索、日志分析
        |
        ├── Cassandra ── 高写入量的非关键数据(位置轨迹历史等)
        |
        └── 自研时序系统(M3DB)── 监控指标

6.6 教训与反思

这次迁移给出了几条深刻的实践教训:

第一,数据库选型必须从工作负载(Workload)出发,而不是从数据库的”理论优势”出发。PostgreSQL 在很多场景下是优秀的数据库,但 Uber 的工作负载(高频更新、大量索引、海量数据、频繁 Schema 变更)恰好触发了它的短板。

第二,数据库的运维成熟度与技术特性同等重要。MySQL 在技术特性上并不比 PostgreSQL 先进,但其运维工具链在当时更加完善,这在超大规模运维中是决定性因素。

第三,Polyglot Persistence 是自然演进的结果。Uber 并不是一开始就设计了多存储架构,而是随着不同业务需求的出现,逐步引入了最适合的存储引擎。这种”按需引入”比”预先设计”更加稳健。

第四,自研中间层需要谨慎权衡。Schemaless 解决了 Uber 的特定问题,但维护一个自研存储抽象层的成本巨大。对于大多数公司来说,使用现成的文档数据库(如 DynamoDB 或 MongoDB)是更务实的选择。

需要补充的是,PostgreSQL 社区在 Uber 文章发表后做了大量回应,指出文章中的一些结论具有特定版本和配置的局限性。例如,PostgreSQL 后来的版本改进了 VACUUM 性能、引入了逻辑复制(Logical Replication)、支持了 JIT 编译等。数据库的选型需要基于当下最新版本的特性和基准测试,而不是历史上的问题。

七、选型决策框架与实践建议

7.1 决策矩阵

在实际项目中,可以通过以下维度矩阵来系统化地评估存储引擎的选择:

评估维度 权重 问题
数据模型匹配度 数据的天然结构是表格、文档、图还是时间序列?
访问模式 主要是点查、范围查、全文搜索还是图遍历?
一致性需求 是否需要强一致事务?最终一致是否可接受?
写入吞吐 峰值写入 QPS 是多少?是否有突发流量?
读取延迟 P99 延迟要求是多少?是否接受亚毫秒级延迟?
数据规模 存储总量预计多大?数据增长速率如何?
扩展需求 是否需要水平扩展?单机是否够用?
运维能力 团队是否有该存储引擎的运维经验?
生态工具 ORM 支持、监控方案、备份工具是否完善?
成本 许可证费用、托管服务价格、硬件需求
人才可获得性 市场上是否容易招到有经验的工程师?

7.2 分步决策流程

步骤一:识别数据类别。将系统中的数据按照访问模式分类。例如:

电商系统数据分类:
├── 事务型数据(订单、支付、库存)→ 强一致,中等写入,复杂查询
├── 用户画像(个人信息、偏好、标签)→ 灵活 Schema,中等读写
├── 搜索数据(商品搜索、订单搜索)→ 全文检索,倒排索引
├── 社交数据(关注关系、推荐)→ 图遍历,多跳查询
├── 缓存数据(热点商品、会话)→ 极低延迟读,可容忍丢失
├── 监控数据(接口延迟、错误率)→ 高频写入,时间范围聚合
└── 日志数据(操作日志、审计日志)→ 追加写入,批量分析

步骤二:为每类数据选择候选存储。根据前面的分析,为每类数据列出 2-3 个候选存储引擎。

步骤三:概念验证(Proof of Concept)。对候选方案进行基准测试。重点关注:

  • 在预期数据量下的读写延迟(P50/P95/P99)。
  • 在预期并发下的吞吐量。
  • 故障恢复时间(RTO)和数据丢失容忍度(RPO)。
  • 运维操作的复杂度(扩容、备份、版本升级)。
#!/bin/bash
# 使用 Redis 官方基准测试工具
redis-benchmark -h redis-server -p 6379 \
    -c 100 \          # 100 个并发连接
    -n 1000000 \      # 100 万次请求
    -d 256 \          # 256 字节的值
    -t set,get \      # 测试 SET 和 GET 操作
    --csv              # CSV 格式输出
# 简化的数据库基准测试框架
import time
import statistics
from contextlib import contextmanager

class BenchmarkResult:
    def __init__(self, operation: str, latencies: list[float]):
        self.operation = operation
        self.count = len(latencies)
        self.p50 = statistics.median(latencies)
        self.p95 = sorted(latencies)[int(len(latencies) * 0.95)]
        self.p99 = sorted(latencies)[int(len(latencies) * 0.99)]
        self.avg = statistics.mean(latencies)
        self.qps = self.count / sum(latencies) if latencies else 0

    def __repr__(self):
        return (
            f"{self.operation}: "
            f"count={self.count}, "
            f"p50={self.p50*1000:.2f}ms, "
            f"p95={self.p95*1000:.2f}ms, "
            f"p99={self.p99*1000:.2f}ms, "
            f"QPS={self.qps:.0f}"
        )


def benchmark_write(db_client, num_operations=10000):
    """通用写入基准测试"""
    latencies = []
    for i in range(num_operations):
        start = time.perf_counter()
        db_client.write(
            key=f"bench_{i}",
            value={"id": i, "data": "x" * 256, "timestamp": time.time()}
        )
        latencies.append(time.perf_counter() - start)
    return BenchmarkResult("write", latencies)


def benchmark_read(db_client, keys: list[str]):
    """通用读取基准测试"""
    latencies = []
    for key in keys:
        start = time.perf_counter()
        db_client.read(key=key)
        latencies.append(time.perf_counter() - start)
    return BenchmarkResult("read", latencies)

步骤四:评估运维复杂度。引入一个新的存储引擎意味着需要建立以下能力:

  • 监控告警——需要理解该引擎的核心指标(如 Redis 的内存碎片率、MongoDB 的 oplog 窗口、Elasticsearch 的分片健康状态)。
  • 备份恢复——确保有经过验证的备份和恢复流程。
  • 容量规划——理解该引擎的扩容方式和成本模型。
  • 故障处理——团队至少需要一个人有深入的调优和故障排查经验。

7.3 何时引入新存储

一个常见的问题是:什么时候应该引入新的存储引擎,而不是在现有存储上”将就”?

以下信号暗示可能需要引入专用存储:

  1. 为了支持某种查询模式,你在关系型数据库上做了大量的反范式化和冗余索引,维护成本越来越高。
  2. 某类查询的延迟已经无法通过索引优化和查询调优来满足 SLA 要求。
  3. 某类数据的写入量正在接近单机的 I/O 上限,而该数据库的水平扩展方案过于复杂或昂贵。
  4. 应用层为了弥补存储引擎的不足,引入了越来越多的”胶水代码”(如手工维护倒排索引、应用层缓存一致性逻辑)。

以下信号暗示不应该引入新存储:

  1. 团队规模过小,无法承担多套存储的运维开销。
  2. 现有存储通过简单的调优或增加资源即可满足需求。
  3. 引入新存储的唯一动机是”技术追新”或”业界流行”。
  4. 数据一致性要求极高,引入多存储会带来不可接受的一致性风险。

7.4 托管服务 vs 自建

在云计算时代,一个重要的选型维度是使用云厂商的托管服务还是自建集群:

维度 托管服务 自建集群
运维负担 低(云厂商负责) 高(需要专人)
定制化能力 低(受限于服务商提供的选项) 高(完全可控)
成本(小规模) 低(按需付费) 高(固定开销)
成本(大规模) 高(溢价明显) 低(规模效应)
数据主权 受限(数据在云厂商) 完全控制
迁移灵活性 低(锁定风险)
故障响应速度 取决于 SLA 取决于团队能力

对于大多数团队,建议的策略是:

  • 初期使用托管服务快速启动,避免在基础设施上花费过多时间。
  • 随着规模增长,对核心存储逐步评估自建的 ROI。
  • 避免使用云厂商的专有接口,尽量使用标准协议和开源兼容的接口(如 DynamoDB 的 API vs 标准的 Cassandra CQL),为未来的迁移保留灵活性。

7.5 渐进式迁移策略

当决定引入新存储或从旧存储迁移时,渐进式迁移(Strangler Fig Pattern)是最安全的方式:

# 渐进式迁移:双读策略
class ProductRepository:
    def __init__(self, mysql_client, mongodb_client, feature_flags):
        self.mysql = mysql_client
        self.mongodb = mongodb_client
        self.flags = feature_flags

    def get_product(self, product_id: str) -> dict:
        if self.flags.is_enabled("read_from_mongodb", product_id):
            # 新路径:从 MongoDB 读取
            product = self.mongodb.products.find_one({"_id": product_id})
            if product is None:
                # 回退到 MySQL(数据可能还未迁移完)
                product = self._read_from_mysql(product_id)
                if product:
                    # 补写到 MongoDB
                    self.mongodb.products.insert_one(product)
            return product
        else:
            # 旧路径:从 MySQL 读取
            return self._read_from_mysql(product_id)

    def _read_from_mysql(self, product_id: str) -> dict | None:
        cursor = self.mysql.cursor(dictionary=True)
        cursor.execute("SELECT * FROM products WHERE id = %s", (product_id,))
        return cursor.fetchone()


# 渐进式迁移:影子写入 + 比对验证
class DualWriteProductRepository:
    def __init__(self, mysql_client, mongodb_client, metrics):
        self.mysql = mysql_client
        self.mongodb = mongodb_client
        self.metrics = metrics

    def save_product(self, product: dict):
        # 主写入:MySQL(Source of Truth)
        self._write_to_mysql(product)

        # 影子写入:MongoDB(验证阶段)
        try:
            self.mongodb.products.replace_one(
                {"_id": product["id"]},
                product,
                upsert=True
            )
        except Exception as e:
            # 影子写入失败不影响主流程,仅记录指标
            self.metrics.increment("shadow_write_failure", tags={"target": "mongodb"})
            log.warning(f"MongoDB 影子写入失败: {e}")

    def verify_consistency(self, product_id: str) -> bool:
        """定期对比两个存储的数据是否一致"""
        mysql_product = self._read_from_mysql(product_id)
        mongo_product = self.mongodb.products.find_one({"_id": product_id})

        if mysql_product is None and mongo_product is None:
            return True

        is_consistent = self._deep_compare(mysql_product, mongo_product)
        if not is_consistent:
            self.metrics.increment("data_inconsistency", tags={"entity": "product"})
            log.error(
                f"数据不一致: product_id={product_id}, "
                f"mysql={mysql_product}, mongo={mongo_product}"
            )
        return is_consistent

八、总结

Polyglot Persistence 不是一种银弹,而是一种务实的架构策略。它承认了一个基本事实:不同的数据需求有不同的最优解,试图用一种存储引擎满足所有需求是一种不必要的妥协。

在实践中,采用 Polyglot Persistence 需要遵循以下原则:

  1. 从单一存储开始,按需引入。不要在系统初期就设计多存储架构。当某类数据的访问模式明确无法被现有存储高效支持时,再引入专用存储。

  2. 以 CAP/PACELC 为框架做选型。先明确部署拓扑和一致性需求,再在候选系统中选择。理解你的系统在分区时的行为和正常运行时的延迟-一致性权衡。

  3. 数据一致性是第一优先级。使用 CDC 或事务发件箱模式替代应用层双写。明确每个数据流的 Source of Truth。为不可避免的不一致设计检测和修复机制。

  4. 运维能力是选型的硬约束。一个团队不理解的数据库,再好的特性也无法发挥。选择团队能够驾驭的存储引擎,或者投资在团队培养上。

  5. 用渐进式迁移降低风险。通过影子写入、双读比对、灰度切换等方式,将迁移风险控制在可接受的范围内。

最后回到 Uber 的案例:他们的迁移并不是因为 PostgreSQL 不好,而是因为 PostgreSQL 不适合他们特定的工作负载。这正是 Polyglot Persistence 的精髓——没有最好的数据库,只有最合适的数据库。


上一篇:数据建模

下一篇:数据湖与数据仓库

参考资料

  1. Martin Fowler, Pramod Sadalage. “Polyglot Persistence.” martinfowler.com, 2011.
  2. Eric Brewer. “Towards Robust Distributed Systems.” ACM Symposium on Principles of Distributed Computing, 2000.
  3. Seth Gilbert, Nancy Lynch. “Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services.” ACM SIGACT News, 2002.
  4. Daniel Abadi. “Consistency Tradeoffs in Modern Distributed Database System Design.” IEEE Computer, 2012.
  5. Uber Engineering. “Why Uber Engineering Switched from Postgres to MySQL.” Uber Engineering Blog, 2016.
  6. Uber Engineering. “Schemaless: Uber Engineering’s Trip Store Using MySQL.” Uber Engineering Blog, 2016.
  7. Martin Kleppmann. “Designing Data-Intensive Applications.” O’Reilly Media, 2017.
  8. Debezium Documentation. “Change Data Capture for PostgreSQL.” debezium.io.
  9. Chris Richardson. “Microservices Patterns.” Manning Publications, 2018.
  10. Pat Helland. “Life beyond Distributed Transactions: an Apostate’s Opinion.” ACM Queue, 2016.

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计】Uber 架构演进:从单体到领域导向微服务

Uber 在 2010 年上线时只有一个 Python 单体应用,服务三个城市的出行需求。到 2020 年,这家公司运行着超过 4000 个微服务,覆盖出行、外卖、货运、金融等多条业务线,日均处理数千万次行程请求。这段十年的技术演进史,浓缩了单体拆分、微服务膨胀、治理回归三个阶段的完整教训。本文将从时间线出发,逐层拆解…

2026-04-13 · architecture

【系统架构设计】什么是软件架构:从代码结构到系统决策

软件架构的定义至少有三种主流理解,每种指向不同的关注点。本文对比 Booch、Fowler、Johnson 三家定义,分析架构决策的不可逆性特征,拆解 Krutchten 4+1 视图模型,回答一个核心问题:架构和设计的边界到底在哪里。

2026-04-13 · architecture

【系统架构设计】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计】架构决策与 ADR:如何做出可追溯的技术决策

口头约定的架构决策会在人员流动中丢失,会在争论中反复翻车。ADR(Architecture Decision Records)用一种轻量的文档格式,把每一个关键技术决策的背景、选项、理由和代价写下来,跟着代码一起版本管理。本文从 ADR 的三种主流格式讲到 Git 仓库中的实操管理,再拆解 Spotify 和 Uber 的工业实践。