前言

这是极客时间的《MongoDB高手课》视频课程做的笔记。中间件会持续更新。视频课程可以自己搜一下,极客时间的山寨课很多,就是声画不同步看着有点不舒服而已,正版也不贵,新人首单是59。课程介绍说看完这个可以说自己精通MongoDB,欢迎大家在评论区反馈。

MongoDB再入门

关于MongoDB

Question Answer
什么是 MongoDB 一个以 JSON 为数据模型的 文档数据库
为什么叫 文档数据库 文档来自于 JSON Document,并非我们一般理解的 PDF,WORD文档
谁开发的 MongoDB 上市公司 MongoDB Inc. ,总部位于美国纽约
主要用途 1. 应用数据库,类似于Oracle,MySQL
2. 海量数据处理,数据平台
主要特点 1. 建模为可选
2. JSON数据模型比较适合开发者
3. 支持横向扩展,支持大数据量和并发
4. 从4.0版本开始支持ACID事务

MongoDB VS 关系型数据库

MongoDB RDBMS
数据模型 文档类型 关系模型
数据库类型 OLTP(Online Transaction Processing,联机事务处理) OLTP(Online Transaction Processing,联机事务处理)
CRUD操作 MQL/SQL SQL
高可用 复制集 集群模式
横向扩展能力 通过原生分片完善支持 数据分区、应用侵入式(如分库分表)
索引支持 B树,全文索引,地理位置索引,多键索引,TTL索引 B树,B+树
开发难度 容易 困难
数据容量 没有理论上限 千万、亿
扩展方式 垂直扩展+水平扩展 垂直扩展

MongoDB的特色以及优势

  • 简单直观:以自然的方式来建模,以直观的方式来交互。如订单模块,需要用几张表来表示订单-订单明细,MongoDB可以直接用一个集合来表示这个对象。通俗的讲复杂对象只需要用一个表(集合)

  • 结构灵活:弹性模式,从容响应需求的频繁变化

    • 多形性:同一个集合可以包含不同字段(类型)的文档对象

    • 动态性:线上修改数据模式,修改时应用和数据库无需下线重启

    • 数据治理:支持使用JSON schema

      多形性,可动态加新字段
  • 快速开发:做更多的事,写更少的代码

    • 数据库引擎只需要在一个存储区读写。数据库查询操作耗时最长的步骤在定位上,如果有这个一个对象,关系型数据需要6个表,至少需要定位6次,但是在MongoDB只需要一个查询语句定位一次。
    • 反范式,无关联的组织极大的优化查询速度
    • 程序API自然,快速开发
    多表和单集合的对比
  • 原生的高可用和横向扩展能力:多中心容灾,自动故障转移

MongoDB基本操作

使用 insert 完成插入操作

操作格式:

  • db.<集合>.insertOne(<JSON对象>)
  • db.<集合>.insertMany([<JSON 1>, <JSON 2>, ..., <JSON n>])

示例:

1
2
db.fruit.insertOne({name:"apple"})
db.fruit.insertMany([{name:"apple"},{name:"pear"},{name:"orange"}])

使用 find 查询文档

关于find:
  • find 是 MongoDB 中查询数据的基本指令,相当于SQL中的 Select
  • find 返回的是游标
find示例:
1
2
3
4
5
db.movies.find({"year":1975}) //单条件查询
db.movies.find({"year":1989,"title":"Batman"}) //多条件查询
db.movies.find({$and:[{"title":"Batman"}],{"category":"action"}}) //and的另一种形式
db.movies.find({$or:[{"year":1989},{"title":"Batman"}]}) //多条件or查询
db.movies.find({"title":/^B/}) //正则表达式查找,相当于模糊查询
SQL 和 MQL对照表
SQL MQL
a=1 { a: 1}
a<>1 { a: { $ne: 1}}
a>1 { a: { $gt: 1}}
a>=1 { a: { $gte: 1}}
a<1 { a: { $lt: 1}}
a<=1 { a: { $lte: 1}}
a=1 AND b=1 {a: 1,b: 1}{$and:[{a: 1}, {b: 1}]}
a=1 OR b=1 {$or:[{a: 1}, {b: 1}]}
a IS NULL {a:{$exist: false}}
a IN (1,2,3) {a: {$in: [1, 2, 3]}}
a NOT IN(1,2,4) {a: {$nin: [1, 2, 3]}}
查询逻辑运算符
  • $lt:存在并小于
  • $lte:存在并小于等于$gt:存在并大于
  • $gte:存在并大于等于
  • $ne:不存在或存在但不等于
  • $in:存在并在指定数组中
  • $nin:不存在或不在指定数组中
  • $or:匹配两个或多个条件中的一个
  • $and:匹配全部条件
