HICSC
MongoDB单文档事务

在MongoDB中,对单个文档的操作是原子的。在建模时,可以在单个文档中使用内嵌子文档或内嵌数组的方式来描述一个对象和另外一个对象以及对象集合的关系。在实际的应用场景中,这种单文档的原子性消除了许多跨文档事务的需求。在很长一段时间内,MongoDB都仅支持单文档事务。相比于传统的关系型数据库,其在开发和性能上都具有相当大的优势,但由于其不支持多文档事务的缺陷,导致其很难应用在企业的核心业务系统上,这也严重制约了MongoDB的发展。

从4.0版本开始,MongoDB开始支持多文档事务,但4.0仅支持副本集(也称复制集)下的多文档事务,从4.2开始,才支持分片集群的多文档事务。有一点需要注意的是,MongoDB的多文档事务可以跨文档、集合以及数据库。当事务提交时,将执行并保存在事务中所做的所有数据更新。如果事务中有任何操作失败,事务中止,事务中所有数据更新操作都将被丢弃。在事务提交之前,事务中的写入或更新操作在事务外部不可见,即类似于MySQL的事务隔离级别中的读已提交(Read Committed)

有的同学可能对副本集和分片集群的概念有点陌生,这里稍微提一句。副本集类似于MySQL集群中的主从复制,而分片集群类似于在主从复制的基础上增加分库分表,其大致的架构如下图所示。更多的内容可以阅读参考资料中的相关内容。
MongoDB部署架构

写操作事务

首先要明确一个概念,即使在单机环境下,对一行数据进行写操作,在数据库底层实现时,也包含多个操作。比如,如果在更新数据的字段上建有索引,在写入数据后,还要更新对应的索引信息;再比如,每次的数据修改,有可能并没有立即被写入磁盘,而是采用异步刷盘的方式,如果在写操作过程中发生故障,如何保证写入的数据不丢失?如果是在集群的环境下,需要应对的问题会更多。

在MongoDB中,不管部署方式是副本集还是分片集群,对于上层的应用都是透明的。当Mongo向客户端说写操作成功时,其至少应该保证以下两点:

  1. 数据在指定数量节点上已经成功写入
  2. 数据的变更已经被写入journal日志(重放日志,即存储引擎层面的操作日志,保证数据的一致性)

这里以副本集为例,在MongoDB副本集中写入一个文档时,需要修改如下数据:

  1. 将文档数据写入对应集合
  2. 更新集合的索引信息
  3. 写入一条oplog
  4. 写journal日志
注意:MongoDB的oplog主要用于主从复制,类似于MySQL的binlog,客户端将数据写入Primary,Primary写入数据后记录一条oplog,Secondary从Primary(或其它Secondary)拉去oplog并重放,来确保复制集里每个节点存储相同的数据。oplog在MongoDB里是一个capped collection(一种特殊的集合,类似于固定大小的FIFO队列,如果集合已满,新的文档会覆盖最旧的文档,其可以提供较高的读写性能,而且可保证顺序),对于存储引擎来说,oplog只是一部分普通的数据而已。

上面三个操作要么都成功,要么都失败,即要确保MongoDB写操作的原子性。

而在集群环境下,数据只写入一个节点,并不能保证数据安全。比如一条数据在写入Primary后,就挂了,数据还未同步到Secondary到,这时候,MongoDB会重新选举主节点,新的主节点中并没有最新插入的数据,这条数据就丢失了。

MongoDB提供了一种写入安全机制来解决上述问题,即:Write Concern,其描述了MongoDB写入数据到单实例、副本集以及分片集群时,何时响应客户端。WriteConcern的取值包括:

WriteConcern行为

这张图展示了,WriteConcern不同取值时,MongoDB响应客户端的时机。默认情况下,主要Primary写入成功,即响应客户端。而这整个过程中,客户端将被阻塞,直到整个写操作完成(写操作达到指定节点数为止)。另外,需要注意的是,WriteConcern是客户端设置,要么在MongoDB的连接字符串中指定,要么在代码中设置。

