MongoDB 4.2——持久性

📅 2026/7/28 17:32:15 👁️ 阅读次数 📝 编程学习
MongoDB 4.2——持久性

持久性

    • 1、使用日志机制的成员级别持久性
    • 2、使用写关注的集群级别持久性
      • 2.1、writeConcern的w和wtimeout选项
      • 2.2、writeConcern的j(日志)选项
    • 3、使用读关注的集群级别持久性
    • 4、使用写关注的事务持久性
    • 5、MongoDB不能保证什么
    • 6、检查数据损坏

1、使用日志机制的成员级别持久性

为了在服务器发生故障时提供持久性,MongoDB 使用了一种称为日志(journal)的预写式日志(WAL)机制。WAL 是数据库系统中一种常用的持久性技术,其基本原理是,在将对数据库所做的更改应用到数据库本身之前,将对这些更改的一种表示写到持久介质(如磁盘)上。在许多数据库系统中,WAL 也被用来提供原子性这一数据库属性。然而,MongoDB 使用其他技术来确保原子写入。

从 MongoDB 4.0 开始,当应用程序对副本集执行写操作时,MongoDB 会使用与 oplog相同的格式创建日志条目。MongoDB 使用了一种基于操作日志(oplog)的语句级别的复制机制。oplog 中的语句是对写操作影响的每个文档所做的实际更改的表示。

因此,oplog 语句很容易应用于副本集的其他成员,而无须考虑版本、硬件或副本集成员之间的其他差异。此外,每个 oplog 语句都是幂等的,这意味着它可以被应用任意次数,而对数据库的更改结果总是相同的。

像大多数数据库一样,MongoDB 同时维护了日志和数据库数据文件的内存视图。默认情况下,它每 50 毫秒会将日志条目刷新到磁盘上,每 60 秒会将数据库文件刷新到磁盘上。刷新数据文件的 60 秒间隔称为检查点(checkpoint)​。日志用于为自上一个检查点以来写入的数据提供持久性。关于持久性的问题,如果服务器突然停止了,那么在其重新启动时,可以使用日志重放在关闭前没有刷新到磁盘的所有写操作。

对于日志文件,MongoDB 在 dbPath 目录下创建了一个名为 journal 的子目录。WiredTiger(MongoDB 的默认存储引擎)日志文件的名称格式为 WiredTigerLog.<sequence>,其中 <sequence> 是一个从 0 000 000001 开始的零填充数字。除了非常小的日志记录,MongoDB 会对写入日志的数据进行压缩。日志文件的最大大小限制大约为 100MB。一旦日志文件超过这个限制,MongoDB 就会创建一个新的日志文件,并在其中写入新的记录。

由于日志文件只需要在上次检查点之后恢复数据,因此在新的检查点写入完成时,MongoDB 会自动删除“旧的”日志文件,也就是那些在最近检查点之前的写操作。

如果服务器崩溃了(或使用了 kill -9)​,那么 mongod会在启动时重放其日志文件。可能发生的写操作丢失有一个最大范围,默认情况下是最近 100 毫秒加上将日志刷 新到磁盘所花费的时间内所发生的写操作。

如果应用程序需要较短的日志刷新间隔,那么有两种方法。

  • 一种方法是对 mongod 命令使用--journalCommitInterval选项以更改间隔时间。该选项接受从 1 到 500 毫秒的值。
  • 另一种方法是在写关注中指定所有写操作都记录到磁盘。缩短日志刷盘的时间间隔将对性能产生负面影响,因此在更改日志记录默认值之前,需要确定对应用程序的影响。

2、使用写关注的集群级别持久性

通过写关注(write concern)​,可以指定应用程序在响应写请求时需要何种级别的确认。在副本集中,网络分区、服务器故障或数据中心断电都可能会阻止写操作复制到每个成员,甚至大多数成员。当副本集恢复到正常状态时,可能会回滚那些未复制到大多数成员的写操作。在这些情况下,客户端和数据库可能对已提交的数据有不同的看法。

