1. 模型设计基础
1.1 数据模型
数据模型是一组由符号、文本组成的集合,用以准确表达信息,达到有效交流、沟通的目的
Steve Hoberman 霍伯曼. 数据建模经典教程
数据模型设计的元素
传统模型设计
概念模型:
逻辑模型:
第三范式下的物理模型:
数据模型的三要素:
- 实体
- 属性
- 关系
问答
- 第三范式的理由是什么呢?是因为节省数据库空间吗?因为我感觉只有一个group的表然后有name, description和一组contact_id的数组也可以把?中间抽离一个contactGroup的意义何在呢
第三范式是关系设计的实践。 节省空间是一个很大的驱动 - 关系型理论出来的时候,1GB硬盘需要10万美元的价格。 数据一致性也是第三范式的一个目标。 你说的那种方式就是MongoDB的设计模式了。内嵌数组
1.2 JSON文档模型设计特点
- MongoDB 文档模型设计的三个误区:
不需要模型设计
MongoDB 应该用一个超级大文档来组织所有数据
MongoDB 不支持关联或者事务
- 关于 JSON 文档模型设计:
文档模型设计处于是物理模型设计阶段 (PDM)
JSON 文档模型通过内嵌数组或引用字段来表示关系
文档模型设计不遵从第三范式,允许冗余
- 为什么人们都说 MongoDB 是无模式:
严格来说,MongoDB 同样需要概念/逻辑建模,文档模型设计的物理层结构可以和逻辑层类似 MongoDB 无模式由来:可以省略物理建模的具体过程
逻辑模型 – JSON 模型:
文档模型的设计原则:性能和开发易用(没有类似于第三范式的原则)
- 关系模型 vs 文档模型对比:
问答
mongodb除了collection之外还有一个功能可以存附件,比如文件,视频,音频等。这个是和普通关系型数据库有区别的地方吧,还有就是以前面试的时候,面试官也拿mongodb和fastdfs提问,两者有什么区别,老师你可以用之前的项目经验解答下么
MongoDB的GridFS 不是一个真正的filesystem。它其实是一个api sugarcoating。实现是在驱动端(不是mongodb服务器!),提供一个字节流的API, 允许你把二进制文件通过这个接口提交给MongoDB 驱动,或者从这个接口读取二进制文件流。然后驱动会把这个文件分块,每一块作为一个mongodb的文档插入到集合里。 当你需要的时候,mongodb驱动会把所有相关的切块读出来,拼成一整个文件,返回给应用程序。 所以这个不是真的文件系统,只是提供了一个虚拟的文件接口供应用进行文件存储和读取。优点是可以用到mongo的分布式能力,做海量的二进制文件管理和高可用等。
数据冗余时,当有数据要更新时岂不是很麻烦?比如原来组A叫“同是”,但是后面发现“同是”错了,应该是“同事”。比如有很多这样的数据,岂不是要一个一个去改
不用一个个改,可以是一个update语句,更新所有匹配项
我这边有个疑问,MongoDB默认生成的 _id 类型是 ObjectId,在做查询时,经常需要考虑做类型转换处理,不然会碰到查不到数据的问题。 虽然ORM框架可以处理这个问题,但我想确认一个问题就是如果每次插入数据的时候都提前 new ObjectID,然后设置一个 string 类型的主键,和原生 ObjectID 相比是否有什么不好的地方,比如说性能是否会差一些
没有太大区别,性能上也不会有什么差别,除了你自己要保证这个_id的唯一性
在学习过程中遇到一个概念Tailable Cursor,这个应该怎么理解呢
Linux 有个命令叫tail,如果你理解那个的用法,就知道这个名词的由来了。 tail -f debug.log 这个命令会打印debug.log 的内容,然后不会退出,会在那里监听debug.log文件是否有新的日志写入,一旦有新的,就会马上在控制台打印出来。 使用tailable cursor, 你的程序不会退出,读完cursor最后一条以后会block,等待下一条数据过来后继续。很多时候可以用来做一些类似Java里面 observer pattern的事情或者传统数据库里触发器的事情。
1.3 文档模型设计之一:基础设计
文档模型设计三部曲
建立基础文档模型
根据概念模型或者业务需求推导出逻辑模型 – 找到对象
列出实体之间的关系(及基数) - 明确关系
套用逻辑设计原则来决定内嵌方式 – 进行建模
完成基础模型构建
联系人管理应用的案例:
1-1 关系建模: portraits
1-N 关系建模: Addresses
一个文档最大16MB
N-N 关系建模:内嵌数组模式
总结:
90:10 规则: 大部分时候你会使用内嵌来表示 1-1,1-N,N-N
内嵌类似于预先聚合(关联)
内嵌后对读操作通常有优势(减少关联)
问答
感觉多对多这样做并不科学把,如果修改其中一个属性,那么就会有好多写的操作,性能不会有问题吗
要看这种操作是否频繁。比如说,部门和人员之间的多对多,修改部门是很少见的操作,就可以接受。因为大部分时候是在读,那我们就是优化了读操作
在N-N时,当我要需要频繁查询某个组内的所有成员时,应该怎么设计呢?我自己的想法是加一张组到联系人id的表,不知道这样对不对
一般不需要。可以直接在联系人表里面有一个 groups 字段: { _id:xxx, contact_name: ‘tj’, group_ids:[1, 2, 5] } 对groups字段加个索引就可以快速的根据groupid快速检索该group 的所有成员了
我使用的mongo是4.0.11版本的,架构是分片集群,现在问题是,默认的最大连接数不够用的,819,怎么调优最大连接数
有可能是和ulimits相关。你看下这个 https://docs.mongodb.com/manual/reference/ulimit/
按照嵌套的方式 如果只建立一张联系人表,其他附属信息都通过嵌套的方式,这样是不是不太好呢
不是所有的信息都可以嵌套,比如联系人的一些activity/事件。但是联系人的静态属性(相对变化不频繁),则基本都可以内嵌
在mongo里,把数据和数据关系都存到一起,一是会产生数据冗余。数据的紧凑性能保证吗?以例子说明,比如开始没有头像信息和联系人,稍后再进行更新。这样在存储是会存储在多个位置吧
可以使用multi-update, 一次更新多条相关语句。mongo 有事务可以支持这个场景。
1.4 文档模型设计之二:工况细化
联系人管理应用的分组需求:
用于客户营销
有千万级联系人
需要频繁变动分组(group)的信息,如增加分组及修改名称及描述以及营销状态
一个分组可以有百万级联系人
一个分组信息的改动意味着 百万级的 DB 操作
解决方案:
- 类似于关系型设计
- 用 id 或者唯一键关联
- 使用 $lookup 来提供一次查询多表的能力(类似关联)
引用模式下的关联查询
另一个案例:联系人的头像采用引用模式
- 头像使用高保真,大小在 5MB10MB
- 头像一旦上传,一个月不可更换
- 基础信息查询(不含头像)和 头 像查询的比例为 9 :1
- 建议: 使用引用方式,把头像数 据放到另外一个集合,可以显著提 升 90% 的查询效率
什么时候该使用引用方式
内嵌文档太大,数MB 或者超过 16MB(比如头像)
内嵌文档或数组元素会频繁修改
内嵌数组元素会持续增长并且没有封顶
MongoDB 引用设计的限制:
MongoDB 对使用引用的集合之间并无主外键检查
MongoDB 使用聚合框架的 $lookup 来模仿关联查询
$lookup只支持 left outer join
$lookup的关联目标(from)不能是分片表(不能两个表都是分片表)
问答
如果被$lookup关联的表分片了,是不是只查询和主表在一个分区的数据了
无法工作了
想问下头像图片文件,不是应该直接文件系统,数据库中只存链接吗
头像图片文件,如果只是几十KB那样,完全可以直接放在JSON里面。除非是大到数个MB,就建议分开存放了。 至于是否文件系统,有别的考量了。文件系统不容易扩展,量达到亿/十亿级别管理会很麻烦
关于这一节我想请教下,在开发中我们的需求或者模型是变化的。比如这一节的group_ids对象类型,刚开始的时候是 json对象的数组,后面把这个抽出来使用单独的集合时候,group_ids对象类型变成了 整形数组. 这个变化会有一个开发和上线的一个过程,这里有没有什么好的最佳实践,谢谢老师. 这个变化,在上线的时候, 1:是需要再单独写一个脚本去清洗数据的吗? 2. 在清洗的时候, mongodb使用的是raft,是CP模型吗?这样的话,服务会暂时的对外不可用吗??是否会存在像mysql5.6之前的版本处理DML时进行锁表的问题
如果我是你,我会选择的是保留JSON格式,但是只留group_id字段。这样的话你只需要更新业务代码,不用停服务更新数据库。然后你可以写个语句把JSON字段里面的其他字段删除掉。整个过程都是线上操作,也不会有不一致数据问题。 如果是你的方案,就是彻底改变一个内嵌字段的类型,那就必须要写个脚本,然后最好是停止业务的访问,进行对数据库数据的修改
假如需求是联系人的属性是动态的,这种要怎么设计。关系型肯定更不好操作吧,比如说页面上有增删联系人属性的功能,这个模型怎么处理,尤其是作为面向对象开发,以Java为例,那这个联系人的属性对应的实体类属性就完全不知道怎么入手了,老师有什么建议吗,我是感觉这种功能好像是mongo数据库比较合适
对这个动态模型就是MongoDB的特色。如果是非强类型语言如python / nodejs 会更加简单,直接JSON入库,JSON可以增加属性。 模型处理mongodb端并不需要做任何工作,主要是你Java里面要能够给一个对象动态增加属性,然后把新对象交给Mongo就可以了
文档内嵌的层级基于性能考虑有没有限制,举个例子,比如实际业务中也可能内嵌5层这样的情况出现
之前的版本嵌套超过2层就会导致无法in place update, 需要把整个子文档读到应用端整体修改更新。新版本使用arryFilter可以更新任意深度了,从这个角度上来讲还好
当数据量非常大的情况下,内嵌文档被频繁修改要遍历所有的顶层文档,如果改为引用设计也只能建立一个关系表对多对多的情况优化的好些,如果是一对多的,除了修改从表的数据外,对从表的增删还是要在主表操作
对的,频繁修改是一个设计的考量,通常这个时候要考虑分出去另外一个集合,通过引用来表示关系
1.5 文档模型设计之三:模式套用
文档模型: 无范式,无思维定式,充分发挥想象力
设计模式:实战过屡试不爽的设计技巧,快速应用
举例:一个 IoT 场景的分桶设计模式,可以帮助把存储空间降低 10 倍并且查询效率提升数十倍.
问题假设: 物联网场景下的海量数据处理 – 飞机监控数据,格式如下
1 | { |
则大概需要的数据量如下:
分桶设计
解决办法:即每分钟一个文档改成每一个小时一个文档
分桶模式小结:
一个好的设计模式可以显著地:
• 提升数据读写的效率
• 降低资源的需求
更多的 MongoDB 设计模式:
问答
这个索引需要的 存储空间大小,是怎么计算出来的啊
db.collection.stats() 的结果里面有索引大小。
分桶优势是减少读的次数及空间的占用,那劣势是什么呢
如果一个查询正好要跨越两个或多个桶,可能会略麻烦。比如说,我们每个小时的数据放在一个文档内,从00分到59分。但是如果你想要从15分到下个小时15分之间的数据来统计分析,就没那么直观了
1.6 设计模式集锦
列转行
问题: 大文档,很多字段,很多索引
解决办法,列转行,索引为:
1 | db.movies.createIndex({“releases.country”:1, “releases.date”:1}) |
版本字段
问题: 模型灵活了,如何管理文档不同版本解决方案: 增加一个版本字段,可以在需要的时候再出现
近似计算
问题: 统计网页点击流量
预聚合
问题: 业绩排名,游戏排名,商品统计等精确统计:
热销榜:某个商品今天卖了多少,这个星期卖了多少,这个月卖了多少?
电影排行:观影者,场次统计
传统解决方案:通过聚合计算
痛点:消耗资源多,聚合计算时间长
解决方案: 用预聚合字段
问答
请问下预聚合场景下,提前设计好需要统计的字段这一部分我听懂了。但是针对于统计日销量、周销量和月销量,每一个文档是否需要时间戳,文档具体该如何划分,来配合业务逻辑,保证可以精准切分时间段,从而支持查询。比如说查询的时候经常是分为月份来查。那么是不是每个文档中还需要month_num这个字段。按照周来查,就是需要week_num这个字段
通常这种就是典型的维度数据设计,在分析库里常用。如果你的数据分析类型没有达到数十数百,只是针对少数几个数据类型,那么可以采用这种方式,在文档里通过一个月统计数组,周统计数组,每个数组元素增加mongo_num字段的方式来做
提前问个索引相关的问题,为何Mongo的索引用B树而不是B+树呢,我理解B+树的最大优点是范围查找,MySQL用的B+树,Mongo也有这个需求啊
MongoDB用的是B+树。 http://source.wiredtiger.com/mongodb-3.4/tune_page_size_and_comp.html
2. 事务开发
2.1 写操作事务
writeConcern
writeConcern 决定一个写操作落到多少个节点上才算成功。writeConcern 的取值包括:
• 0:发起写操作,不关心是否成功;
• 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功;
• majority:写操作需要被复制到大多数节点上才算成功。
发起写操作的程序将阻塞到写操作到达指定的节点数为止
3节点复制集不作任何特别设定(默认值):会存在丢数据的情况
majority(参数 w:”majority”):大多数节点确认模式(2/3)
all(参数 w:”all”):全部节点确认模式
对于5个节点的复制集来说,写操作落到多少个节点上才算是安全的?
3 4 5 majority
writeConcern 实验
在复制集测试writeConcern参数
1 | db.test.insert( {count: 1}, {writeConcern: {w: "majority"}}) |
配置延迟节点,模拟网络延迟(复制延迟)
1 | conf=rs.conf() |
观察复制延迟下的写入,以及timeout参数
1 | db.test.insert( {count: 1}, {writeConcern: {w: 3}}) |
注意事项
虽然多于半数的 writeConcern 都是安全的,但通常只会设置 majority,因为这是 等待写入延迟时间最短的选择;
不要设置 writeConcern 等于总节点数,因为一旦有一个节点故障,所有写操作都将失败
writeConcern 虽然会增加写操作延迟时间,但并不会显著增加集群压力,因此无论是否等待,写操作最终都会复制到所有节点上。设置 writeConcern 只是让写操作等待复制后再返回而已;
应对重要数据应用 {w: “majority”},普通数据可以应用 {w: 1} 以确保最佳性能
journal(参数 j: true )
writeConcern 可以决定写操作到达多少个节点才算成功,journal 则定义如何才算成功。取值包括:
• true: 写操作落到 journal 文件中才算成功
• false: 写操作到达内存即算作成功
问答
请问一下:日志、journal、oplog 这三个有什么联系与区别呢?感觉这三者的概念很混淆
日志: 这个是一个比较通用的概念,可以包括你说的所有(journal,oplog,以及server日志)。 具体一点来说, journal日志是数据库的crash recovery手段。通常的做法是把数据库内的数据块修改,提前用文件顺序写方式刷到盘上,然后再去真正的提交数据的修改。这样的目的是在服务器宕机的时候,内存中被丢失的数据可以在恢复过程中从journal 日志文件中读回来。 Oplog也是记录的数据库的操作日志,但是记的是逻辑操作命令。主要的目的是用于节点之间复制数据,而不是上面journal主要是用来recover crash。 还有一种就是mongod.log,这个就是一个文本文件,记录数据库系统的正常运行和错误信息等等。
mongo是否可以在服务器端配置 majority write concern ,跟 j : true ? 如果在sql层面每条语句加 w:majority , j : true 有点麻烦,开发也不一定按照规范来操作。 如果可以在服务器直接配置,该如何配置
服务器端无法配置。你可以在Connection String上设置,这个一般是全局统一的设置
例子:mongodb://db0.example.com,db1.example.com,db2.example.com/?replicaSet=myRepl&w=majority&wtimeoutMS=5000
就是一主二从的模式,writeConcern 为 majority,刚好写入一条数据时主节点宕机,此时两个从节点一个有数据,一个没有这条数据,有数据的一个作为主节点。此时,没有数据的一个还是从节点,它的同步是去监听新的主节点的日志变化,但是刚刚写的那条数据是在它监听之前写入新的主节点的日志的,那么这个从节点不就丢失了这条写的数据了吗?mongodb还有其他什么机制保证吗
最后一个从节点通过比较自己的oplog和新主的oplog来确定是否要同步新数据。两者的oplog肯定会差一条(新的数据),就会从新主那里复制
Mongo 落盘的时候,有用到文件系统的 Page Cache 机制吗?当有一条数据更新的时候是先写到内存,然后是Page Cache 最后是硬盘吗。j 这个参数是代表写到 Page Cache 还是硬盘呢
j:1 表示Journal 日志刷盘,所以就是要写文件到硬盘。 正常数据只是写到内存的Cache(不是操作系统的cache,而是WiredTiger自己管理的Cache),然后异步刷盘。
mongodb事务的默认隔离级别是什么?听说是相当于read uncommitted,这样似乎存在问题啊
默认是read uncommitted。正常情况都不会有问题,除了发生宕机的时候,这个可能会有问题 - 你读到的一条数据可能会在宕机的时候被回滚掉。这种事情概率很小,发生了可能问题也不算特别严重,很多场景可以接受。如果要求比较高,那么需要用 readConcern: majority 来提高隔离级别
请问一下,oplog是在内存中还是在磁盘上呢? 每一次写操作都会事务的记录oplog对吧
oplog和就是一个MongoDB的collection ,都是存储在磁盘上的。 对oplog的操作只是顺序追加写入,所以效率相对来说会高不少,相比于普通collection 的随机读写。 每一次增删改都会记录相应的oplog,并且对oplog的操作和数据表的写入是同一个原子事务的
怎么就确定重新启动的主节点是同步x=1的节点呢.也可能是还没同步x=1节点
按照mongodb复制集选举规则,有最新数据的会第一优先被选为主节点
请问个问题呀,一条数据在某个特定的业务场景,我需要把它写入 es 与 mongo(先写入mongo,然后在写入es),写入mongo是ok的,但是由于某些原因写入es失败。这个时候我需要回滚 mongo 里面的数据,怎么处理呢
跨系统事务(XA)目前在MongoDB/ES上并无实现。Tough luck, 搜索下Saga Pattern,自己做事务补偿吧
w=majority时,写入成功会数据一定不会丢吧?比如副本集有5个节点,某一次更新录录记为r,更新成功。主和另一从节点宕机,剩下A,B,C三个节点,且满足 1. A与B,C能通信,但B和C之间无法通信 2. A比B和C有更高优先级 3. A的opIog比B和C旧,少一个修改记录r 这种情况下,A是不是有可能成为主节点呢?如果是,B和C同步A的数据后,会回滚那个记录r吧?数据不就丢了吗
你这个是Triple Failure - 三重错误。 如果按照这种假设,极端点的话就像是3个硬盘同时坏掉,你全部写入都没用。数据还是丢了。 然后,你的描述确实有可能发生
请问怎样限制 MongoDB 使用的总内存大小。将 mongod.cfg 中storage.wiredTiger.engineConfig.cacheSizeGB 设置为 32 之后,在任务管理器中发现,MongoDB Server 占用的内存还是达到了 45 GB
32GB只是控制数据的缓存空间大小。MongoDB服务器本身还需要内存来进行工作,如管理TCP连接,聚合、排序运算等
请问一下 j:true 是保证主节点的日志写入成功,还是保证writeConcern配置的参数对应的节点的日志写入成功才返回呢
后者。对应于 w:N 里面N个节点的日志落盘
2.2 读操作事务
在读取数据的过程中我们需要关注以下两个问题:
• 从哪里读?
• 什么样的数据可以读?
第一个问题是是由 readPreference 来解决;第二个问题则是由 readConcern 来解决
什么是 readPreference
readPreference 决定使用哪一个节点来满足正在发起的读请求。可选值包括:
• primary: 只选择主节点;
• primaryPreferred:优先选择主节点,如如果不可用则选择从节点;
• secondary:只选择从节点;
• secondaryPreferred:优先选择从节点,如果从节点不可用则选择主节点;
• nearest:选择最近的节点
readPreference 场景举例
- 用户下订单后马上将用户转到订单详情页——primary/primaryPreferred。因为此 时从节点可能还没复制到新订单;
- 用户查询自己下过的订单——secondary/secondaryPreferred。查询历史订单对 时效性通常没有太高要求;
- 生成报表——secondary。报表对时效性要求不高,但资源需求大,可以在从节点 单独处理,避免对线上用户造成影响;
- 将用户上传的图片分发到全世界,让各地用户能够就近读取——nearest。每个地区 的应用选择最近的节点读取数据。
readPreference 与 Tag
readPreference 只能控制使用一类节点。Tag 则可以将节点选择控制 到一个或几个节点。考虑以下场景:
- 一个 5 个节点的复制集
- 3 个节点硬件较好,专用于服务线上客户
- 2 个节点硬件较差,专用于生成报表
可以使用 Tag 来达到这样的控制目的:
- 3 个较好的节点打上 {purpose: “online”};
- 为 2 个较差的节点打上 {purpose: “analyse”};
- 在线应用读取时指定 online,报表读取时指定 reporting。
readPreference 配置
通过 MongoDB 的连接串参数:
mongodb://host1:27107,host2:27107,host3:27017/?replicaSet=rs&readPreference=secondary
通过 MongoDB 驱动程序 API:
MongoCollection.withReadPreference(ReadPreference readPref)
Mongo Shell:
db.collection.find({}).readPref( “secondary” )
readPreference 实验: 从节点读
- 主节点写入 {x:1}, 观察该条数据在各个节点均可见
- 在两个从节点分别执行 db.fsyncLock() 来锁定写入(同步)
- 主节点写入 {x:2}
- db.test.find({a: 123})
- db.test.find({a: 123}).readPref(“secondary”)
- 解除从节点锁定 db.fsyncUnlock()
- db.test.find({a: 123}).readPref(“secondary”)
注意事项
- 指定 readPreference 时也应注意高可用问题。例如将 readPreference 指定 primary,则发生故障转移不存在 primary 期间将没有节点可读。如果业务允许,则应选择 primaryPreferred;
- 使用 Tag 时也会遇到同样的问题,如果只有一个节点拥有一个特定 Tag,则在这个节点失效时 将无节点可读。这在有时候是期望的结果,有时候不是。例如:
- 如果报表使用的节点失效,即使不生成报表,通常也不希望将报表负载转移到其他节点上,此时只有一个 节点有报表 Tag 是合理的选择;
- 如果线上节点失效,通常希望有替代节点,所以应该保持多个节点有同样的 Tag;
- Tag 有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它成为主节点,则优先级应为 0。
问答
我印象中有版本的mongodb,做读写分离的时候会导致主库时不时出现写入慢的情况
4.0 之前从节点可能会阻塞,在写入很大的情况下
聚合操作语句如果设置readpreference 为从节点,聚合中再’$out生成新的集合,这样能插入到从节点吗?
不能。写入必须是主节点
两个从节点都锁住了,但是主节点执行db.test.find().readPref(“secondary”)是两条数据
mongo mongodb://root1:123456@10.0.0.29:28017,10.0.0.30:28017,10.0.0.31:28017/?replicaSet=my_repl 使用此方式链接
2.3 读操作事务2
什么是 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};
t0时间点 将x值更新为 1;
t1时间点复制到sencondary1
t2时间点复制到secondary2
如果在各节点上应用 {readConcern: “majority”} 来读取数据:
t3的时候primary已经知道在多数节点能查到
t5的时候s1也知道primary和s1都有数据了
majority 的实现方式: 考虑 t3 时刻的 Secondary1,此时:
- 对于要求 majority 的读操作,它将返回 x=0;
- 对于不要求 majority 的读操作,它将返回 x=1;
如何实现?节点上维护多个 x 版本,MVCC 机制 MongoDB 通过维护多个快照来链接不同的版本:
- 每个被大多数节点确认过的版本都将是一个快照;
- 快照持续到没有人使用为止才被删除;
实验 : ”majority” vs “local”
安装 3 节点复制集。
注意配置文件内 server 参数 enableMajorityReadConcern
将复制集中的两个从节点使用 db.fsyncLock() 锁住写入(模拟同步延迟)
1 | db.test.save({“A”:1}) |
在某一个从节点上执行 db.fsyncUnlock() ,结论:
使用 local 参数,则可以直接查询到写入数据
使用 majority,只能查询到已经被多数节点确认过的数据
update 与 remove 与上同理
majority 与脏读
MongoDB 中的回滚:
- 写操作到达大多数节点之前都是不安全的,一旦主节点崩溃,而从节还没复制到该次操作,刚才的写操作就丢失了;
- 把一次写操作视为一个事务,从事务的角度,可以认为事务被回滚了。
所以从分布式系统的角度来看,事务的提交被提升到了分布式集群的多个节点级别的 “提交”,而不再是单个节点上的“提交”。在可能发生回滚的前提下考虑脏读问题:
- 如果在一次写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作 回滚了,则发生了脏读问题;
使用 {readConcern: “majority”} 可以有效避免脏读
readConcern: 如何实现安全的读写分离
考虑如下场景:
向主节点写入一条数据;
立即从从节点读取这条数据。
如何保证自己能够读到刚刚写入的数据?下述方式有可能读不到刚写入的订单
1 | db.orders.insert({ oid: 101, sku: ”kite", q: 1}) |
使用 writeConcern + readConcern majority 来解决:
1 | db.orders.insert({ oid: 101, sku: "kiteboar", q: 1}, {writeConcern:{w: "majority”}}) |
readConcern 主要关注读的隔离性, ACID 中的 Isolation, 但是是分布式数据库里特有的概念。readCocnern: majority 对应于事务中隔离级别中的哪一级?答案:Read Committed
readConcern: linearizable
只读取大多数节点确认过的数据。和 majority 最大差别是保证绝对的操作线性顺序 – 在写操作自然时间后面的发生的读,一定可以读到之前的写
只对读取单个文档时有效;
可能导致非常慢的读,因此总是建议配合使用 maxTimeMS;
主节点和其他节点网络断开
readConcern: snapshot
{readConcern: “snapshot”} 只在多文档事务中生效。将一个事务的 readConcern 设置为 snapshot,将保证在事务中的读:
- 不出现脏读;
- 不出现不可重复读
- 不出现幻读
因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放
readConcern: 小结
- available:读取所有可用的数据
- local:读取所有可用且属于当前分片的数据,默认设置
- majority:数据读一致性的充分保证,可能你最需要关注的
- linearizable:增强处理 majority 情况下主节点失联时候的例外情况
- snapshot:最高隔离级别,接近于 Seriazable
问答
有个问题想请教,本节中的{readConcern:”majority”}的例子,所有节点的数据达到共识的时间,与复制集的应该不一样吧?复制集的情况应该会简单点,就是当primary节点把数据复制给secondary时,顺便把x=1写入P成功的信息告诉S1和S2,这样S1和S2只需要告诉P他们也写入成功就完事了,没必要到t5、t6吧?只有在writeConcern时才像例子中说的?
一个读线程,使用 readConcern:majority,在t1 的时候读S1节点,S1是否要返回最新的一个写入 x=1 还是要等到t5?
首先理解,这个读线程只能读 一个被大多数节点确认的写入(满足write majority),这才可以满足readConcern: majority的条件
在t1的时候,x=1实际上已经到达两个节点(majority),但是判断这个事实的是主节点。在主节点没有知道并确认x=1这个操作已经被多数节点写入的时候,这条数据是不够资格被发给 readConcern: majority 读线程的
这个是3个节点,想象一下5个节点 - 那S1是肯定不知道数据已经写入到多数节点,只能等primary 告诉他
我用readConcern和writeConcern就是想基于Quorum原理来解决一致性问题的, 但是我看官网说“readConcern设置为majority时,能保证读到的数据『不会发生回滚』,但并不能保证读到的数据是最新的“。这句话指的是我刚才写的数据提交后,在发起读操作并没法马上读到刚才提交的数据吗
如果你的writeConcern是默认,比如说你写 x:1 到主节点, ,它会立即返回。如果你马上再去用majority readConcern去读这条数据,这条数据有可能没被复制到多数节点,你就读不到它。如果在同一个线程内,一般这个是不可以接受的
正解是,在读写操作是同一个应用发起时,你要配合使用 writeConcern:majority 来保证你的写已经被提交到大多数节点,这样你马上再用readConcern: majority 就可以读到你刚才的写入了
mysql单节点时仍然是事务型的。我觉得事务最核心的标志是 begin; xxx; commit|rollback; 当xxx中出问题或者业务程序里出问题可以rollback,保证整个过程是完整的原子的
完整的事务包括ACID。你的理解和大部分程序员一样,更加关注的是里面的A: Atomicty。 就是几个操作要group为一个原子操作,要么全部成功,要么回滚所有操作。
2.4 事务开发:多文档事务
MongoDB 虽然已经在 4.2 开始全面支持了多文档事务,但并不代表大家应该毫无节制 地使用它。相反,对事务的使用原则应该是:能不用尽量不用, 事务 = 锁,节点协调,额外开销,性能影响
MongoDB 多文档事务的使用方式与关系数据库非常相似:
1 | try (ClientSession clientSession = client.startSession()) { |
事务的隔离级别:
- 事务完成前,事务外的操作对该事务所做的修改不可访问
- 如果事务内使用 {readConcern: “snapshot”},则可以达到可重复读 Repeatable Read
实验:启用事务后的隔离性
1 | db.tx.insertMany([{ x: 1 }, { x: 2 }]); |
实验:可重复读 Repeatable Read
1 | var session = db.getMongo().startSession(); |
事务写机制:MongoDB 的事务错误处理机制不同于关系数据库:
- 当一个事务开始后,如果事务要修改的文档在事务外部被修改过,则事务修改这个 文档时会触发 Abort 错误,因为此时的修改冲突了;
- 这种情况下,只需要简单地重做事务就可以了;
- 如果一个事务已经开始修改一个文档,在事务以外尝试修改同一个文档,则事务以 外的修改会等待事务完成才能继续进行(write-wait.md实验)。
实验:写冲突, 继续使用上个实验的tx集合 开两个 mongo shell 均执行下述语句
1 | var session = db.getMongo().startSession(); |
注意事项:
- 可以实现和关系型数据库类似的事务场景
- 必须使用与 MongoDB 4.2 兼容的驱动;
- 事务默认必须在 60 秒(可调)内完成,否则将被取消;
- 涉及事务的分片不能使用仲裁节点;
- 事务会影响 chunk 迁移效率。正在迁移的 chunk 也可能造成事务提交失败(重试 即可);
- 多文档事务中的读操作必须使用主节点读;
- readConcern 只应该在事务级别设置,不能设置在每次读写操作上
问答
MongoDB还是很厉害的,一开始就支持单文档事务,4.0支持多文档事务,4.2支持有分片的分布式事务。不过,单文档事务这叫法有点鸡肋,虽然也的确是事务,但一般我们讲事务时,默认是指多文档的,对比MySQL就是多行。如果我用两条更新语句更新一个文档,那应该也不是事务吧?单文档事务这叫法,给人感觉在一个文档上执行多条DML,使用的一个事务,但其实不是。另外,MongoDB在没有显式使用事务语句时,应该就是自动提交吧
这个单文档事务叫法背后的逻辑: 关系型里面,用户表和用户电话是分开在两张表。当你需要新增一个新用户,你需要: insert into USER
insert into PHONE
这两个DML需要放在事务范围内,保证两条数据(其实是一条,用户数据)要么一起成功写入,要么回滚。
在MongoDB里面,类似的操作变成了一个单文档操作:
db.user.insert({ name: “TJ”,phone: [“1234”]})
这个操作里面其实相当于做了两个事情:1) 添加用户 2) 添加电话号码。
这两个事情是有原子事务性的-要么一起成功,要么一起失败。 这个是mongo单文档事务的由来。
chunk过程中各个分片上的oplog是会变化的吗?这时候oplog跟分片上的数据能对应起来吗?这部分日志是不是无用的日志啊
会产生日志。日志里有个特殊字段会标记出来是来自于chunk migration
我想请教一下mongodb有分布式事务锁吗?我想实现某一字段int值自增生成唯一主键
你这个不用分布式事务吧,用 findAndModify就可以实现了。
2.5 Change Stream
Change Stream 是 MongoDB 用于实现变更追踪的解决方案,类似于关系数据库的触发器,但原理不完全相同:
Change Stream 是基于 oplog 实现的。它在 oplog 上开启一个 tailable cursor 来追踪所有复制集上的变更操作,最终调用应用中定义的回调函数。 被追踪的变更事件主要包括:
- insert/update/delete:插入、更新、删除
- drop:集合被删除;
- rename:集合被重命名;
- dropDatabase:数据库被删除;
- invalidate:drop/rename/dropDatabase 将导致 invalidate 被触发, 并关闭 change stream;
Change Stream 只推送已经在大多数节点上提交的变更操作。即“可重复读”的变更。 这个验证是通过 {readConcern: “majority”} 实现的。因此:
- 未开启 majority readConcern 的集群无法使用 Change Stream
- 当集群无法满足 {w: “majority”} 时,不会触发 Change Stream(例如 PSA 架构 中的 S 因故障宕机)。
变更过滤: 如果只对某些类型的变更事件感兴趣,可以使用使用聚合管道的过滤步骤过滤事件。
1 | var cs = db.collection.watch([{ |
故障恢复:假设在一系列写入操作的过程中,订阅 Change Stream 的应用在接收到“写3”之后 于 t0 时刻崩溃,重启后后续的变更怎么办?
想要从上次中断的地方继续获取变更流,只需要保留上次变更通知中的id即可。 右侧所示是一次 Change Stream 回调所返回的数据。每 条这样的数据都带有一个 id,这个 id 可以用于断点恢 复。例如: var cs = db.collection.watch([], {resumeAfter: <_id>}) 即可从上一条通知中断处继续获取后续的变更通知。
Change Stream 使用场景:
跨集群的变更复制——在源集群中订阅 Change Stream,一旦得到任何变更立即写入目标集群
微服务联动——当一个微服务变更数据库时,其他微服务得到通知并做出相应的变更
其他任何需要系统联动的场景
注意事项:
- Change Stream 依赖于 oplog,因此中断时间不可超过oplog 回收的最大时间窗
- 在执行 update 操作时,如果只更新了部分数据,那么 Change Stream 通知的也是增量部分
- 同理,删除数据时通知的仅是删除数据的 _id
问答
事务内的写冲突在高并发下会造成大量的错误,那岂不是很鸡肋
除非你是计数器计数那种对同一个字段并发修改。高并发很多是各自用户修改各自的信息,比如说摩拜单车,每辆自行车都在不断修改GPS轨迹,导致高并发访问。但是每辆车只是修改自己的那条文档。这种不会有冲突。
我想咨询change stream是如何触发微服务的。比如库存低于一定阈值后,触发Java服务发送邮件。
可以看这个例子: https://github.com/spring-projects/spring-data-examples/tree/master/mongodb/change-streams
事务的可见性必须在“复制集”模式下才有 local和majority(readConcern), 单主节点模式下可见性是不是只有local
单主节点的majority就是1 所以你用local还是majority没有区别
3. 开发最佳实践
3.1 连接到 MongoDB
关于驱动程序:总是选择与所用之 MongoDB 相兼容的驱动程序。这可以很容易地从驱动 兼容对照表中查到, 如果使用第三方框架(如 Spring Data),则还需要考虑框架版本与驱动的兼容性;
关于连接对象 MongoClient:使用 MongoClient 对象连接到 MongoDB 实例时总是应该 保证它单例,并且在整个生命周期中都从它获取其他操作对象。
关于连接字符串:连接字符串中可以配置大部分连接选项,建议总是在连接字符串中配置 这些选项;
// 连接到复制集
mongodb://节点1,节点2,节点3…/database?[options]
// 连接到分片集
mongodb://mongos1,mongos2,mongos3…/database?[options]
常见连接字符串参数
1 | maxPoolSize //连接池大小 |
连接字符串节点和地址
无论对于复制集或分片集,连接字符串中都应尽可能多地提供节点地址,建议全部 列出:
- 复制集利用这些地址可以更有效地发现集群成员;
- 分片集利用这些地址可以更有效地分散负载
连接字符串中尽可能使用与复制集内部配置相同的域名或 IP;
使用域名连接集群
在配置集群时使用域名可以为集群变更时提供一层额外的保护。例如需要将集群整体 迁移到新网段,直接修改域名解析即可
另外,MongoDB 提供的 mongodb+srv:// 协议可以提供额外一层的保护。该协议允 许通过域名解析得到所有 mongos 或节点的地址,而不是写在连接字符串中
mongodb+srv://server.example.com/ Record TTL Class Priority Weight Port Target _mongodb._tcp.server.example.com. 86400 IN SRV 0 5 27317 mongodb1.example.com. _mongodb._tcp.server.example.com. 86400 IN SRV 0 5 27017 mongodb2.example.com
不要在 mongos 前面使用负载均衡
基于前面提到的原因,驱动已经知晓在不同的 mongos 之间实现负载均衡,而复制集则需要根据节点的角色来选择发送请求的目标。如果在 mongos 或复制集上层部署负 载均衡:
- 驱动会无法探测具体哪个节点存活,从而无法完成自动故障恢复
- 驱动会无法判断游标是在哪个节点创建的,从而遍历游标时出错
结论:不要在 mongos 或复制集上层放置负载均衡器,让驱动处理负载均衡和自动故障恢复。
游标使用
如果一个游标已经遍历完,则会自动关闭;如果没有遍历完,则需要手动调用 close() 方法,否则该游标将在服务器上存在 10 分钟(默认值)后超时释放,造成不必要的资源浪费。 但是,如果不能遍历完一个游标,通常意味着查询条件太宽泛,更应该考虑的问题是 如何将条件收紧。
关于查询及索引
- 每一个查询都必须要有对应的索引
- 尽量使用覆盖索引 Covered Indexes (可以避免读数据文件)
- 使用 projection 来减少返回到客户端的的文档的内容
关于写入
- 在 update 语句里只包括需要更新的字段
- 尽可能使用批量插入来提升写入性能
- 使用TTL自动过期日志类型的数据
关于文档结构
- 防止使用太长的字段名(浪费空间)
- 防止使用太深的数组嵌套(超过2层操作比较复杂)
- 不使用中文,标点符号等非拉丁字母作为字段名
处理分页问题 – 避免使用 count
尽可能不要计算总页数,特别是数据量大和查询条件不能完整命中索引时。 考虑以下场景:假设集合总共有 1000w 条数据,在没有索引的情况下考虑以下查询:
db.coll.find({x: 100}).limit(50);
db.coll.count({x: 100});
前者只需要遍历前 n 条,直到找到 50 条队伍 x=100 的文档即可结束;后者需要遍历完 1000w 条找到所有符合要求的文档才能得到结果。为了计算总页数而进行的 count() 往往是拖慢页面整体加载速度的原因
处理分页问题 – 巧分页
避免使用skip/limit形式的分页,特别是数据量大的时候; 替代方案:使用查询条件+唯一排序条件
1 | 第一页:db.posts.find({}).sort({_id: 1}).limit(20); |
关于事务
使用事务的原则:
- 无论何时,事务的使用总是能避免则避免
- 模型设计先于事务,尽可能用模型设计规避事务
- 不要使用过大的事务(尽量控制在 1000 个文档更新以内)
- 当必须使用事务时,尽可能让涉及事务的文档分布在同一个分片上,这 将有效地提高效率
问答
分页可以用skip吗? 如果每页50个,那skip(n*50).limit(50)?
简单的skip功能okay,但是skip多的时候有性能问题。因为在服务端mongo如果你skip 10万条,它真的会load 10万条到内存一条条跳过直到10万条开始返回数据。
MongoDB适合存储车辆GPS信息吗?比如十万辆车,每辆车每十秒上传一次GPS信息(时间,坐标,方向,位置,速度等),MongoDB可以支持这种量级的数据吗?还要考虑车辆位置实时监控,历史时间段轨迹查询等应用
这个有非常多的案例。比较著名的是有最大的一个共享单车,国内最大的汽车制造厂之一,以及最著名的电动车厂都是用mongo来记录车辆GPS位置。放心去用吧
请问MongoDB复制集的连接数 和机器的配置应该怎么换算?怎么样达到合理的连接数
可以参考下这个文档: https://docs.atlas.mongodb.com/connection-limits/index.html
mongodb atlas云服务里面的规格和AWS机器规格对应: M30 m4.large M40 m4.xlarge M50 m4.2xlarge M60 m4.4xlarge
使用MongoClient对象连接到MongoDB实例时总是应该保证它单例,并且在整个生命周期中都从它获取其他操作对象。老师 这块为什么一定要是单例
MongoClient本身设计就是个Singleton 类,自己维护了连接池(java默认100个)。如果你用10个MongoClient实例,就是100x10个连接,对mongo服务器压力会太大
目前我们公司物联网准备用mongodb或hbase ,有一些聚合计算,更倾向用mongodb,可是目前遇到问题20亿2个分片,16核32G内存,现在总是分片均衡特别慢,6000个分片,一个2000多,一个3000多,所有服务都停了,均衡一周了还没均衡完,还差好几百个分片没均衡,找了好多看了监控一直找不到原因
4.0以后在均衡方面性能有不错的提升
请问一下我默认只连接主节点,不知道为什么主节点变成从节点了,这样最终导致程序只能读不能写了,这种情况怎么处理呢?我现在是手动把原来连的节点重新配置成主节点来解决的
你应该用replicaset的模式连,这样换主也可以继续工作。 mongodb://host1:27017,host2:27017,host3:27017/mydb?replicaSet=myset
关于那个count计数的问题,我想请教一下,前端要count数量来做分页显示,这个没法不使用count,还是另外一个集合记录其他集合的count数量,每次需要count的时候去记录集合取
这个没有太好的办法,在mongo里面,就是要去库里查一下。 除非你的count是不带任何条件,只是全表count,那个是没有性能影响的
请问MongoDB的用户是在库上来配置的吗,还是只是在admin库里配置的
理论上都可以 - 用户可以在admin库里,也可以在用户库里。我们一般建议建在admin库里方便统一管理
MongoDB集群分片数据分配不均衡,怎么办
少数不均衡不是问题(比如说5-10% 的差别)。多了不均衡,要看看是不是有下面问题?
1) Jumbo chunk
2) balancer 是不是停了? sh.getBalancerState()
3) 写入太频繁,超过IO能力
4. MongoDB索引
4.1 MongoDB索引机制一
相关概念:Index、Key、DataPage
Covered Query/FETCH:
表示查询覆盖/抓取, 如果所有需要的字段都在索引中,不需要额外的字段,就可 以不再需要从数据页加载数据,这就是查询覆盖。
db.human.createIndex({firstName: 1, lastName: 1, gender: 1, age: 1})
IXSCAN/COLLSCAN:表示索引扫描/集合扫描:
Query Shape:
Index Prefix:
索引前缀
1 | db.human.createIndex({firstName: 1, lastName: 1, gender: 1, age: 1}) |
以上索引的全部前缀包括:
- {firstName: 1}
- {firstName: 1, lastName: 1}
- {firstName: 1, lastName: 1, gender: 1}
所有索引前缀都可以被该索引覆盖,没有必要针对这些查询建立额外的索引
Selectivity过滤性
在一个有10000条记录的集合中:
- 满足 gender= F 的记录有4000 条
- 满足 city=LA 的记录有 100 条
- 满足 ln=‘parker’ 的记录有 10 条
条件 ln 能过滤掉最多的数据,city 其次,gender 最弱。所以 ln 的过 滤性(selectivity)大于 city 大于 gender。
如果要查询同时满足: gender == F && city == SZ && ln == ‘parker’ 的记录,但只允许为 gender/city/ln 中的一个建立索引,应该把索引 放在哪里?
B+树结构
问答
MongoDB 磁盘目录清理的话,是直接删除集合吗?删除集合部分数据,可以释放空间吗?谢谢
不能。必须删除整个集合才会回收
mongo的索引跟mysql一样也是遵循最左前缀原则吗
是的
4.2 MongoDB索引机制二
索引执行计划
explain
索引类型
单键索引
组合索引
1 | db.members.find({ gender: “F”, age: {$gte: 18}}).sort(“join_date:1”) |
组合索引的最佳方式:ESR原则
• 精确(Equal)匹配的字段放最前面
• 排序(Sort)条件放中间
• 范围(Range)匹配的字段放最后面
同样适用: ES, ER
组合索引工作模式:
组合索引工作模式: 精确匹配
1 | db.test.createIndex({a: 1, b: 1, c: 1}) |
组合索引工作模式: 范围查询
1 | db.test.createIndex({a: 1, b: 1, c: 1}) |
索引字段顺序的影响:
1 | db.test.find({a: 2, b: {$gte: 2, $lte: 3}, c: 1}) |
1 | db.test.find({a: 2, b: {$gte: 2, $lte: 3}).sort({c: 1} |
多值索引
地理位置索引
1 | -- 创建索引 |
全文索引
1 | -- 插入数据 |
TTL索引
部分索引
1 | - 创建部分索引 |
哈希索引
其他索引技巧
后台创建索引
1
db.member.createIndex( { city: 1}, {background: true} )
对BI报表专用节点单独创建索引
该从节点priority设为0
关闭该从节点,
以单机模式启动
添加索引(分析用)
关闭该从节点,以副本集模式启动
问答
请教一下我有A B C三个字段,这三个字段可能单独查询,也可能ABC一起组合查询。这样我是三个字段单独建索引高效,还是建组合索引
如果3个字段分别有可能单独查询,并且频率都差不多,那你需要3个索引
ABC:1 B: 1 C: 1
有个内嵌文档的唯一性问题请教,因为是唯一性的组合索,保证的是内嵌文档跨文档唯一,但我需要的是跨文档可以重复,但当前文档内内嵌文档唯一(比如每个客户有多个联系地址,地址的姓名/电话不能重复),MONGODB支持这种索引吗?还是说需要业务代码来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15这个很容易实现。比如说你是这样的文档:
{
_id: ObjectId(),
customer_id: 101,
customer_name: "Nina",
emergency_contacts: [
{ contact_name: 'Nina mom', contact_phone: 12345},
{ contact_name: 'Nina dad', contact_phone:12346}
]
}
你可以建一个这样的唯一索引:
{ customer_id: 1, "emergency_contacts.contact_name":1,
"emergency_contacts.contact_phone":1}我这个删除方法1000万数据时,删除要60s,其中查询字段都建立了索引
这个是正常的。删除需要删除数据和索引,是一个成本不低的操作
最后面改单机加索引的步骤,是因为副本集存储位点信息吗?不然他怎么知道从哪里继续同步呢
对。每个节点都会记住自己的同步信息和offset