DBA MongoDB 事务相关
本篇前言
MongoDB在4.0版本之后已经完美支持事务了。
因此MongoDB可以用作核心业务的数据库,这是其他NoSQL产品望尘莫及的,也是MongoDB的一大特性。
在前面的文章中,我们介绍了MongoDB复制集的搭建,在本章节中我们将着重介绍MongoDB如何保证复制集中各个节点数据一致性,并且对读写分离进行介绍。
那么,Lets’ go!!
writeConcern
功能概述
写关注,这个参数名字用的很好,在MongoDB中,当主库发生变更操作时,writeConcern参数决定该变更操作作用到多少节点上才算成功。
它的可选值如下所示:
0 : 主库发起变更操作,不关心是否成功被从库复制,此变更操作视为完成
1 : 主库发起变更操作,该操作必须被指定节点数成功复制后才算该操作完成
majority : 主库发起变更操作,该操作必须被大多数节点成功复制后才算该操作完成
all : 主库发起变更操作,该操作必须被所有节点成功复制后才算该操作完成
默认行为
默认行为是0,并且在3个节点都不做任何设置的情况下,如图所示:
虚线部位,有可能从库复制了,有可能还没来得及复制。
majority
主库发起变更操作,该操作必须被大多数节点成功复制后才算该操作完成,如图所示:
all
主库发起变更操作,该操作必须被所有节点成功复制后才算该操作完成,如图所示:
journal
writeConcern参数决定变更操作到达多少节点才算成功。
而journal参数则定义如何才算成功,如下所示:
true : 写操作必须落到journal文件中才算成功
false : 写操作到达内存即算作成功
说实话,这个journal日志好像是与MySQL中的redolog很像,下面是官方定义说明:
WiredTiger使用检查点提供磁盘上数据的一致视图,并允许MongoDB从上一个检查点恢复。但是,如果MongoDB在检查点之间意外退出,则需要日志记录以恢复在最后一个检查点之后发生的信息。
这玩意儿是WiredTiger所提供的预写日志,并不是常说的工作日志
如果该参数为true,则如下图所示:
readPreference
功能概述
这个参数名也很有趣,读的偏好,其实说白了就是读写分离中应用需要从哪个节点库读数据的指定方式。
它的可选值如下:
primary :从主节点读,并且只从主节点读
primaryPreferred :优先选择主节点读,当主节点不可用时则选择从节点
secondary :只选择从节点
secondaryPreferred :优先选择从节点读,当从节点不可用时则选择主节点
nearest : 选择最近的节点,无论主从
场景选择
用户下单后立马要跳转到订单详情页,此时选择主节点读或者优先从主节点读,因为此时从节点可能还未复制到订单信息。
用户查询自己下过的订单,可以选择在从节点上读或者优先从从节点上读,查询历史订单对时效性通常没有太高的要求。
生成报表,选择从节点读取,生成报表的操作对时效性要求不高,但是对资源需求大,可在从节点单独处理,避免对线上用户造成影响。
将用户上传的图片分发到全世界,让各地用户都能就近读取,此时选择最近读。
标签读取
标签读取常用于在一个集群中不同的节点具有不同功能的情况下使用。
如下图所示,有5个节点的复制集,3个节点是属于OLTP,服务前台线上应用,2个节点属于OLAP,专门做数据分析或者处理,此时就可以为这个集群中的2组节点打上不同的标签:
在读取不同数据时,通过标签读取指定组内的数据。
readConcern
功能概述
读关注,在readPreference指定了读取节点后,readConcern决定读取这个节点上的那些数据,与关系型数据库的隔离级别相似,具体如下:
available : 读取所有可用的数据
local : 读取所有可用,且属于当前分片的数据
majority : 读取在大多数节点上提交完成的数据(类似于可重复读的界别,也是MySQL默认级别)
linearizable :串行化读取
snapshot : 读取最近快照中的数据
available与local
在复制集中,available与local是没有区别的,两者区别主要体现在分片集上。
考虑以下场景:
- 一个chunk x正在从shard1向shard2迁移
- 整个迁移过程中chunk x中的部分数据会在shard1和shard2中同时存在,但源分片shard1仍然是chunk x的负责方:
- 所有对chunk x的读写操作仍然进入shard1
- config中记录的信息chunk x仍然属于shard1
- 如果此时读取shard2,则会体现出available与local的区别:
- local :只取应该由shard2负责的数据,则不包含chunk x
- available : shard2上有什么就读什么,包括chunk x
在两个参数的注意事项:
- 虽然看上去总是应该选择local,但毕竟对结果集进行过滤会造成额外消耗,在一些无关紧要的场景下(例如统计),也可以考虑available
- MongoDB版本小于或等于3.6不支持对从节点使用
- 从主节点读取数据时默认readConcern是local,在从节点读取数据时默认readConcern是available
majority
从一个节点中读取数据时,该参数表示只读取在大多数节点上提交完成的数据。
通过MVCC机制在所有节点上维护了一个版本快照,在指定该参数读取时只有被大多数节点确认过的数据才会加入这个版本快照。
快照持续到没有人使用时才会被删除。
总之majority是一个非常不错的选择项,它和local的区别如下:
- 主库读的时候,local可以直接查询到写入的数据
- 主库读的时候,majority则只能查询到已经被多数节点确认过的数据
并且使用majority可以有效避免脏读。
linearizable
串行化读取,效率较慢,不做介绍。
snapshort
只有在多文档事务中生效。
如果设置为该级别,将不会出现幻读,不会出现脏读,不会出现不可重复读。
因为所有的读都使用同一个快照,直到事务提交时该快照才会被释放。
三者使用
writeConcern
一般来讲,对于writeConcern参数我们设置为majority即可,这是性能与安全的最中和的选择。
一定不要将writeConcern设置为等于总节点数,因为一旦有一个节点故障,所有写操作都将失败。
writeConcern参数虽然会增加些操作的延迟时间,但并不会显著增加集群压力,因此无论是否等待,变更操作都最终会将作用到所有节点上,设置writeConcern参数只是为了让变更操作等待复制后再返回而已。换而言之,该参数缩减了非延迟从库与主库之间的oplog的差距,与半同步复制有相似之处。
通过MongoDB的链接串参数:
mongodb://主库地址:端口,从库地址:端口,从库地址:端口/?authSource=验证库&replicaSet=复制集名称&w=majority&wtimeoutMS=5000
# wtimeoutMS写入的超时时间,单位是毫秒
在shell中指定每次写入操作的writeConcern:
db.collection.insert({ "k" : "v" }, {"writeConcern" : {"w" : "majority", "j" : true}})
# 可在update,insert中指定
# j就是journal
readPreference
通过MongoDB的链接串参数:
mongodb://主库地址:端口,从库地址:端口,从库地址:端口/?authSource=验证库&replicaSet=复制集名称&readPreference=secondary
在shell中指定每次读取操作的readPref:
db.collection.find().readPref("secondary")
在使用时,要注意以下事项:
- 指定readPreference时也要注意高可用问题,例如将readPreference指定为primary,则在故障转移期间导致没有节点可读,如果业务允许,尽量选择primaryPreferred。
- 使用tag时也会遇到同样的问题,如果只有一个节点拥有一个特定的tag,则再这个节点失效时将导致无节点可读,如线上节点的tag应该保持多组,具有同样的tag
readConcern
通过MongoDB的链接串参数:
mongodb://主库地址:端口,从库地址:端口,从库地址:端口/?authSource=验证库&replicaSet=复制集名称&readConcernLevel=majority
在shell中指定每次读取操作的readConcern:
db.collection.find().readConcern("level" : 'majority' )
ACID多文档事务
使用说明
以下是多文档事务的使用说明:
var session = db.getMongo().startSession()
session.startTransaction({readConcern: { level: 'majority' },writeConcern: { w: 'majority' }})
var coll = session.getDatabase('test').getCollection('user');
coll.update({name: 'Jack'}, {$set: {age: 18}})
// 成功提交事务
session.commitTransaction();
// 失败事务回滚
session.abortTransaction();