分布式专题(8)之MongoDB存储原理&多文档事务详解

发布于:2024-12-20 ⋅ 阅读:(9) ⋅ 点赞:(0)

一、MongoDB存储原理

        存储引擎是数据库的组件,负责管理数据如何存储在内存和磁盘上,MongoDB支持多个存储引擎,因为不同的存储引擎对特定的工作负载表现更好,选择合适的存储引擎可以显著影响应用程序的性能。

1.1 WiredTiger介绍

        MongoDB从3.0开始引入可插拔存储引擎的概念,主要有MMAPV1、WiredTiger存储引擎可供选择。从MongoDB 3.2开始,WiredTiger存储引擎是默认的存储引擎。从4.2版开始,MongoDB删除了废弃的MMAPv1存储引擎。

1.2 WiredTiger读写模型

        读缓存:理想情况下,MongoDB可以提供进似与内存级别的读写性能。WiredTiger引擎实现了数据的二级缓存,第一层是操作系统的页面缓存,第二层则是引擎提供的内部缓存。

        读数据的流程如下:

  • 数据库发起Buffer IO读操作,由操作系统将磁盘数据页加载到文件系统 得页缓存区;
  • 引擎层读取页缓存区的数据,进行解压后存放到内部缓存区;
  • 在内存中完成匹配查询,将结果返回给应用。

        MongoDB为了尽可能保证业务查询的“热数据”能快速被访问,其内部缓存的默认大小达到了内存的一半,该值由wiredTigerCacheSize参数指定,其默认的计算公式如下:

wiredTigerCacheSize=Math.max(0.5*(RAM-1GB),256MB)

        写缓存:当数据发生 写入时,MongoDB并不会立即持久化到磁盘上,而是现在内存中记录这些变更,之后通过CheckPoint机制将变化的数据写入磁盘。这样子处理主要有以下两个原因:

  • 如果每一次写入都操作磁盘I/O,那么开销会非常大,而且响应时长会比较大。
  • 多个变更的写入可以尽可能进行I/O合并,降低资源负荷;

        那MongoDB如何保证单机的数据可靠性,主要包括以下两个部分:

        CheckPoint(检查点)机制:快照(snapshot)描述了某一时刻(point-in-time)数据在内存中的一致性视图,而这种数据的一致性是WiredTiger通过MVCC(多版本并发控制)实现的。当建立CheckPoint时,WiredTiger会在内存中建立所有数据的一致性快照,并将该快照覆盖的所有数据变化一并进行持久化(fsync)。成功之后,内存中数据的修改才得以真正保存。默认情况下,MongoDB每60s建立一次CheckPoint,在检查点写入过程中,上一个检查点仍然是可用的。这样可以保证一旦出错,MongoDB仍然能恢复到上一个检查点。

          Journal日志:Journal是一种预写式日志(write ahead log)机制,主要用来弥补CheckPoint机制的不足。如果开启了Journal日志,那么WiredTiger会将每个写操作的redo日志写入Journal缓冲区,该缓冲区会频繁地将日志持久化到磁盘上。默认情况下,Journal缓冲区每100ms执行一次持久化。此外,Journal日志达到100MB,或是应用程序指定journal:true,写操作都会触发日志的持久化。一旦MongoDB发生宕机,重启程序时会先恢复到上一个检查点,然后根据Journal日志恢复增量的变化。由于Journal日志持久化的间隔非常短,数据能得到更高的保障,如果按照当前版本的默认配置,则其在断电情况下最多会丢失100ms的写入数据。

        WiredTiger写入数据流程:

  • 应用向MongoDB写入数据(插入、修改或删除)。
  • 数据库从内部缓存中获取当前记录所在的页块,如果不存在则会从磁盘中加载(Buffer I/O)
  • WiredTiger开始执行写事务,修改的数据写入页块的一个更新记录表,此时原来的记录仍然保持不变。
  • 如果开启了Journal日志,则在写数据的同时会写入一条Journal日志(Redo Log)。该日志在最长不超过100ms之后写入磁盘
  • 数据库每隔60s执行一次CheckPoint操作,此时内存中的修改会真正刷入磁盘。

        Journal日志的刷新周期可以通过参数storage.journal.commitIntervalMs指定,MongoDB 3.4及以下版本的默认值是50ms,而3.6版本之后调整到了100ms。由于Journal日志采用的是顺序I/O写操作,频繁地写入对磁盘的影响并不是很大。

       CheckPoint的刷新周期可以调整storage.syncPeriodSecs参数(默认值60s),在MongoDB 3.4及以下版本中,当Journal日志达到2GB时同样会触发CheckPoint行为。如果应用存在大量随机写入,则CheckPoint可能会造成磁盘I/O的抖动。在磁盘性能不足的情况下,问题会更加显著,此时适当缩短CheckPoint周期可以让写入平滑一些。

