为什么 “majority” 不是 MongoDB 的默认 read concern?
Source: Dev.to
TL;DR
如果你习惯于传统的 SQL 数据库和同步的请求‑响应流程——在同一个事务或会话中读取你的写入——请在 MongoDB 中使用 majority 读取关注。它提供了你能从数据库中期望的最高隔离性和持久性。
- 它不是默认设置,但将其改为你的连接是安全的。
- 默认设置针对事件驱动、微服务架构的异步通信进行了优化,即使这意味着有时会读取可能随后被回滚的状态,也更倾向于更低的延迟。
为什么 MongoDB 默认不启用最强一致性
PostgreSQL 用户通常期望写入只有在被确认后才对其他会话可见,无论是通过自动提交的 DML 还是显式的 COMMIT。
在 MongoDB 中,必须 启用 majority 读取关注(read concern) 才能实现类似的 ACID 保证,而这 并非默认设置。
看似令人惊讶的是,MongoDB 在分布式数据库中提供了最强的一致性选项——完整的 ACID 语义,却没有默认开启,尽管其性能影响似乎并不大。
这种差异促使我们进一步探讨其背后的设计理由。
历史背景:SQL 与 NoSQL
-
SQL 标准:隔离级别最初是根据并发会话在读取和写入相同数据时可能出现的 异常(现象)来定义的。
- 这些定义与 基于锁的实现 紧密关联,而不是抽象模型;它们假设读取和写入使用锁,并且活动事务共享单一的当前数据库状态。
-
现代数据库 通常会选择不同的设计以实现可扩展性:
| 设计 | 示例 | 关键特性 |
|---|---|---|
| 使用 MVCC 的非阻塞读取 | PostgreSQL、MongoDB | 读取看到的是快照;会出现 写偏斜 等异常;支持如快照隔离(Snapshot Isolation,SI)等隔离级别。PostgreSQL 将其 SI 级别称为 可重复读,以匹配 SQL 标准。 |
| 非阻塞写入(乐观并发) | MongoDB | 写冲突会立即被检测到并抛出可重试异常,而不是等待锁。 |
Source: …
MongoDB 中的隔离性与持久性
要理解 MongoDB 中的隔离性和持久性,必须先分别考虑 读 与 写关注点——尤其是在复制的、分布式的环境中,读写可能落在不同的服务器上。随后我们再看看在写入后进行读取时,它们是如何相互作用的。
隔离性 vs. 持久性(ACID 中的 I 与 D)
| 概念 | 它保证的内容 |
|---|---|
| 隔离性 | 不同会话之间的读写如何相互可见。它必须在事务完成之前隐藏未提交写入的中间状态,并防止出现遗漏已提交写入的陈旧读取。 |
| 持久性 | 数据一旦写入后就保持持久,即使发生故障也不会丢失。已经被读取的数据也必须保证保持持久。 |
最初这些定义是基于单节点数据库的。 在现代系统中,持久性还必须应对网络和数据中心故障,因此数据会跨多个节点持久化,而不仅仅保存在本地磁盘上。
典型提交流程(SQL 风格)
- 提交被发起。
- WAL 刷写到本地磁盘 – 本地持久性。
- WAL 刷写到远程磁盘 – 全局持久性。
- 更改对其他会话可见(隔离结束)。
- 在客户端会话中确认提交。
上述顺序对应:
- PostgreSQL 中
synchronous_commit = on,或 - MongoDB 中
w:majority并且 读取会话使用majority读关注点。
其他配置会重新排列这些步骤:
| 系统 / 设置 | 顺序差异说明 |
|---|---|
| Oracle(默认) | 在 redo 日志刷写之前就让更改可见(除非设置 paranoid_concurrency_mode)。 |
PostgreSQL synchronous_commit = local 或 MongoDB w:1 | 在全局持久性之前就返回确认。 |
MongoDB local 读关注点 | 数据在持久化之前就已经可见。 |
“缺失”的默认设置:为何不使用最强的序列?
最强的隔离‑和‑持久性序列(上述步骤 1‑5)并不是 MongoDB 的默认设置。原因之一是 SQL 标准从未描述的额外异常,因为它假设 对单一数据库状态的读锁和写锁是互斥的。
MVCC 引入了两个逻辑时间
- 读取时间 – 事务的开始(或在 Read Committed 中语句的开始)。所有读取都使用该时间的快照。
- 写入时间 – 事务的结束;所有写入在提交时原子出现。
因为 读取时间 …
关键要点: 在 MVCC 系统中,应用程序常常在写入确认到达之前 通知其他服务,假设写入已经完成。只有当读取关注确保写入既持久又可见时,这种期望才是安全的。
摘要
majority读取关注 在 MongoDB 中提供最强的隔离性和持久性,可与许多 SQL 数据库的默认行为相媲美。- 它不是默认设置,因为 MongoDB 面向 事件驱动、低延迟的微服务架构,在这种场景下,偶尔出现的陈旧读取是可以接受的权衡。
- MVCC(读取时间 …)引入的 因果异常
提示: 使用 w:1 时,选择 local 读取关注 以避免此异常。w:1 不是 默认值;仅在希望降低延迟而非保证持久性时才会选择它。
延迟 vs. 持久性 权衡
-
本地确认 (
w:1/synchronous_commit = local)- 优点: 延迟更低;写入快速完成。
- 缺点: 发生故障时丢失事件的风险更高(写入可能被回滚)。
-
多数确认 (
w:majority/synchronous_commit = on)- 优点: 更强的持久性;写入能在节点或数据中心故障后仍然存活。
- 缺点: 需要额外的网络延迟,因为系统要等待远程确认。
即使使用多数确认,如果微服务 B 在多数已提交写入之前读取,也可能出现读后写异常。使用 MongoDB 的 本地读取关注 可以消除该异常,但可能会暴露随后可能被回滚的数据。
默认读取关注及其原理
- 默认读取关注(对 MongoDB 通常是 “majority”)与事件驱动架构高度契合,而事件驱动架构是早期 NoSQL 系统的主要使用场景。
- 开发者通常期望读取返回 最新的更改,即使这些更改尚未在原始线程中完全确认。
传统架构
当 MongoDB 在更传统、注重持久性的部署中使用时:
- 首选 “majority” 读取关注。
- 不会产生额外的性能惩罚,因为写入延迟已经在等待写入确认时付出了代价。
“majority” 的工作方式:
- 将读取时间戳设置为 最后提交时间,同时保持读取本地化。
- 在极少数情况下可能会短暂阻塞(例如实例启动、回滚、不可用或落后的从节点)。
- 通常 不会产生明显的性能影响。
MongoDB的可配置一致性模型
与许多 SQL 数据库对每个 DML 操作强制统一的一致性保证不同,MongoDB 将更多的责任交给开发者,让他们自行选择合适的一致性和性能设置。
关键配置选项
-
Write Concern – 决定持久性
- 示例:
w:majority用于抵御网络或数据中心故障的弹性。
- 示例:
-
Read Concern – 决定读取的可见性保证
- 示例:
majority– 读取最近的多数提交数据。snapshot– 为多分片事务提供更强的一致性。
- 示例:
-
Read Preference – 决定读取的路由位置
- 允许在副本集之间扩展读取,在可接受的情况下容忍一定的陈旧度。
通过调节这三个旋钮,MongoDB 能够适配各种一致性和性能需求。