理解 MongoDB 一致性:为开发者解释的 BASE 模型
通过这份面向开发者的深度指南,解锁 MongoDB 的一致性模型。了解 BASE 模型如何驱动 MongoDB 的可扩展性,并与传统 ACID 数据库进行对比。我们将解析最终一致性,探索 MongoDB 灵活的读写关注(Read and Write Concerns),并提供实际示例来调整数据库以实现最佳性能和数据的完整性。理解为何这些选择对于在分布式 NoSQL 平台上构建弹性、高性能的应用程序至关重要。
理解 MongoDB 一致性:为开发者解释的 BASE 模型
MongoDB 的一致性常常令人困惑,因为人们将其简化为一句话:“MongoDB 是最终一致性的。”这种说法过于笼统,缺乏实用性。一个 MongoDB 副本集可以根据写入关注、读取关注、读取偏好以及是否发生故障转移,为某些操作提供强一致性行为,而为其他操作提供过时读取。
如果你是从 PostgreSQL 或 MySQL 转过来的,最大的调整在于一致性并非整个应用程序的固定设置。你需要为每个路径选择所需的保证。结账流程、通知推送和分析仪表盘对数据新鲜度和持久性的要求各不相同。
ACID 与 BASE:两种一致性方法
在深入探讨 MongoDB 的模型之前,了解数据库一致性的两种主要范式——ACID 和 BASE——会很有帮助。
ACID 属性(传统关系型数据库)
像 PostgreSQL 或 MySQL 这样的传统关系型数据库管理系统通常遵循 ACID 属性,以确保数据的可靠性,尤其是在事务性工作负载中。ACID 代表:
- 原子性(Atomicity):每个事务被视为一个不可分割的单元。它要么完全执行(提交),要么完全不执行(回滚)。不存在部分事务。
- 一致性(Consistency):事务将数据库从一个有效状态转变为另一个有效状态。它确保写入数据库的数据必须符合所有定义的规则和约束。
- 隔离性(Isolation):并发事务独立执行,看起来像是顺序执行的。并发事务的结果与它们一个接一个执行的结果相同。
- 持久性(Durability):一旦事务提交,即使在断电、崩溃或其他系统故障的情况下,它也会保持提交状态。更改会被永久存储。
ACID 保证了强一致性,使其成为需要严格数据完整性的应用程序(如金融交易)的理想选择。
BASE 属性(像 MongoDB 这样的 NoSQL 数据库)
相比之下,许多 NoSQL 数据库(包括 MongoDB)优先考虑可用性和分区容忍性,而不是即时一致性,通常遵循 BASE 模型。BASE 代表:
- 基本可用(Basically Available):系统保证可用性,意味着它会响应任何请求,即使无法保证数据是最新版本。
- 软状态(Soft State):系统的状态可能随时间变化,即使没有输入。这是由于最终一致性模型导致数据在系统中异步传播。
- 最终一致性(Eventual Consistency):如果对某个数据项没有新的更新,最终对该项的所有访问都将返回最后更新的值。在分布式系统中,更改在所有节点上可见之前会存在延迟。
符合 BASE 的系统专为分布式环境中的高可用性和可扩展性而设计,适用于可以容忍数据传播中一定延迟的应用程序。
理解 MongoDB 中的最终一致性
当从副本节点读取数据或写入尚未完全复制时,MongoDB 可能表现出最终一致性行为。这意味着当你向 MongoDB 副本集写入数据时,主节点会确认写入,然后异步地将该写入复制到其副本节点。虽然主节点确保写入是持久的,但它不会等待所有副本节点都跟上进度才向客户端确认成功。因此,随后从副本节点读取的数据可能不会立即反映最新的写入,但最终会变得一致。
这种设计选择是 MongoDB 能够水平扩展并保持高可用性的基础。通过不要求所有节点在每个操作上都完美同步,MongoDB 可以继续提供读写服务,即使某些节点暂时不可用或滞后。
最终一致性的权衡
- 优点:更高的可用性、更好的性能(写入延迟更低)以及分布式系统更强的可扩展性。
- 缺点:应用程序必须设计为能够处理读取到过时数据的可能性。这对于需要所有副本之间即时一致性的操作尤其重要。
MongoDB 的读写关注:调整一致性
虽然 MongoDB 默认是最终一致性,但它提供了强大的机制——读取关注和写入关注——允许开发者在每个操作的基础上调整一致性级别。这使你能够根据应用程序的需求平衡一致性、可用性和性能。
写入关注
写入关注描述了 MongoDB 对写入操作请求的确认级别。它规定了在操作返回成功之前,必须有多少个副本集成员确认写入。
关键的写入关注选项:
w:指定必须确认写入的mongod实例数量。w: 0:无确认。客户端不等待数据库的任何响应。这提供了最高的吞吐量,但如果主节点在写入后立即崩溃,则存在数据丢失的风险。w: 1(默认):仅主节点确认。主节点确认已接收并处理写入。速度很快,但不保证写入已复制到任何副本节点。w: "majority":副本集大多数成员(包括主节点)的确认。这提供了更强的持久性保证,因为写入已提交到大多数节点。如果主节点发生故障,数据保证存在于大多数其他节点上。
j:指定mongod实例在确认写入之前是否应将写入写入磁盘日志。启用日志记录(j: true)可在mongod进程崩溃时提供持久性。wtimeout:满足写入关注的时间限制。如果在此时间内未满足写入关注,则写入操作返回错误。
写入关注示例(使用 w: "majority" 并启用日志记录):
db.products.insertOne(
{ item: "laptop", qty: 50 },
{ writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
);
提示:对于必须持久且高可用的关键数据,建议使用
w: "majority"和j: true。对于不太关键的数据或高吞吐量的日志记录,w: 1甚至w: 0可能是可以接受的。
读取关注
读取关注允许你指定读取操作的一致性和隔离级别。它决定了 MongoDB 返回给查询的数据,尤其是在复制环境中。
关键的读取关注选项:
local:返回客户端连接的实例(主节点或副本节点)上的数据。这是独立实例和副本节点的默认设置。对于副本集,这提供了最低的延迟,但可能返回过时数据。available:返回实例上的数据,而不保证数据已写入副本集的大多数成员。类似于local,它优先考虑可用性和低延迟。majority:返回已被副本集大多数成员确认的数据。这保证了数据的持久性,并且不会被回滚。与local或available相比,它提供了更强的一致性,但代价是可能更高的延迟。linearizable:保证返回的数据反映全局范围内最新的已确认写入。这是最强的读取关注,确保读取操作能看到所有已被majority写入关注确认的写入。它可能会带来显著的性能开销,并且仅适用于从主节点读取。snapshot(用于多文档事务):保证查询返回特定时间点的数据,允许在事务内跨多个文档进行一致的读取。
读取关注示例(使用 majority):
db.products.find(
{ item: "laptop" },
{ readConcern: { level: "majority" } }
);
警告:虽然
linearizable提供了强一致性,但它会带来性能影响。请谨慎使用,仅用于需要严格排序和全局写入可见性的场景。
为什么 BASE 和最终一致性对扩展很重要
BASE 模型和最终一致性是 MongoDB 实现可扩展性和高可用性的核心推动因素:
- 水平扩展(分片):通过放宽即时一致性,MongoDB 可以将数据分布到多个分片(副本集集群)上。每个分片相对独立地运行,使数据库能够水平扩展以处理海量数据集和高吞吐量,而无需整个分布式系统中的每个节点始终保持完美同步。
- 高可用性和容错性:在副本集中,如果主节点不可用,可以从副本节点中选举出新的主节点。最终一致性意味着即使在故障转移期间,副本节点也可以继续提供读取服务(取决于读取关注),并且系统保持可用。如果主节点每次写入都必须等待所有副本节点,那么一个滞后的副本节点就可能成为整个系统的瓶颈。
- 性能:较宽松的一致性要求意味着写入操作的延迟更低,整体吞吐量更高,因为系统无需在继续之前阻塞并等待所有节点的确认。
通过提供可通过读写关注调整的一致性,MongoDB 使开发者能够做出明智的决策。优先考虑高可用性和吞吐量的应用程序(例如,物联网数据采集、实时分析)可以选择较弱的一致性。相反,需要更强数据完整性的应用程序(例如,金融交易、库存更新)可以选择更强的一致性级别,并接受相关的性能权衡。
实际考虑因素和最佳实践
- 识别关键数据:确定哪些数据绝对需要强一致性(例如,账户余额),哪些数据可以容忍最终一致性(例如,用户资料更新、会话数据)。
- 设计幂等性:当使用较弱的写入关注时,写入可能在主节点上成功,但在复制到副本节点之前失败,导致后续回滚,而客户端认为写入失败。如果客户端重试该操作,可能会导致重复。尽可能将操作设计为幂等的。
- 客户端读取自己的写入:如果用户执行写入后立即尝试读取,并且使用较弱的读取关注从副本节点读取,则可能会看到过时数据。为了确保用户始终能读取到自己最近的写入,可以考虑将此类读取定向到主节点,或使用
majority读取关注,可能结合针对这些特定操作的majority写入关注。 - 监控:使用
rs.printReplicationInfo()或 MongoDB Atlas 指标密切关注副本集延迟。高复制延迟可能会加剧最终一致性问题。
一种更有用的思考 MongoDB 一致性的方式
MongoDB 并非在所有情况下都简单地是“最终一致性”,这样看待它会导致设计粗糙。在已确认写入后从主节点读取,与从落后几秒的副本节点读取,行为可能截然不同。使用 w: 1 确认的写入与使用 w: "majority" 确认的写入具有不同的风险特征。一致性情况取决于你的读取偏好、读取关注、写入关注、拓扑结构以及是否在错误的时间发生故障转移。
对于普通的产品页面,最终一致性可能没问题。如果管理员更改了产品描述,而客户在短时间内从副本节点看到旧描述,业务影响通常很小。对于订单确认页面,容忍度则不同。如果客户提交订单后,下一个屏幕无法找到它,即使是很短的时间,系统也会感觉有问题。在这种情况下,读取自己写入的行为比原始吞吐量更重要。
一个实用的模式是:对面向用户的确认路径使用更强的设置,对后台或分析路径使用较宽松的设置。例如,订单写入可能使用 w: "majority",而即时确认读取可能指向主节点。汇总昨天活动的仪表盘可以从副本节点读取,因为一点延迟通常是可以接受的。日志采集管道可能接受比计费账本更弱的确认,但仍应诚实地说明在崩溃或故障转移期间可能丢失的内容。
也要小心“可用”这个词。分布式数据库可以在故障期间继续服务某些请求,但这并不意味着每个请求都能以相同的保证成功。主节点选举会短暂暂停写入。副本节点只有在你的读取偏好允许时才能提供读取服务。网络分区可能迫使 MongoDB 选择安全性,而不是接受不再属于大多数节点的写入。这些不是缺陷;它们是保持复制数据不分裂成两个冲突历史所需的权衡。
以下是我会写入应用程序设计说明中的决策:
关键账户、订单和库存变更:
- writeConcern: majority
- 在确认用户自身操作时从主节点回读
- 在驱动程序支持的地方使用可重试写入
- 使用请求 ID 或唯一键使写入操作幂等
搜索页面、信息流页面、分析和非关键资料展示:
- 从副本节点读取可能是可接受的
- 在 UI 中容忍过时结果
- 当数据新鲜度重要时显示时间戳
这种说明比简单地说“MongoDB 是 BASE”然后继续更有用。它告诉未来的工程师在哪里可以接受过时读取,在哪里不可以。