有些应用程序在某些情况下可以接受写操作的回滚。例如,在某些社交应用程序中回滚少量的评论不会有什么问题。MongoDB 在集群级别上支持一系列耐久性保证,使应用程序设计者能够选择最适合其场景的耐久性级别。

2.1、writeConcern的w和wtimeout选项

MongoDB 查询语言支持为所有插入和更新方法指定写关注。假设现在有一个电子商务应用程序,我们希望确保所有的订单都是持久的。将订单写入数据库的代码如下所示:

try{db.products.insertOne({sku:"H1100335456",item:"Electric Toothbrush Head",quantity:3},{writeConcern:{w:"majority",wtimeout:100}});}catch(e){print(e);}

所有的插入和更新方法都接受第二个参数,其格式是一个文档。在该文档中可以为 writeConcern 指定一个值。在前面示例中指定的写关注表示,希望得到服务器的确认。这个确认是,只有当写入被成功复制到副本集的大多数成员时其才算成功完成。此外,如果写操作没有在 100 毫秒或更短的时间内复制到大多数副本集成员,则应该返回错误。在这种情况下,MongoDB 不会撤销写关注超过时间限制之前成功执行的数据修改,而应该由应用程序决定如何处理这种情况下的超时。通常来说,应该对wtimeout 值进行配置,这样只有在不寻常的情况下应用程序才会超时,而应用程序在响应超时错误时所采取的行动将确保数据处于正确的状态。在大多数情况下,应用程序应该尝试确定超时是由于网络通信的短暂问题还是其他更严重的原因造成的。

写关注文档中 w 参数的值可以指定为 “majority”(如本例中那样)​。或者,也可以指定为一个介于零和副本集成员数量之间的整数。最后,还可以指定为副本集成员的标签,比如标识那些在 SSD 或机械硬盘上的成员,或者标识那些用于报表系统或 OLTP 工作负载的成员。还可以指定标签集合作为 w 的值,以确保只有在提交给至少一个与所提供的标签集合匹配的副本集成员时才会对写操作进行确认。

2.2、writeConcern的j(日志)选项

除了为 w 选项提供值之外,还可以通过在写关注文档中使用 j 选项来要求对写操作的日志写入情况进行确认。如果 j 的值为 true,则 MongoDB 只有在请求的成员数(w的值)都已经将操作写入它们磁盘上的日志中时,才会确认写操作成功。继续刚才的例子,如果想确保大多数成员的所有写操作记录在日志中,则可以像下面这样更新代码:

try{db.products.insertOne({sku:"H1100335456",item:"Electric Toothbrush Head",quantity:3},{writeConcern:{w:"majority",wtimeout:100,j:true}});}catch(e){print(e);}

在不等待日志记录的情况下,如果服务器进程或硬件停止运行,那么在每个成员上会有一个大约 100 毫秒的短暂的时间窗口可能发生写操作丢失。然而,在确认对副本集成员的写操作之前等待日志记录确实会造成性能损失。

在解决持久性问题时,必须仔细评估应用程序的需求,并权衡所选择的持久性设置对性能的影响。

3、使用读关注的集群级别持久性

在 MongoDB 中,读关注(read concern)允许对何时读取结果进行配置。这可以让客户端在写操作被持久化之前就看到写入的结果。读关注可以与写关注一起使用,以控制对应用程序的一致性和可用性的保证级别。不要将读关注与读偏好(read preference)相混淆,后者处理从何处读取数据的问题。具体来说,读偏好决定了副本集中承载数据的成员。默认的读偏好是从主节点中读取。

读关注决定了正在读取的数据的一致性和隔离性。默认的readConcern 是 local,它所返回的数据不保证已经被写入了大多数承载数据的副本集成员。这可能会导致数据在将来的某个时刻被回滚。majority 读关注只返回被大多数副本集成员确认的持久数据(不会被回滚)​。MongoDB3.4 中增加了 linearizable 读关注,它确保返回的数据反 映了在读操作开始之前已成功完成的经过大多数确认的写操作。在返回结果之前,它可能会等待那些正在并发执行的写操作完成。