使用 find 查询

find 支持 field.sub_field的形式查询子文档。假设有一个文档:

1
2
3
4
5
6
7
db.fruit.insertOne({
name:"apple",
from: {
country: "China",
province: "Guangdon"
}
})

查询语句

1
2
db.fruit.find({"from.country":"China"})
db.fruit.find({"from":{country:"China"}}) //这种是错误写法
使用find搜索数组中的对象

考虑以下文档,在其中搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
db.movies.insertOne({
"title":"Raiders of the Lost Ark",
"filming_location":[
{"city":"Los Angels",state:"CA","country":"USA"},
{"city":"Rome",state:"Lazio","country":"Italy"},
{"city":"Florence",state:"SC","country":"USA"}
]
})
// 查找城市的记录
db.movies.find({
"filming_location.city":"Rome"
})
// 同一个子对象满足多个条件的查询,需要使用$elemMatch
db.movies.find({
"filming_location.city":{
$elemMatch:{"city":"Rome","country":"USA"}
}
})
指定返回字段
  • find可以指定只返回某些字段
  • _id字段必须明确指明不返回,默认返回
  • MongoDB 中我们称之为 投影(projection)
  • db.movies.find({"category":"action"},{_id:0,title:1})中,第二个对象为 投影字段,为0表示不返回

使用 remove 删除文档

  • remove 需要配合查询条件来使用

  • 匹配到的文档会被删除

  • 指定一个空文档查询条件会删除所有数据

  • 以下为示例

    1
    2
    3
    4
    db.testcol.remove({a:1}) // 删除a = 1的数据
    db.testcol.remove({a: {$lt:1}}) // 删除a < 1的数据
    db.testcol.remove({}) // 删除所有数据
    db.testcol.remove() // 报错

使用 update 更新文档

  • update 操作格式: db.<集合>.update(<查询条件>,<更新字段>)

  • 以下数据为例

    1
    2
    db.fruit.insertMany([{name:"apple"},{name:"pear"},{name:"orange"}])
    db.fruit.updateOne({name:"apple"},{$set: {from: "China"}}) //$set指令可以修改原字段,也可以新增字段
  • updateOne 只会更新匹配到的第一条数据

  • updateMany 表示匹配到多少条就更新多少条

  • updateOne/updateMany 方法要求更新部分必须具备一下条件之一,否则报错:

    • $set/$unset

    • $push/$pushAll/$pop:增加一个对象到数组底部 / 增加多个对象到数组底部 / 从数组底部删除一个对象

    • $pull/$pullAll:如果匹配到相应的值则从数组中删除相应的对象 / 如果匹配到任意的值,从数组中删除相应的对象

    • $addToSet:如果不存在则新增一个值到数组

    • $inc:预聚合,适合排行榜,点击率等场景。(在chatgpt中提问,说MongoDB中的预聚合是线程安全的,可以自行查证)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      db.inventory.update(
      {_id:123},
      {
      $inc:{
      quantity: -1,
      daily_sales: 1,
      weekly_sales: 1,
      monthly_sales: 1
      }
      }
      )

使用 Drop 删除一个集合

  • 使用 db.<集合>.drop() 删除一个集合
  • 集合中所有文档会被删除
  • 集合相关的索引也会被删除

MongoDB的聚合查询

聚合运算的基本格式

1
2
3
4
5
pipeline = [$stage1, $stage2, ..., $stage3]
db.<collection>.aggregate(
pipeline,
{ options }
)

常见步骤

步骤 作用 SQL等价运算符
$match 过滤 WHERE
$project 投影 AS
$sort 排序 ORDER BY
$group 分组 GROUP BY
$skip 和 $limit 结果集限制 SKIP/LIMIT
$lookup 左外连接 LEFT OUTER JOIN
$unwind 展开数组
$graphLookup 图搜索
$facet 和 $bucket 分面搜索

常见步骤中的运算符

$match $project $group
$eq / $gt / $gte / $lt / $lte
$and / $or / $not / $in
$geoWithin / $intersect
… …
选择需要的或不需要的字段
$map / $reduce / $filter
$range
$multiply / $divide / $substract / $add
$year / $month / $dayOfMonth / $hour / $minute / $second
… …
$sum / $avg
$push / $addToSet
$first / $last / $max / $min
… …