二、MongoDB多文档事务详解

2.1事务简介 

         事务(transaction)是传统数据库所具备的一项基本能力,其根本目的是为数据的可靠性与一致性提供保障。而在通常的实现中,事务包含了一个系列的数据库读写操作,这些操作要么全部完成,要么全部撤销。例如,在电子商城场景中,当顾客下单购买某件商品时,除了生成订单,还应该同时扣减商品的库存,这些操作应该被作为一个整体的执行单元进行处理,否则就会产生不一致的情况。

        数据库事务需要包含4个基本特性,即常说的ACID,具体如下:

  • 原子性(atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性(consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
  • 隔离性(isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  • 持久性(durability):已被提交的事务对数据库的修改应该是永久性的。

2.2 MongoDB多文档事务

        在MongoDB中,对单个文档的操作是原子的。由于可以在单个文档结构中使用内嵌文档和数组来获得数据之间的关系,而不必跨多个文档和集合进行范式化,所以这种单文档原子性避免了许多实际场景中对多文档事务的需求。

        对于那些需要对多个文档(在单个或多个集合中)进行原子性读写的场景,MongoDB支持多文档事务。而使用分布式事务,事务可以跨多个操作、集合、数据库、文档和分片使用。

        MongoDB 虽然已经在 4.2 开始全面支持了多文档事务,但并不代表大家应该毫无节制地使用它。相反,对事务的使用原则应该是:能不用尽量不用。 通过合理地设计文档模型,可以规避绝大部分使用事务的必要性。    

        使用事务的原则:
  • 无论何时,事务的使用总是能避免则避免;
  • 模型设计先于事务,尽可能用模型设计规避事务;
  • 不要使用过大的事务(尽量控制在 1000 个文档更新以内);
  • 当必须使用事务时,尽可能让涉及事务的文档分布在同一个分片上,这将有效地提高效率;

2.3 MongoDB对事务的支持

事务属性

支持程度

Atomocity 原子性

单表单文档 : 1.x 就支持

复制集多表多行:4.0

分片集群多表多行:4.2

Consistency 一致性

writeConcern, readConcern (3.2)

Isolation 隔离性

readConcern (3.2)

Durability 持久性

Journal and Replication

2.3.1 writeConcern

        writeConcern 决定一个写操作落到多少个节点上才算成功。MongoDB支持客户端灵活配置写入策略(writeConcern),以满足不同场景的需求。

        语法格式:

{ w: <value>, j: <boolean>, wtimeout: <number> }

         1)w: 数据写入到number个节点才向用客户端确认

{w:0}对客户端的写入不需要发送确认,适用于性能要求高,但不关注正确性的场景

{w:1}默认的writeConcern,数据写入到Primary就向客户端发送确认

{w:"majority"}数据写入到副本集大多数成员后向客户端发送确认,适用于数据安全性要求比较高的场景,该选项会降低写入性能

         2)j: 写入操作的journal持久化后才向客户端确认

默认为{i: false},如果要求Primary写入持久化了才向客户端确认,则指定该选项为true

         3)wtimeout: 写入超时时间,仅w的值大于1时有效。

