分布式技术MongoDB

  • 1. 分片简介
  • 2. MongoDB分片集群架构
  • 3. 环境搭建
    • 3.1 分片集群搭建
    • 3.2 使用mtools搭建分片集群
  • 4 使用分片集群
  • 5. 分片策略
    • 5.1 什么是chunk
    • 5.2 分片算法
    • 5.3 哈希分片
    • 5.4 分片标签
    • 5.4 分片键(ShardKey)的选择
    • 5.5 分片键(ShardKey)的约束
  • 6. 数据均衡
    • 6.1 均衡的方式
      • 6.1.1 手动均衡
      • 6.1.2 自动均衡
    • 6.2 chunk分裂
      • 6.2.1 自动均衡
    • 6.3 廷移阈值
    • 6.4 迁移速度
    • 6.5 数据均衡带来的问题
  • 7. MongoDB多文档事务详解
    • 7.1 事务简介
    • 7.2 MongoDB多文档事务
    • 7.3 writeConcern
    • 7.4 readPreference
    • 7.5 readPreference场景举例
    • 7.6 readPreference配置
    • 7.7 从节点读测试
    • 7.8 readConcern

本文是按照自己的理解进行笔记总结,如有不正确的地方,还望大佬多多指点纠正,勿喷。

课程内容:

1.MongoDB分片集群架构及其原理分析

2.MongoDB分片集群环境搭建

3.MongoDB分片策略和数据均衡详解

4.写事务之writeConcern实战

5.读事务之readPreference&readConcern实战

6.MongoDB多文档事务详解

分片集群架构

1. 分片简介

分片(shard)是指在将数据进行水平切分之后,将其存储到多个不同的服务器节点上的一种扩展方式。分片在概念上非常类似于应用开发中的“水平分表”。不同的点在于,MongoDB本身就自带了分片管理的能力,对于开发者来说可以做到开箱即用。

为什么要使用分片?

MongoDB复制集实现了数据的多副本复制及高可用,但是一个复制集能承载的容量和负载是有限的。 在你遇到下面的场景时,就需要考虑使用分片了∶

  • 存储容量需求超出单机的磁盘容量。
  • 活跃的数据集超出单机内存容量,导致很多请求都要从磁盘读取数据,影响性能。
  • 写IOPS超出单个MongoDB节点的写服务能力。

垂直扩容(Scale Up)VS水平扩容(Scale Out) :

  • 垂直扩容:用更好的服务器,提高CPU处理核数、内存数、带宽等
  • 水平扩容:将任务分配到多台计算机上

2. MongoDB分片集群架构

MongoDB分片集群(Sharded Cluster)是对数据进行水平扩展的一种方式。MongoDB使用分片集群来支持大数据集和高吞吐量的业务场景。在分片模式下,存储不同的切片数据的节点被称为分片节点,一个分片集群内包含了多个分片节点。当然,除了分片节点,集群中还需要一些配置节点、路由节点,以保证分片机制的正常运作。

核心概念

  • 数据分片:分片用于存储真正的数据,并提供最终的数据读写访问。分片仅仅是一个逻辑的概念,它可以是一个单独的mongod实例,也可以是一个复制集。图中的Shard1、Shard2都是一个复制集分片。在生产环境中也一般会使用复制集的方式,这是为了防止数据节点出现单点故障。

  • 配置服务器(Config Server):配置服务器包含多个节点,并组成一个复制集结构,对应于图中的ConfigRepISet。配置复制集中保存了整个分片集群中的元数据,其中包含各个集合的分片策略,以及分片的路由表等。如果我们查询一个数据,到底在哪个Shard里面,就是通过这个配置知道的。

  • 查询路由(mongos) : mongos是分片集群的访问入口,其本身并不持久化数据。mongos启动后,会从配置服务器中加载元数据。之后mongos开始提供访问服务,并将用户的请求正确路由到对应的分片。在分片集群中可以部署多个mongos以分担客户端请求的压力。

3. 环境搭建

3.1 分片集群搭建

环境准备

  • 3台Linux虚拟机,准备MongoDB环境,配置环境变量。
  • 一定要版本一致(重点),当前使用version4.4.9