聚合查询示例:

分页查询男性用户名字
1
SELECT FIRST_NAME AS '名', LAST_NAME AS '姓' FROM Users WHERE GENDER = '男' SKIP 100 LIMIT 20
1
2
3
4
5
6
7
8
9
10
11
db.users.aggregate([
{$match:{gender:'男'}},
{$skip:100},
{$limit:20},
{
$project:{
"名":"$first_name",
"姓":"$last_name"
}
}
])
每个部门女员工的数量
1
2
3
4
SELECT DEPARTMENT, COUNT(*) AS EMP_QTY
FROM Users
WHERE GENDER = '女'
GROUP BY DEPARTMENT HAVING COUNT(*) < 10
1
2
3
4
5
6
7
8
9
10
db.users.aggregate([
{$match:{gender:"女"}},
{
$group:{
_id:"$DEPARTMENT",
emp_qyt:{$sum: 1},
}
},
{$match:{emp_qyt:{$lt:10}}}
])
数组展开 unwind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//元数据
{
"name":"张三",
"score":[
{
"subject":"语文",
"score":84
},
{
"subject":"数学",
"score":90
},
{
"subject":"英语",
"score":60
}
]
}
//展开
db.students.aggregate([
{$unwind:'$score'}
])
//展开后数据
{
"subject":"语文",
"score":84
},
{
"subject":"数学",
"score":90
},
{
"subject":"英语",
"score":60
}
MQL特有步骤Bucket
特有步骤Bucket
1
2
3
4
5
6
7
8
db.products.aggregate([
{$bucket:{
groupBy:"$price",
boundaries:[0,10,20,30,40],
default:"Other",
output:{"count":{$sum:1}}
}}
])
组合bucket和facet
1
2
3
4
5
6
7
8
9
10
11
12
13
//组合bucket用facet
db.products.aggregate([
{
$facet:{
price:{
$bucket:{...}
},
year:{
$bucket:{...}
}
}
}
])

复制集机制与原理

复制集的作用

  • 主要意义在于实现高可用
  • 它的现实依赖与两个方面的功能
    • 数据写入时快速复制到另一个节点上
    • 接受写入数据的节点发生故障时,自动选举出另一个节点替代。(故障自动转移)
  • 实现高可用的同时,复制集的其他几个作用
    • 数据分发:将数据从一个区域分发到另一个区域,减少另一个区域的读延迟
    • 读写分离:不同类型的压力分到不同的节点上
    • 异地容灾:在数据中心发生故障时,快速切换到异地

典型的复制集结构

复制集结构

一个典型的复制集结构至少由三个具有投票权的节点组成,包括:

  • 一个主节点(Primary):接受写入操作和选举时投票
  • 两个以上从节点(Secondary):复制主节点的数据和选举时投票
  • 不推荐使用Arbiter节点(投票节点,类似于Redis的Sentinel)

数据是如何复制的?

  • 当一个写操作到达主节点时,它会被记录下来,这些称之为oplog
  • 从节点 通过在 主节点 打开一个 tailable 游标不断获取新进入主节点的 oplog ,并在自己的数据上回放,来保持和主节点的数据一致
复制集结构数据同步

通过选举完成故障恢复

复制集间的心跳检测
  • 具有投票权的节点相互发送心跳,5次心跳失联认为节点故障
  • 如果失联的是主节点,从节点会进行选举,选出新的主节点;如果是从节点,则不影响。
  • 选举基于 RAFT一致性算法 实现,选举成功的条件是大多数投票节点存活。
  • 复制集最多可以有50个节点,但具有投票权的最多有7个

影响节点选举的因素

  • 整个集群必须有大多数节点存活着
  • 被选为主节点的节点必须:
    • 能够与多数的节点建立连接
    • 拥有最新的oplog
    • 具有较高的优先级(如果有配置的话)

增加从节点不会提高系统写入的性能

从熟练到精通的开发之路

MongoDB 文档模型设计的三个误区

文档模型设计对比1 文档模型设计对比2
  1. 不需要模型设计 错误

    MongoDB同样需要概念/逻辑建模,只是和关系型数据库的建模不一样,没有主外键的部分。并且,文档模型设计的物理结构可以和逻辑层类似。所以物理模型这个步骤可以被省略

  2. MongoDB 用一个超大的文档来组织所有的数据 错误,不可能也不现实

  3. MongoDB 不支持事务 错误,4.0开始支持ACID事务