当指定{w:}时,数据需要成功写入number个节点才算成功,如果写入过程中有节点故障,可能导致这个条件一直不能满足,从而一直不能向客户端发送确认结果,针对这种情况,客户端可设置wtimeout选项来指定超时时间,当写入过程持续超过该时间仍未结束,则认为写入失败

         注意事项

  • 虽然多于半数的 writeConcern 都是安全的,但通常只会设置 majority,因为这是等待写入延迟时间最短的选择;
  • 不要设置 writeConcern 等于总节点数,因为一旦有一个节点故障,所有写操作都将失败;
  • writeConcern 虽然会增加写操作延迟时间,但并不会显著增加集群压力,因此无论是否等待,写操作最终都会复制到所有节点上。设置 writeConcern 只是让写操作等待复制后再返回而已;
  • 应对重要数据应用 {w: “majority”},普通数据可以应用 {w: 1} 以确保最佳性能。

         在读取数据的过程中我们需要关注以下两个问题:

  • 从哪里读?
  • 什么样的数据可以读?
        第一个问题是是由 readPreference 来解决,第二个问题则是由 readConcern 来解决

2.3.2 readPreference

        readPreference决定使用哪一个节点来满足正在发起的读请求。可选值包括:

  • primary: 只选择主节点,默认模式;
  • primaryPreferred:优先选择主节点,如果主节点不可用则选择从节点;
  • secondary:只选择从节点;
  • secondaryPreferred:优先选择从节点, 如果从节点不可用则选择主节点;
  • nearest:根据客户端对节点的 Ping 值判断节点的远近,选择从最近的节点读取。

     合理的 ReadPreference 可以极大地扩展复制集的读性能,降低访问延迟。

   

        readPreference 场景举例
  • 用户下订单后马上将用户转到订单详情页——primary/primaryPreferred。因为此时从节点可能还没复制到新订单;
  • 用户查询自己下过的订单——secondary/secondaryPreferred。查询历史订单对 时效性通常没有太高要求;
  • 生成报表——secondary。报表对时效性要求不高,但资源需求大,可以在从节点单独处理,避免对线上用户造成影响;
  • 将用户上传的图片分发到全世界,让各地用户能够就近读取——nearest。每个地区的应用选择最近的节点读取数据。

        扩展:Tag

        readPreference 只能控制使用一类节点。Tag 则可以将节点选择控制到一个或几个节点。考虑以下场景:
  • 一个 5 个节点的复制集;
  • 3 个节点硬件较好,专用于服务线上客户;
  • 2 个节点硬件较差,专用于生成报表;
        可以使用 Tag 来达到这样的控制目的:
  • 为 3 个较好的节点打上 {purpose: "online"};
  • 为 2 个较差的节点打上 {purpose: "analyse"};
  • 在线应用读取时指定 online,报表读取时指定 analyse。

# 为复制集节点添加标签
conf = rs.conf()
conf.members[1].tags = { purpose: "online"}
conf.members[4].tags = { purpose: "analyse"}
rs.reconfig(conf)

#查询
db.collection.find({}).readPref( "secondary", [ {purpose: "online"} ] )

2.3.3 readConcern

        在 readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据哪些是可读的,类似于关系数据库的隔离级别。可选值包括:

  • available:读取所有可用的数据;
  • local:读取所有可用且属于当前分片的数据;
  • majority:读取在大多数节点上提交完成的数据;
  • linearizable:可线性化读取文档,仅支持从主节点读;
  • snapshot:读取最近快照中的数据,仅可用于多文档事务;

         readConcern: local 和 available,在复制集中 local 和 available 是没有区别的,两者的区别主要体现在分片集上。

        一个 chunk x 正在从 shard1 向 shard2 迁移;整个迁移过程中 chunk x 中的部分数据会在 shard1 和 shard2 中同时存在,但源分片 shard1仍然是chunk x 的负责方:

        所有对 chunkx的读写操作仍然进入 shard1;config 中记录的信息 chunkx仍然属于 shard1;

        此时如果读 shard2,则会体现出 local 和 available 的区别:

  • local:只取到应有由shard2负责的数据(不包括x);
  • available:shard2上有什么就读什么(包括x)

        注意事项

  • 虽然看上去总是应该选择 local,但毕竟对结果集进行过滤会造成额外消耗。在一些无关紧要的场景(例如统计)下,也可以考虑 available;
  • MongoDB <=3.6 不支持对从节点使用 {readConcern: "local"};
  • 从主节点读取数据时默认 readConcern 是 local,从从节点读取数据时默认readConcern 是 available(向前兼容原因)

网站公告

今日签到

点亮在社区的每一天
去签到