3+3+3+2=11,也就是MongoDB生产环境至少要11台机器。

配置域名解析
在3台虚拟机上执行以下命令,注意替换实际IP地址

echo "192.168.65.97 mongo1 mongo01.com mongo02.com" >> /etc/hostsecho "192.168.65.190 mongo2 mongo03.com mongo04.com" >> /etc/ hostsecho "192.168.65,200 mongo3 mongo05.com mongo06.com" >> /etc/ hosts

准备分片目录

在各服务器上创建数据目录,我们使用`/data’,请按自己需要修改为其他目录:在mongo01.com / mongo03.com / mongo05.com 上执行以下命令:

mkdir -p /data/shard1/db /data/shard1/log /data/config/db /data/config/log

在mongo02.com / mongo04.com / mongo06.com 上执行以下命令:

mkdir -p /data/shard2/db /data/shard2/log /data/mongos/

创建第一个分片用的复制集

在mongo01.com / mongo03.com / mongo05.com 上执行以下命令:

mongod --bind_ip 0.0.0.0 --replSet shard1 --dbpath /data/shard1/db \--logpath /data/shard1/log/mongod.log --port 27010 --fork \--shardsvr --wiredTigerCacheSizeGB 1

–shardsvr 声明这是集群的一个分片

–wiredTigerCacheSizeGB 设置内存大小

初始化第一个分片复制集

# 进入mongo shellmongo mongo01.com:27010#shard1复制集节点初始化rs.initiate({_id: "shard1","members" : [{"_id": 0,"host" : "mongo01.com:27010"},{"_id": 1,"host" : "mongo03.com:27010"},{"_id": 2,"host" : "mongo05.com:27010"}]})#查看复制集状态rs.status()

创建 config server 复制集

在mongo01.com / mongo03.com / mongo05.com上执行以下命令:

mongod --bind_ip 0.0.0.0 --replSet config --dbpath /data/config/db \--logpath /data/config/log/mongod.log --port 27019 --fork \--configsvr --wiredTigerCacheSizeGB 1

初始化 config server 复制集

# 进入mongo shellmongo mongo01.com:27019#config复制集节点初始化rs.initiate({_id: "config","members" : [{"_id": 0,"host" : "mongo01.com:27019"},{"_id": 1,"host" : "mongo03.com:27019"},{"_id": 2,"host" : "mongo05.com:27019"}]})

搭建 mongos
在mongo01.com / mongo03.com / mongo05.com上执行以下命令:

#启动mongos,指定config复制集mongos --bind_ip 0.0.0.0 --logpath /data/mongos/mongos.log --port 27017 --fork \--configdb config/mongo01.com:27019,mongo03.com:27019,mongo05.com:27019

mongos加入第1个分片

# 连接到mongosmongo mongo01.com:27017#添加分片mongos>sh.addShard("shard1/mongo01.com:27010,mongo03.com:27010,mongo05.com:27010")#查看mongos状态mongos>sh.status()

创建分片集合

连接到mongos, 创建分片集合mongo mongo01.com:27017mongos>sh.status()#为了使集合支持分片,需要先开启database的分片功能mongos>sh.enableSharding("company")# 执行shardCollection命令,对集合执行分片初始化mongos>sh.shardCollection("company.emp", {_id: 'hashed'})mongos>sh.status() #插入测试数据 use companyfor (var i = 0; i < 10000; i++) {db.emp.insert({i: i});}#查询数据分布db.emp.getShardDistribution()

创建第2个分片的复制集
在mongo02.com / mongo04.com / mongo06.com上执行以下命令:

mongod --bind_ip 0.0.0.0 --replSet shard2 --dbpath /data/shard2/db\--logpath /data/shard2/log/mongod.log --port 27011 --fork \--shardsvr --wiredTigerCacheSizeGB 1

初始化第二个分片的复制集

# 进入mongo shellmongo mongo06.com:27011#shard2复制集节点初始化rs.initiate({_id: "shard2","members" : [{"_id": 0,"host" : "mongo06.com:27011"},{"_id": 1,"host" : "mongo02.com:27011"},{"_id": 2,"host" : "mongo04.com:27011"}]})#查看复制集状态rs.status()

mongos加入第2个分片

# 连接到mongosmongo mongo01.com:27017#添加分片mongos>sh.addShard("shard2/mongo02.com:27011,mongo04.com:27011,mongo06.com:27011")#查看mongos状态mongos>sh.status()

3.2 使用mtools搭建分片集群

mtools介绍
mtools是一套基于Python实现的MongoDB工具集,其包括MongoDB日志分析、报表生成及简易的数据库安装等功能。它由MongoDB原生的工程师单独发起并做开源维护,目前已经有大量的使用者。

mtools所包含的一些常用组件如下:

  • mlaunch支持快速搭建本地测试环境,可以是单机、副本集、分片集群。
  • mlogfilter日志过滤组件,支持按时间检索慢查询、全表扫描操作,支持通过多个属性进行信息过滤,支持输出为JSON格式。
  • mplotqueries支持将日志分析结果转换为图表形式,依赖tkinter(Python图形模块)和matplotlib模块。
  • mlogvis支持将日志分析结果转换为一个独立的HTML页面,实现与mplotqueries同样的功能。
ToolsDescription
mlogfilter合并、分割日志文件,过滤慢查询,集合扫描,格式转换等
mloginfo统计日志内的数据库信息(启停、连接、集群状态等)
mplotqueries日志转化为图表形式
mlogvis日志转化为HTML页面,与mplotqueries类似
mlaunch快速搭建本地测试环境(单机、集群、分片)

安装mtools

环境准备

  • mtools需要调用MongoDB的二进制程序来启动数据库,因此需保证Path路径中包含{MONGODB_HOME}/bin这个目录
  • 需要安装Python环境,需选用Python 3.7、3.8、3.9版本。 Centos7安装Python3.9

pip安装
安装依赖

pip3 install python-dateutilpip3 install psutil pymongo 

安装mtools

pip3 install mtools

通过源码安装

wget https://github.com/rueckstiess/mtools/archive/refs/tags/v1.6.4.tar.gz#解压后进入mtoolspython setup.py install

使用mtools创建复制集

#准备复制集使用的工作目录mkdir -p /data/mongocd /data/mongo#初始化3节点复制集mlaunch init --replicaset --nodes 3

端口默认从27017开始,依次为2017,27018,27019

查看复制集状态

#准备分片集群使用的工作目录mkdir /data/mongo-clustercd /data/mongo-cluster/# 执行mlaunch init初始化集群mlaunch init --sharded 2 --replicaset --node 3 --config 3 --csrs --mongos 3 --port 27050 

选项说明

  • –sharded 2:启用分片集群模式,分片数为2。
  • –replicaset –nodes 3:采用3节点的复制集架构,即每个分片为一致的复制集模式。
  • –config 3 –csrs:配置服务器采用3节点的复制集架构模式,–csrs是指Config Server as a Replica Set
  • –mongos 3:启动3个mongos实例进程。
  • –port 27050:集群将以27050作为起始端口,集群中的各个实例基于该端口向上递增。
  • –noauth:不启用鉴权。
  • –arbiter 向复制集中添加一个额外的仲裁器
  • –single 创建单个独立节点
  • –dir 数据目录,默认是./data
  • –binarypath 如果环境有二进制文件,则不用指定
    如果执行成功,那么片刻后可以看到如下输出:

检查分片实例

mlaunch list命令可以对当前集群的实例状态进行检查

# 显示标签mlaunchlist --tags # 显示启动命令mlaunchlist --startup

连接mongos,查看分片实例的情况

mongo --port 27050mongos> db.adminCommand({listShards:1})

停止、启动
如果希望停止集群,则可以使用mlaunch stop命令

再次启动集群,可以使用mlaunch start命令

使用mtools搭建测试集群是相当方便的,相比手工搭建的方式可缩减大量的时间。

4 使用分片集群

为了使集合支持分片,需要先开启database的分片功能

sh . enablesharding("shop")

执行shardCollection命令,对集合执行分片初始化

sh.shardCollection("shop.product",{productId:"hashed",false,{numInitialChunks:4})

shop.product集合将productld作为分片键,并采用了哈希分片策略,除此以外,“numInitialChunks:4″表示将初始化4个chunk。numInitialChunks必须和哈希分片策略配合使用。而且,这个选项只能用于空的集合,如果已经存在数据则会返回错误。

向分片集合写入数据

向shop.product集合写入一批数据

db = db.getSiblingDB("shop");var count = 0;for (var i = 0; i < 1000; i++) {var p = [];for (var j = 0; j < 100; j++) {p.push({"productId": "P-" + i + "-" + j,name: "羊毛衫",tags: [{tagKey: "size", tagValue: ["L", "XL", "XXL"] },{tagKey: "color", tagValue: ["蓝色", "杏色"] },{tagKey: "style", tagValue: "韩风" }]});}count+=p.length;db.product.insertMany(p);print("insert",count)}

查询数据的分布

db.product.getShardDistribution()

5. 分片策略

通过分片功能,可以将一个非常大的集合分散存储到不同的分片上,如图:

假设这个集合大小是1TB,那么拆分到4个分片上之后,每个分片存储256GB的数据。这个当然是最理想化的场景,实质上很难做到如此绝对的平衡。一个集合在拆分后如何存储、读写,与该集合的分片策略设定是息息相关的。在了解分片策略之前,我们先来介绍一下chunk。

5.1 什么是chunk

chunk的意思是数据块,一个chunk代表了集合中的“一段数据”,例如,用户集合(db.users)在切分成多个chunk之后如图所示:

chunk所描述的是范围区间,例如,db.users使用了userld作为分片键,那么chunk就是userld的各个值(或哈希值)的连续区间。集群在操作分片集合时,会根据分片键找到对应的chunk,并向该chunk所在的分片发起操作请求,而chunk的分布在一定程度上会影响数据的读写路径,这由以下两点决定:

  • chunk的切分方式,决定如何找到数据所在的chunk

  • chunk的分布状态,决定如何找到chunk所在的分片

5.2 分片算法

chunk切分是根据分片策略进行实施的,分片策略的内容包括分片键和分片算法。当前,MongoDB支持两种分片算法:

范围分片(range sharding)

假设集合根据x字段来分片,x的完整取值范围为minKey, maxKey,其将整个取值范围划分为多个chunk,例如:

  • chunk1包含x的取值在[minKey,-75)的所有文档。
  • chunk2包含x取值在[-75,25)之间的所有文档,依此类推。

范围分片能很好地满足范围查询的需求,比如想查询x的值在[-30,10]之间的所有文档,这时mongos直接将请求定位到chunk2所在的分片服务器,就能查询出所有符合条件的文档。范围分片的缺点在于,如果Shard Key有明显递增〈或者递减)趋势,则新插入的文档会分布到同一个chunk,此时写压力会集中到一个节点,从而导致单点的性能瓶颈。一些常见的导致递增的Key如下:

  • 时间值。
  • Objectld,自动生成的_id由时间、计数器组成。
  • UUID,包含系统时间、时钟序列。
  • 自增整数序列。

5.3 哈希分片

哈希分片会先事先根据分片键计算出一个新的哈希值(64位整数),再根据哈希值按照范围分片的策略进行chunk的切分。适用于日志,物联网等高并发场景。

哈希分片与范围分片是互补的,由于哈希算法保证了随机性,所以文档可以更加离散地分布到多个chunk上,这避免了集中写问题。然而,在执行一些范围查询时,哈希分片并不是高效的。因为所有的范围查询都必然导致对所有chunk进行检索,如果集群有10个分片,那么mongos将需要对10个分片分发查询请求。哈希分片与范围分片的另一个区别是,哈希分片只能选择单个字段,而范围分片允许采用组合式的多字段作为分片键。

哈希分片仅支持单个字段的哈希分片:

{ x : "hashed" }{x : 1 , y : "hashed" } // 4.4 new

4.4以后的版本,可以将单个字段的哈希分片和一个到多个的范围分片键字段来进行组合,比如指定x:1,y是哈希的方式。

5.4 分片标签

MongoDB允许通过为分片添加标签(tag)的方式来控制数据分发。一个标签可以关联到多个分片区间(TagRange)。均衡器会优先考虑chunk是否正处于某个分片区间上(被完全包含),如果是则会将chunk迁移到分片区间所关联的分片,否则按一般情况处理。

分片标签适用于一些特定的场景。例如,集群中可能同时存在OLTP和OLAP处理,一些系统日志的重要性相对较低,而且主要以少量的统计分析为主。为了便于单独扩展,我们可能希望将日志与实时类的业务数据分开,此时就可以使用标签。

为了让分片拥有指定的标签,需执行addShardTag命令

sh.addShardTag("shard01","oltp")sh.addShardTag("shard02","oltp")sh.addShardTag("shard03","olap")

实时计算的集合应该属于oltp标签,声明TagRange

sh.addTagRange("main.devices",{shardKey:MinKey},{shardKey:MaxKey},"oltp")


而离线计算的集合,则属于olap标签

sh.addTagRange("other.systemLogs",{shardKey:MinKey},{shardKey:MaxKey},"olap")

main.devices集合将被均衡地分发到shard01、shard02分片上,而other.systemLogs集合将被单独分发到shard03分片上。

5.4 分片键(ShardKey)的选择

在选择分片键时,需要根据业务的需求及范围分片、哈希分片的不同特点进行权衡。一般来说,在设计分片键时需要考虑的因素包括:

  • 分片键的基数(cardinality),取值基数越大越有利于扩展。

    • 以性别作为分片键︰数据最多被拆分为2份

    • 以月份作为分片键︰数据最多被拆分为12份

  • 分片键的取值分布应该尽可能均匀。

  • 业务读写模式,尽可能分散写压力,而读操作尽可能来自一个或少量的分片。

  • 分片键应该能适应大部分的业务操作。

5.5 分片键(ShardKey)的约束

ShardKey必须是一个索引。非空集合须在ShardCollection前创建索引;空集合ShardCollection自动创建索引

4.4版本之前:

  • ShardKey 大小不能超过512 Bytes;
  • 仅支持单字段的哈希分片键;
  • Document中必须包含ShardKey;
  • ShardKey包含的Field不可以修改。

4.4版本之后:

  • ShardKey大小无限制;
  • 支持复合哈希分片键;
  • Document 中可以不包含ShardKey,插入时被当做Null处理;
  • 为ShardKey添加后缀refineCollectionShardKey命令,可以修改ShardKey包含的Field;

而在4.2版本之前,ShardKey对应的值不可以修改;4.2版本之后,如果ShardKey为非_ID字段,那么可以修改ShardKey对应的值。

6. 数据均衡

6.1 均衡的方式

一种理想的情况是,所有加入的分片都发挥了相当的作用,包括提供更大的存储容量,以及读写访问性能。因此,为了保证分片集群的水平扩展能力,业务数据应当尽可能地保持均匀分布。这里的均匀性包含以下两个方面:

  1. 所有的数据应均匀地分布于不同的chunk上。
  2. 每个分片上的chunk数量尽可能是相近的。

其中,第1点由业务场景和分片策略来决定,而关于第2点,
我们有以下两种选择:

6.1.1 手动均衡

一种做法是,可以在初始化集合时预分配一定数量的chunk(仅适用于哈希分片),比如给10个分片分配1000个chunk,那么每个分片拥有100个chunk。另一种做法则是,可以通过splitAt、moveChunk命令进行手动切分、迁移。

6.1.2 自动均衡

开启MongoDB集群的自动均衡功能。均衡器会在后台对各分片的chunk进行监控,一旦发现了不均衡状态就会自动进行chunk的搬迁以达到均衡。其中,chunk不均衡通常来自于两方面的因素:

  • 一方面,在没有人工干预的情况下,chunk会持续增长并产生分裂(split),而不断分裂的结果就会出现数量上的不均衡;
  • 另一方面,在动态增加分片服务器时,也会出现不均衡的情况。自动均衡是开箱即用的,可以极大简化集群的管理工作。

6.2 chunk分裂

在默认情况下,一个chunk的大小为64MB,该参数由配置的chunksize参数指定。如果持续地向该chunk写入数据,并导致数据量超过了chunk大小,则MongoDB会自动进行分裂,将该chunk切分为两个相同大小的chunk。

chunk分裂是基于分片键进行的,如果分片键的基数太小,则可能因为无法分裂而会出现jumbo chunk(超大块)的问题。例如,对db.users使用gender(性别)作为分片键,由于同一种性别的用户数可能达到数千万,分裂程序并不知道如何对分片键(gender)的一个单值进行切分,因此最终导致在一个chunk上集中存储了大量的user记录(总大小超过64MB)。
jumbo chunk对水平扩展有负面作用,该情况不利于数据的均衡,业务上应尽可能避免。一些写入压力过大的情况可能会导致chunk多次失败(split),最终当chunk中的文档数大于1.3×avgObjectSize时会导致无法迁移。此外在一些老版本中,如果chunk中的文档数超过250000个,也会导致无法迁移。

6.2.1 自动均衡

MongoDB的数据均衡器运行于Primary Config Server(配置服务器的主节点)上,而该节点也同时会控制chunk数据的搬迁流程。

流程说明:

  • 分片shardO在持续的业务写入压力下,产生了chunk分裂。

  • 分片服务器通知Config Server进行元数据更新。

  • Config Server的自动均衡器对chunk分布进行检查,发现shard0和shard1的chunk数差异达到了阈值,向shard0下发moveChunk命令以执行chunk迁移。

  • shard0执行指令,将指定数据块复制到shard1。该阶段会完成索引、 chunk数据的复制,而且在整个过程中业务侧对数据的操作仍然会指向shard0;所以,在第一轮复制完毕之后,目标shard1会向shard0确认是否还存在增量更新的数据,如果存在则继续复制。

  • shard0完成迁移后发送通知,此时Config Server开始更新元数据库,将chunk的位置更新为目标shard1。在更新完元数据库后并确保没有关联cursor的情况下,shard0会删除被迁移的chunk副本。

  • Config Server通知mongos服务器更新路由表。此时,新的业务请求将被路由到shard1。

6.3 廷移阈值

均衡器对于数据的“不均衡状态”判定是根据两个分片上的chunk个数差异来进行的

chunk个数迁移阈值
少于202
20~794
80及以上8

6.4 迁移速度

数据均衡的整个过程并不是很快,影响MongoDB均衡速度的几个选项如下:

  • _secondaryThrottle:用于调整迁移数据写到目标分片的安全级别。如果没有设定,则会使用w:2选项,即至少一个备节点确认写入迁移数据后才算成功。从MongoDB 3.4版本开始,_secondaryThrottle被默认设定为false, chunk迁移不再等待备节点写入确认。
  • _waitForDelete:在chunk迁移完成后,源分片会将不再使用的chunk删除。如果_waitForDelete是true,那么均衡器需要等待chunk同步删除后才进行下一次迁移。该选项默认为false,这意味着对于旧chunk的清理是异步进行的。
  • 并行迁移数量:在早期版本的实现中,均衡器在同一时刻只能有一个chunk迁移任务。从MongoDB 3.4版本开始,允许n个分片的集群同时执行n/2个并发任务。

随着版本的迭代,MongoDB迁移的能力也在逐步提升。从MongoDB 4.0版本开始,支持在迁移数据的过程中并发地读取源端和写入目标端,迁移的整体性能提升了约40%。这样也使得新加入的分片能更快地分担集群的访问读写压力。

6.5 数据均衡带来的问题

数据均衡会影响性能,在分片间进行数据块的迁移是一个“”繁重”的工作,很容易带来磁盘/O使用率飙升,或业务时延陡增等一些问题。因此,建议尽可能提升磁盘能力,如使用SSD。除此之外,我们还可以将数据均衡的窗口对齐到业务的低峰期以降低影响。

登录mongos,在config数据库上更新配置,代码如下:

use configsh.setBalancerState(true)db.settings.update({_id:"balancer"},{$set:{activewindow:{start:"02:00",stop:"04:00"}}},{upsert:true})

在上述操作中启用了自动均衡器,同时在每天的凌晨2点到4点运行数据均衡操作

对分片集合中执行count命令可能会产生不准确的结果,mongos在处理count命令时会分别向各个分片发送请求,并累加最终的结果。如果分片上正在执行数据迁移,则可能导致重复的计算。替代办法是使用db.collection.countDocuments({})方法,该方法会执行聚合操作进行实时扫描,可以避免元数据读取的问题,但需要更长时间。

在执行数据库备份的期间,不能进行数据均衡操作,否则会产生不一致的备份数据。在备份操作之前,可以通过如下命令确认均衡器的状态:

  1. sh.getBalancerState():查看均衡器是否开启。
  2. sh.isBalancerRunning():查看均衡器是否正在运行。
  3. sh.getBalancerWind@w():查看当前均衡的窗口设定。

7. MongoDB多文档事务详解

7.1 事务简介

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

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

7.2 MongoDB多文档事务

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

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

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

使用事务的原则

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

MongoDB对事务支持

事务属性支持程度
Atomocity 原子性单表单文档:1.x就支持,复制集多表多行:4.0分片集群多表多行:4.2
Consistency 一致性writeConcern, readConcern (3.2)
lsolation隔离性readConcern (3.2)
Durability 持久性Journal and Replication

使用方法

MongoDB多文档事务的使用方式与关系数据库非常相似:

try(ClientSession clientSession = client.startSession()){clientSession.startTransaction();collection.insertOne(clientSession,docOne);collection.insertOne(clientSession,docTwo);clientSession.commitTransaction() ;}

7.3 writeConcern

https://docs.mongodb.com/manual/reference/write-concern/

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

语法格式:

{w:<value>,j:<boolean>,wtimeout:<number>}
  1. w:数据写入到number个节点才向用客户端确认
    • {w:0}对客户端的写入不需要发送任何确认,适用于性能要求高,但不关注正确性的场景
    • {w: 1}默认的writeConcern,数据写入到Primary就向客户端发送确认
    • {w: “majority”}数据写入到副本集大多数成员后向客户端发送确认,适用于对数据安全性要求比较高的场景,该选项会降低写入性能

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

    • 默认为{j: false},如果要求Primary写入持久化了才向客户端确认,则指定该选项为true
  2. wtimeout: 写入超时时间,仅w的值大于1时有效。

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

思考:对于5个节点的复制集来说,写操作落到多少个节点上才算是安全的" />db.user.insertone({name: "李四"},{writeConcern:{w:"majority"}})#等待延迟节点写入数据后才会响应db.user.insertOne({name:"王五"},{writeconcern:{w:3}})#超时写入失败db.user.insertOne({name:"小明"}, {writeConcern:{w:3,wtimeout:3000}})

注意事项

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

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

  • 从哪里读?
  • 什么样的数据可以读?

第一个问题是是由readPreference来解决,第二个问题则是由readConcern来解决

7.4 readPreference

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

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

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

7.5 readPreference场景举例

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

7.6 readPreference配置

通过MongoDB的连接串参数:

mongodb://host1:27107, host2:27107, host3:27017/?replicaSet=rs0&readPreference=secondary

通过MongoDB驱动程序API:

 MongoCollection.withReadPreference(ReadPreference readPref)

Mongo Shell:

db.collection.find().readPref("secondary")

7.7 从节点读测试

  1. 主节点写入{count:1},观察该条数据在各个节点均可见
# mongo --host rs0/localhost:28017rs0:PRIMARY> db.user.insert({count:1})

在primary节点中调用readPref(“secondary”)查询从节点用直连方式(mongo
localhost:28017)会查到数据,需要通过mongo –host rs0/localhost:28017方式连接复制集,参考: https://jira.mongodb.org/browse/SERVER-22289

  1. 在两个从节点分别执行db.fsyncLock()来锁定写入(同步)
#mongo localhost:28018rs0:SECONDARY> rs.secondaryok()rs0:SECONDARY> db.fsyncLock()
  1. 主节点写入{count:2}
rs0:PRIMARY> db.user.insert({count : 2})rs0:PRIMARY> db.user.find()rs0:PRIMARY> db.user.find().readPref("secondary")rs0:SECONDARY> db.user.find()
  1. 解除从节点锁定db.fsyncUnlock()
rs0:SECONDARY>db.fsyncUnlock()
  1. 主节点中查从节点数据
rs0:PRIMARY> db.user.find().readPref("secondary")

扩展: 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: "analyse"}])

注意事项

  • 指定readPreference时也应注意高可用问题。例如将readPreference指定 primary,则发生故障转移不存在primary期间将没有节点可读。如果业务允许,则应选择primaryPreferred;

  • 使用Tag时也会遇到同样的问题,如果只有一个节点拥有一个特定Tag,则在这个节点失效时将无节点可读。这在有时候是期望的结果,有时候不是。例如:

    • 如果报表使用的节点失效,即使不生成报表,通常也不希望将报表负载转移到其他节点上,此时只有一个节点有报表Tag是合理的选择;
    • 如果线上节点失效,通常希望有替代节点,所以应该保持多个节点有同样的Tag;
  • Tag有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它成为主节点,则优先级应为0。

7.8 readConcern

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

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

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

  • 一个chunk x正在从shard1向shard2迁移;

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

    • 所有对chunk x的读写操作仍然进入shard1;
    • config 中记录的信息 chunk x仍然属于shard1;
  • 此时如果读shard2,则会体现出local和available 的区别:

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

注意事项

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

readConcern:majority
只读取大多数据节点上都提交了的数据。考虑如下场景:

  • 集合中原有文档{x: 0};
  • 将x值更新为1;

如果在各节点上应用{readConcern: “majority”}来读取数据:


考虑t3时刻的Secondary1,此时:

  • 对于要求majority的读操作,它将返回x=0;
  • 对于不要求majority的读操作,它将返回x=1;

如何实现" />replication:replSetName: rs0enableMajorityReadConcern: true

  1. 将复制集中的两个从节点使用db.fsyncLock()锁住写入(模拟同步延迟)
  2. 测试
rs0:PRIMARY> db.user.insert({count: 10})rs0: PRIMARY> db.user.find ().readConcern("local")rs0: PRIMARY> db.user.find ().readConcern("majority")

结论:

  • 使用local参数,则可以直接查询到写入数据
  • 使用majority,只能查询到已经被多数节点确认过的数据
  • update 与remove 与上同理。

readConcern: majority与脏读MongoDB中的回滚:

  • 写操作到达大多数节点之前都是不安全的,一旦主节点崩溃,而从节点还没复制到该次操作,刚才的写操作就丢失了;
  • 把一次写操作视为一个事务,从事务的角度,可以认为事务被回滚了。

所以从分布式系统的角度来看,事务的提交被提升到了分布式集群的多个节点级别的“提交”,而不再是单个节点上的“提交”。

在可能发生回滚的前提下考虑脏读问题:

  • 如果在一次写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作回滚了,则发生了脏读问题;

使用{readConcern: "majority"}可以有效避免脏读

如何安全的读写分离考虑如下场景:

  1. 向主节点写入一条数据;
  2. 立即从从节点读取这条数据。

思考:如何保证自己能够读到刚刚写入的数据?

下述方式有可能读不到刚写入的订单

db.orders.insert({oid:101,sku:"kite",q:1})db.orders.find({oid: 101}).readPref("secondary")

使用writeConcern+readConcern majority来解决

db.orders.insert({oid:101,sku:"kite",q:1}, {writeConcern:{w:"majority")))db.orders.find({oid:101}).readPref("secondary").readConcern("majority")

readConcern: linearizable
只读取大多数节点确认过的数据。和majority最大差别是保证绝对的操作线性顺序

  • 在写操作自然时间后面的发生的读,一定可以读到之前的写
  • 只对读取单个文档时有效;
  • 可能导致非常慢的读,因此总是建议配合使用maxTimeMS;


readConcern: snapshot

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

  • 不出现脏读;
  • 不出现不可重复读;·
  • 不出现幻读。

因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放。

小结

  • available:读取所有可用的数据
  • local:读取所有可用且属于当前分片的数据,默认设置
  • majority:数据读一致性的充分保证,可能你最需要关注的
  • linearizable:增强处理majority 情况下主节点失联时候的例外情况
  • snapshot:最高隔离级别,接近于关系型数据库的Serializable

MongoDB默认的事务隔离级别是:读已提交级别

snapshot不是串行化,应该是读已提交。