关系型数据库 MongoDB
模型设计层次 概念模型
逻辑建模
物理模型
概念模型
逻辑建模
模型实体 集合
模型属性 字段
模型关系 关联关系,主外键 内嵌数组,引用字段

文档模型设计

MongoDB模型设计三部曲

第一步:建立基础文档模型

  1. 根据概念模型或者业务需求推导出逻辑模型 - 找到对象
  2. 列出实体之间的关系 (及基数) - 明确关系(一对多,一对一,多对多)
  3. 套用逻辑设计原则来决定内嵌方式 - 进行建模
  4. 完成基础模型构建

第二步:根据读写工况细化

  • 最频繁的数据查询模式

  • 最常用的查询参数

  • 最频繁的数据写入模式

  • 读写操作比例

  • 数据量大小

  • 基于内嵌文档的模型,根据业务需求:

    • 使用引用来避免性能瓶颈
    • 使用冗余来优化访问性能
    引用模式下的关联查询

这个有点想mybatis里关联查询 association ,这种在mysql中容易造成 N+1的问题,但是在 MongoDB 中,是相当于 LEFT OUTER JOIN

MongoDB引用设计模式的一些限制

  • MongoDB在使用引用的集合中并无主外键检查
  • MongoDB使用聚合查询的 $lookup来模仿关联查询
  • $lookup 只支持 Left Outer Join
  • $lookup的关联目标(from)不能是分片表

第三步:套用设计模式

  • 文档模型:无范式,无思维定式,充分发挥想象力

  • 设计模式:实战过屡试不爽的设计技巧,快速应用

  • 举例:一个IoT场景的分桶设计模式,可以帮助把存储空间降低10倍,并且提高查询效率数十倍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//每分钟一个文档
{
"_id":"2016010105000:WG9943",
"icao":"WG9943",
"ts":ISODate('2016-01-01T05:00:00.000+0000'),
"event":{
"a":24943,"b":319,"p":[41,70],"s":56
}
}
//每小时一个文档
{
"_id":"2016010105000:WG9943",
"icao":"WG9943",
"ts":ISODate('2016-01-01T05:00:00.000+0000'),
"event":[ // 分桶设计,一小时60条数据
{
"a":24943,"b":319,"p":[41,70],"s":56,
"t":ISODate('2016-01-01T05:00:00.000+0000')
},
{
"a":24943,"b":319,"p":[41,70],"s":56,
"t":ISODate('2016-01-01T05:00:01.000+0000')
},
...
]
}

事务

什么是 writeConcern?

writeConcern 决定一个写操作落到多少个节点才算成功。writeConcern取值包括:

  • 0:发起写操作,不关心是否成功
  • 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功。
  • majority:写操作落到大多数节点中才算成功

发起写操作的程序将阻塞到写操作到达指定节点数为止。默认为主节点成功就成功。

writeConcern 实验

  • 在复制集测试writeConcern参数

    1
    2
    3
    db.test.insert( {count: 1}, { writeConcern: {w: "majority" }})
    db.test.insert( {count: 1}, { writeConcern: {w: 3 }})
    db.test.insert( {count: 1}, { writeConcern: {w: 4 }})
  • 配置延迟节点,模拟网络延迟(复制延迟)

    1
    2
    3
    4
    conf=rs.conf()
    conf.members[2].slaveDelay = 5
    conf.members[2].priority = 0 //配置成不可以参与选举的节点
    rs.reconfig(conf)
  • 观察复制延迟下的写入,以及timeout参数

    1
    2
    db.test.insert( {count: 1}, { writeConcern:{w: 3}})
    db.test.insert( {count: 1},{ writeConcern: {w: 3, wtimeout:3000 }}) // 这里同步延迟时间为5s,但是等待3s会报错,数据实际上已经写进去了,实际业务场景中,最好做一个日志,方便回查

读操作事务

读数据操作中需要关注两个问题

  • 从哪里读?关注节点位置 (readReference)
  • 什么样的数据可以读?关注数据的隔离性 (readConcern)

什么是readReference?

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

  • primary:只选择主节点;
  • primaryPreferred:有限选择主节点,如果不可用则选择从节点
  • secondary:只选择从节点
  • secondaryPreferred:有限选择从节点,如果从节点不可用则选择主节点
  • nearest:选择最近的节点