和写关注一样,在为应用程序选择合适的关注选项之前,需要权衡读关注对性能的影响,以及它们提供的持久性和隔离性保证。

4、使用写关注的事务持久性

在 MongoDB 中,对单个文档的操作是原子的。可以在单个文档中使用内嵌文档和数组来表示实体之间的关系,而不是使用范式化的数据模型将实体和关系拆分到多个集合中。因此,很多应用程序不需要多文档事务。

然而,对于需要原子更新多个文档的场景,MongoDB 提供了针对副本集执行多文档事务的能力。多文档事务可以跨多个操作、文档、集合和数据库使用。

事务要求其中的所有数据更改都是成功的。如果任何操作失败,则事务将中止,所有数据更改都会被丢弃。如果所有操作都成功,那么事务中所做的所有数据更改都会被保存,并且写操作对之后的读操作都是可见的。

与单个写操作一样,可以为事务指定一个写关注。对于事务来说,应该在事务级别而不是单个操作级别设置写关注。在提交时,事务会使用事务级别的写关注来提交写操作。为事务内部单个操作设定的写关注会被忽略。

可以在事务开始时为事务提交设置写关注。事务不支持将写关注设置为 0。如果对一个事务使用了写关注 1,那么如果发生了故障转移,则事务可能会回滚。如果在副本集中发生可能导致强制性故障转移的网络和服务器故障,则可以使用 “majority” 的 writeConcern 来确保事务在这种情况下的持久性。

functionupdateEmployeeInfo(session){employeesCollection=session.getDatabase("hr").employees;eventsCollection=session.getDatabase("reporting").events;session.startTransaction({writeConcern:{w:"majority"}});try{employeesCollection.updateOne({employee:3},{$set:{status:"Inactive"}});eventsCollection.insertOne({employee:3,status:{new:"Inactive",old:"Active"}});}catch(error){print("Caught exception during transaction, aborting.");session.abortTransaction();throwerror;}commitWithRetry(session);}

5、MongoDB不能保证什么

MongoDB 在一些情况(比如存在硬件问题或文件系统错误)下无法保证持久性。特别是,如果硬盘损坏,则MongoDB 无法保护其中的数据。

此外,不同种类的硬件和软件可能有不同的持久性保证。例如,一些较便宜或较旧的硬盘在写操作排队等待时(而不是在实际写入之后)就会报告写入成功。MongoDB 在这个级别上无法防止误报:如果这时系统崩溃,则数据可能会丢失。

基本上,MongoDB 的安全性与其底层系统相当:如果硬件或文件系统破坏了数据,则 MongoDB 无法防止这种情况。可以使用复制机制来应对系统问题。如果一台机器出了故障,那么希望另一台机器仍能正常工作。

6、检查数据损坏

validate 命令可用于检查集合是否损坏。要对 movies 集合运行 validate 命令,可以执行以下操作:

db.movies.validate({full:true}){"ns":"sample_mflix.movies","nInvalidDocuments":NumberLong(0),"nrecords":45993,"nIndexes":5,"keysPerIndex":{"_id_":45993,"$**_text":3671341,"genres_1_imdb.rating_1_metacritic_1":94880,"tomatoes_rating":45993,"getMovies":45993},"indexDetails":{"$**_text":{"valid":true},"_id_":{"valid":true},"genres_1_imdb.rating_1_metacritic_1":{"valid":true},"getMovies":{"valid":true},"tomatoes_rating":{"valid":true}},"valid":true,"warnings":[],"errors":[],"extraIndexEntries":[],"missingIndexEntries":[],"ok":1}

你要查找的主要字段是 “valid”,希望这个字段为 true。否则,validate 会给出所发现的数据损坏细节。

validate 输出中的大部分内容描述了集合的内部结构,以及用于理解跨集群操作顺序的时间戳。这些对于调试来说不是特别有用。​

validate 命令只适用于集合,它还会检查相关联的索引并记录在 indexDetails 字段中。不过,这需要使用 { full:true } 选项来启用完整的 validate。