WriteConcern可以决定写操作到达多少个节点才算成功,journal则定义如何才算成功,其取值包括:

mongod实例每次启动时都会检查journal日志文件,查看是否有数据需要恢复,虽然写journal文件对写入性能有一定影响,但为保证数据安全,生产环境中开启journaling选项是很有必要的。而且从4.0开始,已经不提供关闭选项,即每次写操作均会写journal日志。

最后,使用WriteConcern时需要注意:

读操作事务

与写操作类似,读取数据时我们需要关注以下两个问题:

ReadPreference

首先来看ReadPreference,其决定读取的数据由哪个节点提供,可取值包含:

如果是需要新增后马上就需要准确查询的,建议使用primary/primaryPreferred,比如用户下单后需要跳转到详情页面,此时,从节点可能还没有复制到新订单。
如果是查询历史数据,其对时效性要求不高,建议使用secondary/secondaryPreferred,比如用户查询自己的历史订单。
如果是生成报表,其对时效性要求不高,但资源需求大,不建议到主节点去查,避免对线上用户造成影响,建议使用secondary。
如果有些数据需要分发到全世界各地,这时使用nearest,每个地区的应用选择最近的节点读取数据。

ReadPreference只能控制使用一类节点,如果还想更灵活的控制读取节点,可以给某几个节点打上Tag,然后从指定Tag上读取数据。比如有一个5个节点的副本集,3个节点硬件较好,用于服务线上客户;2个节点较差,用于生成报表,就可以使用Tag来达到这个目的:

ReadPreference可以通过如下方式配置:

// 通过 MongoDB 的连接串参数: 
mongodb://host1:27107,host2:27107,host3:27017/?replicaSet=rs&readPreference=secondary 
// 通过 MongoDB 驱动程序 API: 
MongoCollection.withReadPreference(ReadPreferencereadPref) 
// Mongo Shell: 
db.collection.find({}).readPref(“secondary”) 

在使用ReadPreference和Tag时应当注意高可用问题,比如将readPreference指定为primary,当主节点发生故障时,在选举期间,将没有节点可读。如果业务允许,最好选择primaryPreferred。

Tag也存在同样的问题,如果某个Tag只有一个节点,当这个节点故障时,将无节点可读。有时候这个结果可以接受,有时候不行,应当合理配置,比如:

另外,Tag有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它成为主节点,则优先级应设为0。

ReadConcern

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

在复制集中local和available是没有区别的。两者的区别主要体现在分片集上。考虑一下场景:

  1. 一个chunk x(chunk:块,包含多个文档)正从shard1迁移到shard2
  2. 整个迁移过程中,chunk x的一部分数据在shard1,另外一部分已迁移的数据在shard2

此时,源分片shard1仍然是chunk x的负责方,所有多chunk x的读写操作仍然进入shard1,这时候,如果有请求读取shard2,则会体现出local和availabe的区别:

虽然看上去应该总是选择local,但对结果集的过滤会造成额外消耗,在一些无关紧要的场景,比如统计下,可以考虑使用available。还有一点需要注意的是,主从节点readConcern的默认值是不同的。从主节点读取数据时默认是local,去从节点读取数据时默认是available(向前兼容原因)。

readConcern为majority时,读取在大多数节点上提交完成的数据,这个没啥好说的。

readConcern为linearizable时,也是读取大多数节点确认过的数据,和majority的区别是,linearizable可以保证绝对操作时序,即写操作后面的读,一定能够读到之前写的数据。因此,其只对读取单个文档时有效,且可能出现严重的耗时,毕竟它需要等待前面的写操作完成。

readConcern为snapshot只在多文档事务中生效。将一个事务的readConcern设置为snapshot,将保证在事务中的读:

参考资料

MongoDB副本集实战
MongoDB分片集群实战
MongoDB journal与oplog,究竟谁先写入?

本文内容参考:极客时间 - MongoDB高手课,对原课程中的相关内容作了增补

Comments

Post a Message

人生在世,错别字在所难免,无需纠正。

提交评论