场景举例:

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

readReferencetag

readReference只能控制使用一类节点,Tag可以更精准控制请求打到某几个节点。

readReference与tag

readReference 配置

  • 通过MongoDB的连接串参数:mongodb://host1:27107,host2:27107,host3:27017/?replicaSet=rs&readPreference=secondary
  • 通过MongoDB驱动程序API:MongoCollection.withReadPreference(ReadReference eadRef)
  • MongoShell:db.collection.find({}).readPref("sencondary")

可以使用 db.fsyncLock()来锁定写入,解除从节点锁定使用 db.fsyncUnlock()

配置了readReference/tag时,如果对应的节点失效则读操作失败,如主节点,则发生故障转移期间没有节点可读。

什么是readConcern

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

  • available:读取所有可用数据
  • local:读取所有可用且属于当前分片的数据
  • majority:读取在大多数节点上提交完成的数据(mvcc实现,相当于RC)
  • linearizable:可线性化读取文档
  • snapshot:读取最近快照中的数据(相当于RR)

ACID多文档事务支持

事务属性 支持程度
Atomicity 原子性 4.2开始支持分片集群多表多文档
Consistency 一致性 writeConcern,readConcern
Isolation 隔离性 readConcern
Durability 持久性 Journal and Replication

使用示例(java)

ACID多文档事务支持

其实也可以使用@transactional,配置好事务管理器就行

分片集群与高级运维之道

MongoDB常见的部署架构

MongoDB的分片类似于Mysql的分库分表。但是MongoDB天生支持分片。分片数最多可以达到1024

MongoDB常见的部署架构

为什么要使用分片集群

可能的场景

  • 数据容量日益增大,访问性能日渐降低
  • 新品上线异常火爆,如何支持更多的并发用户
  • 单库已有10TB数据,恢复需要1-2天
  • 地理分布数据
  • ……

分片集群构成

完整的分片群

  • 路由节点 mongos:为集群提供单一的入口,转发应用端的请求,选择合适的数据节点进行读写,合并多个数据节点的返回。无状态,建议至少两个(高可用和负载均衡)。
  • 配置节点 Config:提供集群元数据存储,分片数据分布的映射。普通的复制集架构。
  • 数据节点 Shard:以复制集为单位,横向扩展,最多1024片,分片之间数据不重复,必须所有的分片缺一不可。

MongoDB分片集群的特点

  • 应用全透明,无特殊处理:单复制集的架构可以无缝升级成分片集群,应用无需特殊处理
  • 数据自动均衡:分片之间的数据自动平衡,如果检测到分片的数据不平衡,会自动均衡
  • 动态扩容,无须下线
  • 提供三种分片方式
    • 基于范围
    • 基于hash
    • 基于zone / tag

MongoDB分片方式

基于范围

基于数值范围对数据进行逻辑分块(chunk),而不是物理分块。

基于范围的分片方式

优点:片键范围查询性能好,优化读

缺点:数据极易出现分布不均,容易有热点。如自增主键有可能导致数据一直在某个分块写数据。

基于Hash

和基于范围分片方式优劣相反,根据片键的hash值分布到不同的chunk中。

基于hash的分片方式

优点:分布均匀,写优化,适合写操作非常多的情况,如日志、物联网等高并发的场景

缺点:范围查询效率低,因为数据不连续。

基于 Zone / Tag

根据特定的标签进行分片,如不同的地域读写操作在最近的机房节点中。

分片集群的设计

分片集群的设计

合理的架构

分片的大小
  • 关于数据:数据量不超过3TB,尽可能保持在2TB一个片;
  • 关于索引:常用索引必须纳入内存。
分片数量:经验公式
  • A = 所需存储总量 / 单服务可挂载容量
  • B = 工作集大小 / (单服务内存容量 * 0.6)
  • C = 并发总数 / (单机并发总数 * 0.7)
  • 分片数 = MAX(A, B, C)
分片的其他需求
  • 是否需要跨机房分布分片?
  • 是否需要容灾?
  • 高可用的要求如何?

正确的姿势

各种术语

各种概念有效到大

  • 片键 shard key:文档中的一个字段;
  • 文档 doc:包含 shard key 的一行数据;
  • 块 Chunk:包含 n 个文档;默认大小为64MB,系统自动规划,无需配置。
  • 分片 Shard:包含 n 个 chunk;
  • 集群 Cluster: 包含 n 个分片。
