数据库选型不是给产品排总分。脱离数据形状、查询路径、写入冲突、硬件、索引和持久化参数讨论“谁更快”,结论通常没有迁移价值。本文比较的是边界,并给出可以验证的决策过程。
一、先把四种产品放回各自的位置#
| 产品 | 首要模型 | 适合承担的责任 | 最常见的误用 |
|---|---|---|---|
| MySQL(InnoDB) | 关系模型、SQL | 通用事务系统、成熟 Web 业务、明确关系与约束 | 因为熟悉就忽略复杂查询、分片和 schema 演进成本 |
| PostgreSQL | 关系模型、SQL,扩展类型丰富 | 事务系统、复杂查询、JSONB、GIS、全文和扩展能力 | 把所有搜索、分析、队列和向量需求都塞进一个实例 |
| Redis | 内存数据结构 | 缓存、限流、会话、短队列、计数和协调 | 把“支持持久化”误解为可以直接替代核心账本 |
| MongoDB | BSON 文档 | 聚合边界清楚、结构可演进的文档数据 | 把所有关系内嵌,最终得到无界文档和大规模重复数据 |
最稳妥的默认起点通常不是“每种数据库都上一套”,而是一个主数据库。只有新的访问模式经过测量,确认主库难以经济地满足时,才增加缓存、搜索或文档存储。每增加一种数据库,就增加一套备份、监控、升级、权限、驱动和一致性故障模式。
二、选型前先写一份工作负载护照#
至少回答下面这些问题,再谈产品名称:
数据与约束#
- 核心实体是什么,实体之间有哪些一对一、一对多和多对多关系?
- 哪些约束必须由数据库保证,例如余额不能为负、订单号唯一、外键必须存在?
- 一次原子操作跨多少条记录、多少个集合或多少个分片?
- 文档字段是真的不稳定,还是团队尚未完成数据建模?
访问模式#
- 前五条读查询和写命令是什么?过滤、排序和聚合字段有哪些?
- 读写比是多少,写入是追加、更新热点行,还是批量导入?
- 延迟目标是 p95 还是平均值?峰值持续多久?
- 查询是否需要任意 JOIN、地理计算、全文、JSON 内部字段或时间范围扫描?
可靠性#
- RPO:故障时最多允许丢多少数据?
- RTO:多长时间内必须恢复服务?
- 是否允许读到旧数据?客户端重试会不会造成重复扣款或重复创建?
- 备份恢复到隔离环境需要多久,最近一次演练是什么时候?
运维条件#
- 团队是否会阅读执行计划、处理锁等待、复制延迟和磁盘增长?
- 能否承担副本集、分片、Sentinel/Cluster 或连接代理?
- 云托管服务能否满足地区、合规、版本和成本要求?
- 三年后迁移数据与业务逻辑的代价是多少?
没有这份护照,“MongoDB 适合海量写”“Redis 十万 QPS”“PostgreSQL 事务最强”都只是无法验收的口号。
三、数据模型决定大部分长期成本#
MySQL 与 PostgreSQL:把约束留在数据旁边#
关系数据库适合需要稳定身份、引用完整性和跨实体事务的数据。例如订单系统可以让数据库直接阻止孤立订单项和重复幂等键:
CREATE TABLE orders ( id BIGINT PRIMARY KEY, idempotency_key VARCHAR(80) NOT NULL UNIQUE, customer_id BIGINT NOT NULL, status VARCHAR(24) NOT NULL, total_cents BIGINT NOT NULL CHECK (total_cents >= 0));
CREATE TABLE order_items ( order_id BIGINT NOT NULL REFERENCES orders(id), line_no INTEGER NOT NULL, sku VARCHAR(64) NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0), PRIMARY KEY (order_id, line_no));应用校验仍然有价值,但数据库约束覆盖所有写入者,包括后台任务、修复脚本和未来服务。
MySQL 8 与 PostgreSQL 都支持事务、窗口函数、CTE、JSON、复制和成熟索引。真正的差异通常出现在细节:
- MySQL InnoDB 的主键是聚簇索引,二级索引叶子保存主键值;主键宽度会影响所有二级索引。
- PostgreSQL 以堆表和独立索引为基础,MVCC 产生的旧行版本需要 VACUUM 管理;可见性映射也影响 index-only scan。
- PostgreSQL 原生提供数组、range、JSONB、GIN、GiST、BRIN 等能力,并通过 PostGIS、pgvector 等扩展拓宽用途。
- MySQL 拥有广泛的托管、运维和应用生态;Vitess 等项目展示了 MySQL 在大规模分片下的工程路径。
这些差异要映射到具体查询和团队能力,不能压缩成“简单业务选 MySQL,复杂业务选 PostgreSQL”。
MongoDB:围绕一起读取和一起更新的数据建模#
MongoDB 的设计单位是文档。应当内嵌还是引用,取决于数据是否具有共同生命周期、是否经常一起读取,以及数组是否会无界增长。
一篇文章内嵌少量固定元数据通常自然;把所有评论永远塞进文章文档则会导致文档持续膨胀、更新争用和生命周期耦合。更合适的模型可能是:
// articles:文章本体与有界元数据{ _id: ObjectId("..."), title: "...", authorId: ObjectId("..."), tags: ["database", "architecture"], stats: { commentCount: 128 }}
// comments:独立增长、独立分页{ _id: ObjectId("..."), articleId: ObjectId("..."), authorId: ObjectId("..."), body: "...", createdAt: ISODate("...")}MongoDB 可以使用 schema validation,也支持单文档原子写和多文档事务。它的优势不是“不要 schema”,而是允许围绕聚合边界设计 schema,并按版本演进。跨文档事务越频繁、跨集合关系越复杂,就越应该重新评估关系模型是否更自然。
Redis:先选数据结构,再设计键空间#
Redis 不只是字符串缓存。Hash、Set、Sorted Set、Stream、Bitmap 和 HyperLogLog 对应不同访问模式。但它没有关系数据库那样的任意查询能力,键设计本身就是索引设计。
例如排行榜适合 Sorted Set,因为“按分数排序并取区间”是数据结构原生能力;如果需要按地区、年龄、时间和多种业务字段任意组合查询,继续堆键会把索引一致性转嫁给应用。
Redis 的许多命令在单个 shard 的命令执行路径上串行完成,因此单条命令具有清晰原子边界;这不代表整个 Redis 系统“只有一个线程”,也不代表长命令不会阻塞其他请求。大 Key、全量扫描、耗时 Lua/Functions 和慢网络输出仍然需要治理。
四、事务与一致性:不要只比较是否支持 ACID#
MySQL InnoDB#
InnoDB 支持 ACID、MVCC、行锁和外键,默认隔离级别通常是 REPEATABLE READ。范围更新和唯一性检查可能涉及 next-key lock;“写不同的行一定不互相阻塞”并不成立,索引选择和访问顺序会影响锁范围与死锁。
工程上要明确:
- 事务内是否包含网络调用或用户等待。
- 所有并发路径是否按相同顺序锁定记录。
- 死锁后客户端是否能安全重试。
- binlog、复制模式和 failover 是否满足一致性目标。
PostgreSQL#
PostgreSQL 默认隔离级别通常是 READ COMMITTED,也提供 repeatable read 与 serializable。Serializable 使用可串行化快照隔离检测危险结构,事务可能因序列化失败而需要重试。更高隔离级别不是一个无需代价的“更强开关”。
PostgreSQL 的 MVCC 允许读写并发,但旧版本清理、长事务、表膨胀和 transaction ID 生命周期成为重要运维主题。VACUUM 不是可选的定期优化,而是系统正常运行的一部分。
MongoDB#
MongoDB 单文档写入原子;多文档事务可覆盖副本集和分片集群,但事务会延长资源占用并增加协调成本。更成熟的用法是先把必须一起更新的数据建模到同一文档,再把多文档事务留给确实跨聚合的业务,而不是用事务修补任意拆分的 schema。
Read concern、write concern 和 read preference 共同决定客户端看到的数据与确认边界。只写一个 majority 标签不足以解释故障语义,还要结合拓扑、超时和重试行为。
Redis#
单条命令通常是原子的;MULTI/EXEC 将命令顺序执行,但不像关系数据库那样在运行时错误后自动回滚。乐观并发可以使用 WATCH,多步原子逻辑可使用 Lua 或 Redis Functions,但跨 Cluster slot 操作受分片边界限制。
如果业务要求“余额扣减、账本落库、审计记录”共同满足持久事务,Redis 不应因为命令快就成为唯一事实来源。它更适合保存可重建派生状态,或在明确持久化与故障模型后承担有限范围的数据责任。
五、索引与查询:写出访问路径再建索引#
不要为所有字段建索引。每个索引都增加写放大、缓存占用、备份体积和维护成本。
| 系统 | 需要重点理解的索引行为 |
|---|---|
| MySQL InnoDB | 联合索引左前缀、覆盖索引、二级索引回表、主键宽度 |
| PostgreSQL | B-tree、GIN/GiST/BRIN、partial/expression index、统计信息与 bitmap scan |
| MongoDB | compound/multikey/partial/TTL index、字段顺序、排序覆盖与 shard key |
| Redis | 键与数据结构就是主要访问路径;二级查询通常要自行维护或使用相应模块 |
PostgreSQL JSONB 与 MongoDB 文档不是同一选择题#
JSONB 适合关系实体中一部分稀疏或可扩展属性,同时保留 SQL、JOIN 和约束;MongoDB 适合整个聚合以文档为主要读写单位。两者都能索引 JSON 内部字段,但更新语义、存储布局、事务边界和扩展方式不同。
同样,不要因为 PostgreSQL 支持全文、向量和地理扩展,就默认一个实例同时承担 OLTP、搜索、RAG 和分析。扩展减少系统种类的收益,必须与资源隔离、备份时间和故障半径一起计算。
用执行计划代替猜测#
关系数据库使用 EXPLAIN / EXPLAIN ANALYZE,MongoDB 使用 explain(),Redis 使用 SLOWLOG、latency diagnostics 和命令统计。验证时关注:
- 估算行数与实际行数是否偏离。
- 扫描了多少数据才返回多少结果。
- 排序和聚合是否落盘。
- 命中索引后是否仍发生大量随机回表。
- p95/p99 是否受锁、GC、磁盘或网络影响。
六、性能没有脱离场景的 QPS 排名#
“Redis 十万 QPS、关系库一万 QPS”缺少硬件、数据集、并发、请求大小、持久化、复制和延迟分位数,不能用于容量设计。同一数据库执行主键点查、跨表聚合和同步刷盘事务,结果可以相差多个数量级。
一个可复现的基准至少固定:
- 数据量与热数据比例。
- 索引、字段长度和返回结果大小。
- 并发连接数与连接池策略。
- 读写比例、事务长度和热点分布。
- 持久化、复制和确认参数。
- CPU、内存、磁盘、网络与版本。
- p50、p95、p99、错误率和资源饱和度。
压测还应包含故障:副本落后、节点重启、磁盘延迟升高、主节点切换和缓存冷启动。只测稳定状态下的峰值吞吐,会遗漏真正决定生产容量的恢复阶段。
七、复制、高可用与备份是三个不同问题#
- 复制提高冗余与读取能力,也会复制误删除和逻辑错误。
- 自动 failover 缩短部分故障的 RTO,但可能带来旧主重新加入、客户端重连和数据确认边界问题。
- 备份用于从误操作、损坏和灾难中恢复,必须与在线集群隔离。
| 产品 | 常见高可用路径 | 备份与恢复重点 |
|---|---|---|
| MySQL | InnoDB Cluster、Orchestrator、云托管、多副本 | 物理/逻辑备份、binlog、PITR、一致性校验 |
| PostgreSQL | 流复制、Patroni/repmgr、云托管 | base backup、WAL archive、PITR、恢复时间 |
| Redis | Sentinel 或 Cluster、云托管 | RDB/AOF 策略、复制积压、可接受丢失窗口、重建来源 |
| MongoDB | Replica Set、sharded cluster、Atlas | 一致快照、oplog/PITR、分片协调恢复 |
Redis 的 RDB 与 AOF 能提供不同持久性选择,并非“Redis 一定会丢数据”;但当配置为每秒 fsync、异步复制或发生复杂 failover 时,应用必须接受相应窗口。MongoDB、MySQL 和 PostgreSQL 也不能仅凭“写成功”三个字推导零数据丢失,必须说明确认级别和故障点。
先写 RPO/RTO,再选拓扑#
例如:
订单主库:RPO ≤ 30 秒,RTO ≤ 30 分钟缓存:允许全部丢失,15 分钟内从主库预热分析副本:允许落后 10 分钟,4 小时内可重建这三类数据即使来自同一业务,也不应该使用同一套恢复策略。
八、缓存是数据一致性协议,不是一个旁路加速器#
最常见的 Cache-Aside 流程是:
读:查缓存 → miss → 查主库 → 写缓存写:写主库 → 删除缓存这个流程仍需处理并发覆盖、删除失败、热点 Key、空值穿透、TTL 同时到期和主库故障。成熟实现通常包含:
- TTL 加随机抖动,避免同批 Key 同时失效。
- 热点 Key 使用请求合并或互斥重建。
- 对不存在的数据做短 TTL 负缓存,并在写入后主动失效。
- 更新路径具备幂等和重试;必要时使用 outbox/CDC 异步失效。
- 缓存不可用时有限流和降级,不能无限把流量回灌主库。
- 命中率与延迟按业务维度观察,而不是只看全局平均值。
如果无法回答“缓存与主库不一致时以谁为准”,就还没有定义这套架构的数据责任。
九、水平扩展:分片键是长期 API#
MySQL 与 PostgreSQL#
关系库通常先通过索引、查询优化、垂直扩容、只读副本、归档和分区延长单主架构寿命。真正分片后,跨分片事务、唯一约束、JOIN、分页和再平衡都会变复杂。
成熟开源路径包括:
- MySQL:Vitess、ProxySQL、Orchestrator。
- PostgreSQL:Citus、Patroni、PgBouncer。
它们解决的层次不同,不能把连接池、故障编排和分布式查询视为同一种“中间件”。
MongoDB#
MongoDB 原生提供 sharded cluster,但“内置”不等于“自动选对”。Shard key 决定写入是否形成热点、查询是否广播、数据是否容易均衡以及未来是否能有效再分片。选择前应使用真实查询分析目标键的基数、频率和单调性。
Redis Cluster#
Redis Cluster 将键分布到固定 hash slot。多键操作通常要求键位于同一 slot,可通过 hash tag 有意识地共置相关键,但过度共置又会制造热点。应用必须理解重定向、故障转移和客户端拓扑刷新。
十、海外成熟案例:提取约束,不照抄答案#
GitHub:MySQL 高可用与关系库分区#
GitHub 的公开工程文章描述了 MySQL 高可用自动化,以及为了规模将关系数据按业务边界 partition 的过程。值得注意的不是“GitHub 用 MySQL,所以 MySQL 适合所有大型网站”,而是这些工程条件:
- 明确主库身份与故障转移流程。
- 客户端和路由层配合拓扑变化。
- 通过自动化降低人工故障处理的不确定性。
- 分片前识别数据边界,处理跨分片访问和迁移。
- 大版本升级需要复制验证、流量迁移和回退路径。
GitHub 也公开记录了升级 GitHub.com 到 MySQL 8.0 的多年工作。这恰好说明“生态成熟”不等于升级没有成本。
Discord:数据库会随工作负载和运维现实改变#
Discord 的公开文章记录了消息存储从早期方案演进到 Cassandra,再迁移到 ScyllaDB 的过程,并讨论分区热点、垃圾回收、读延迟和数据迁移。这个案例没有证明某一个数据库永久获胜;它证明规模、访问模式和团队运维能力变化后,旧选择可能不再成立。
对小系统最有价值的迁移原则是:先定义目标模型,双写或批量搬迁必须可校验,切流要可暂停,旧系统要保留回退窗口。没有校验和回退的迁移,只是在生产上一次性重写数据。
为什么不使用“某大厂使用某库”的名单#
一家公司通常同时运行多种数据库,同一产品也会在不同年代迁移。供应商案例页只能证明某场景曾使用某产品,不能证明它是公司的“主数据库”,更不能替代工作负载分析。案例的可迁移单位是约束、指标、故障和取舍,而不是 Logo。
十一、四类典型决策#
通用事务 Web 应用#
从团队更熟悉的 MySQL 或 PostgreSQL 开始。把唯一性、外键、幂等和关键状态转换交给关系数据库;在执行计划和慢查询证明需要后再加 Redis。
JSON 属性很多的业务系统#
如果核心实体关系稳定,仅部分属性稀疏,优先评估 PostgreSQL JSONB 或 MySQL JSON,并保留关系约束。如果整个聚合天然按文档读写、字段版本频繁演进且跨文档事务少,再评估 MongoDB。
排行榜、限流和短期会话#
Redis 的数据结构与 TTL 很合适,但要定义重建来源、内存上限、淘汰策略和故障降级。Session 是否允许丢失,决定是否需要持久化与副本,而不是由“它只是缓存”决定。
分析、全文与向量#
先区分在线事务与分析检索。小规模时 PostgreSQL 扩展可以降低系统复杂度;当查询资源影响主交易、索引构建过重或数据生命周期不同,再拆到 ClickHouse、OpenSearch 或专用向量系统。MongoDB aggregation 也不是数据仓库的无条件替代。
十二、选型验证:两周 PoC 应该交付什么#
不要做只展示 CRUD 的样例。PoC 应使用脱敏后的真实数据分布和前五条查询,至少交付:
- schema、索引和关键查询的执行计划。
- 稳态与峰值的 p50/p95/p99、错误率和资源曲线。
- 热点写、缓存冷启动或大文档情况下的表现。
- 一次节点重启或主从切换,以及客户端恢复时间。
- 一次备份恢复,记录真实 RPO/RTO。
- schema 变更或数据迁移的向前兼容方案。
- 三年成本估算,包括副本、备份、流量和人力。
最终决策记录应写清:为什么选择、拒绝了什么、哪些假设尚未验证、达到什么指标后重新评估。这样未来迁移时,团队能判断是业务变了,还是当年的假设本来就不成立。
十三、生产验收清单#
- 关键约束由数据库或可靠的单一写入边界强制执行。
- 前五条查询有代表性数据和执行计划。
- 延迟以分位数衡量,压测固定了持久化与复制参数。
- 客户端超时、重试和幂等策略与数据库故障语义一致。
- 备份与在线副本隔离,并做过完整恢复。
- RPO/RTO 有实测,不以“开启了复制”代替。
- 缓存失效、击穿和整体不可用都有降级路径。
- 分片键通过真实访问分布验证,而不是只看基数。
- 数据迁移可校验、可暂停,并保留回退窗口。
- 团队能维护所选拓扑,而不只会调用驱动 API。
参考资料#
- MySQL 8.4 Reference Manual:InnoDB
- MySQL:InnoDB transaction model
- PostgreSQL:Transaction isolation
- PostgreSQL:Routine vacuuming
- PostgreSQL:JSON types
- Redis:Persistence
- Redis:Transactions
- Redis:Cluster specification
- MongoDB:Data modeling
- MongoDB:Transactions
- MongoDB:Shard keys
- GitHub:MySQL High Availability at GitHub
- GitHub:Partitioning GitHub’s relational databases to handle scale
- GitHub:Upgrading GitHub.com to MySQL 8.0
- Discord:How Discord Stores Trillions of Messages
- Vitess 文档
- Patroni 文档