选择基数大的片键

类似于Mysql的索引条件,离散程度选择大的。

对于小基数的片键:

  • 因为备选址有限,那么快的总数量就有限;如性别,只能有两块
  • 随着数据增多,块的大小会越来越大
  • 太大的块会导致水平扩展移动块会非常困难

对于分布不均匀的片键:

  • 造成某些块的数据量急剧增大
  • 这些快压力随之增大
  • 数据均衡以chunk为单位,所以系统无能为力
定向性好

对主要查询要具有定向能力。例如,4个分片的集群,你希望读某条特定的数据。如果你用片键作为条件查询,mongos可以直接定位到具体的分片。反之,mongos 需要把查询发到4个分片,等到所有的分片都响应,mongos 才能响应应用端

足够的资源

  • mongosconfig 通常消耗较少的资源,可以使用低规格的服务器。mongos可以配置多一点的CPU,因为它需要创建很多的连接。
  • shard 需要更多的资源
    • 需要足以容纳热点数据索引的内存
    • 正确创建索引后,CPU通常不会成为瓶颈,除非设计非常多的运算
    • 磁盘尽量选择SSD
  • 在各项指标达到60%就要开始考虑扩容升级

性能诊断工具

常用的监控工具和手段

  • MongoDB Ops Manager(企业版,收费)
  • Percona
  • 通用监控平台,如grafana
  • 程序脚本

如何获取监控数据

  • 监控信息来源:
    • db.serverStatus()
    • db.isMaster()
    • mongoStats命令行工具(只有部分信息)
  • 注意: db.serverStatus()包含的监控信息是从上次看机到现为止的累计数据,不能简单使用

serverStatus()主要信息

  • connections:关于连接数的信息
  • locks:关于MongoDB使用的锁情况
  • network:CRUD的执行次数统计
  • opcounters:CRUD的执行次数统计
  • repl:复制集的配置信息
  • wiredTiger:包含大量WiredTiger执行情况的信息
    • block-manager:WT数据块的读写情况
    • session:session使用数量
    • concurrentTransactions:Ticket使用情况
  • mem:内存使用情况

监控报警的考量

  • 具备一定的容错机制一减少误报的发生

  • 总结应用各指标峰值

  • 适时调整报警阈值

  • 留出足够处理时间

  • 建议监控指标

    指标 意义 获取
    opcounters 查询、更新、插入、删除、getmore 和其他命令的数量 db.serverStatus().opcounters
    tickets(令牌) WiredTiger 存储引擎的读/写令牌数量。令牌数量表示可以进入存储引擎的并发操作数 db.serverStatus().wiredTiger.concurrentTransaction
    replication lag(复制延迟) 写操作到达从节点所需的最小时间。 db.adminCommand({ "replSetGetStatus": 1 })
    oplog window(复制时间窗) oplog所能容纳多长时间的写操作。即从节点允许宕机的最大时间,否则从节点数据会出现部分丢失。建议在24小时以上 db.oplog.rs.find().sort({ $natural: -1 }).limit(1).next().ts - db.oplog.rs.find().sort({ $natural: 1 }).limit(1).next().ts
    connections(连接数) 连接数应作为监控指标的一部分,因为每个连接都将消耗资源。应该计算低峰/正常/高峰时间的连接数,并制定合理的报警阈值范围 db.serverStatus().connections
    Query Targeting(查询专注度) 索引键/文档扫描数量比返回的文档数量,按秒平均。如果该值比较高表示查询系需要进行很多低效的扫描来满足查询。这个情况通常代表了索引不当或缺少索引来支持查询 var status = db.serverStatus()
    status.metrics.queryExecutor.scanned / status.metrics.document.returned
    status.metrics.queryExecutor.scannedObjects / status.metrics.document.returned
    Scan and Order (扫描和排序) 每秒内内存排序操作所占的平均比例。内存排序的成本可能会十分昂贵,因为它们通常要求缓冲大量数据。如果有适当索引的情况下,内存排序是可以避免的 var status db.serverStatus()
    status.metrics.operation.scanAndOrder
    status.opcounters.query
    节点状态 每个节点的状态,如果状态节点不是 PRIMARYSECONDARYARBITER中的一个,或无法执行上述的命令则报警 db.runCommand('isMaster')
    dataSize(数据大小) 整个实例数据总量(压缩前) 每个DB执行 db.stats
    StorageSize(磁盘空间) 已使用的磁盘空间占总空间的百分比

MongoDB的备份和恢复

备份的目的

  • 防止硬件故障引起的数据丢失
  • 防止人为误删数据
  • 时间回溯
  • 监管要求

MongoDB的备份机制

有点类似与redis的RDBAOF,可以对比加深理解

  • 延迟节点备份
  • 全量备份 + Oplog增量
    • mongodump
    • 复制数据文件
    • 文件系统快照

MongoDB安全架构

略。和开发关系不大。

MongoDB索引

术语

  • index索引、key键、DataPage数据页
  • Covered Query - 索引覆盖查询,对比Mysql
  • IXSCAN 索引扫描 、COLLSCAN 集合扫描(全表扫描)
  • Query Shape 查询形状,即查询用到了哪些字段
  • Index Prefix 索引前缀,参考Mysql最左匹配原则
  • Selectivity 过滤性,参考Mysql的数据离散度说法

B-树

参考 Java面试八股文-Mysql索引部分

索引执行计划

索引执行计划

假设有两个索引

1
2
db.createIndex({ "city": 1 })
db.createIndex({"last_name": 1 })

那么查询语句

1
db.find({"city": "LA","last_name": "Parker"})

问题:会走哪个索引?

和Mysql类似,会先尝试走哪个索引比较快,最终还是成本的问题。具体流程是先两个索引都各查1000条,看哪个返回快,然后走哪个,然后把这个查询计划保存起来,下次继续使用。但是MongoDB选择的有时候并非是最优索引,这时候可以通过配置实现

1
2
3
4
5
6
7
8
9
db.collection.find({ "field": "value" }).hint({ "indexName": 1 })
db.collection.find({ "field": "value", "$hint": "indexName" })
// 强制查询走名为 "indexName" 的索引
db.collection.find({ "field": "value" }).hint("indexName")
// 强制查询走多个字段组成的索引,-1为降序排序
db.collection.find({ "field1": "value1", "field2": "value2" }).hint({ "field1": 1, "field2": -1 })

//禁用索引,,强制查询按照文档在磁盘上的物理顺序进行扫描
db.collection.find({ "$query": { "field": "value" }, "$natural": 1 })

MongoDB索引类型

  • 单键索引:单一字段

  • 组合索引:多个字段。遵循ESR原则

    ESR原则

    • 精确 - Equals:匹配的字段放最前面,E有很多的时候,考虑E的离散度,离散程度越高的排越前面
    • 排序 - Sort:条件放中间
    • 范围 - Range:范围放最后
    • 同样适用于ES/ER

    例如:

    db.members.find({gender: F, age:{$gte: 18}}).sort({"join_date": 1})

    遵循ESR原则就是 db.members.createIndex({"gender": 1,"join_date": 1, "age": 1 })

  • 多值索引:从属字段,如field.sub_field

  • 地理位置索引:地理位置索引,用的不多,但是很实用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //创建索引
    db.geo_col.createIndex(
    {location: "2d"},
    {min: -20, max: 20, bits: 10},
    {collation: { locale: "simple" }}
    )
    //查询
    db.geo_col.find({
    location:{
    $geoWithin:{
    $box:[ //查询在这个正方形中的数据
    [1,1],
    [3,3]
    ]
    }
    }
    })
  • 全文索引:只能说专业的事情给专业的中间件做吧,课程讲了这个是新功能,不太成熟,在数据量小,准确度要求不高的情况下可以用。还不如直接上elasticsearch

  • TTL索引:设置数据过期时间,类似于redis的功能

    1
    2
    3
    4
    //在date_created的十秒后失效
    db.collection.createIndex({date_created: new Date('2019-01-03T16:00:00Z')},{expiredAfterSeconds: 10000})
    //在特定时间失效可以把expiredAfterSeconds设置为0,把时间索引字段设置为过期时间
    db.collection.createIndex({expiredAt:new Date('2022-01-03T16:00:00Z')},{expiredAfterSeconds: 0})
  • 部分索引:针对部分数据建索引,这个是Mysql不具备的。

    1
    2
    //针对a大于等于5的数据建索引
    db.collection.createIndex({a: 1},{partialFilterExpression: {a: {$gte: 5}}})
  • 哈希索引

后续部分和开发关系不大,不看了,感兴趣的可以自己去看下

企业架构师进阶之法

主要是讲一些案例,建议看视频好一点。MongoDB高手课