文章目录
-
- MongoDB中的概念
- MongoDB文档操作
- Mongo数据类型
- SpringBoot整合MongoDB
- 聚合操作
- MongoDB索引详解
-
- 索引类型
-
- [**单键索引(Single Field Indexes)**](#单键索引(Single Field Indexes))
- [**复合索引(Compound Index)**](#复合索引(Compound Index))
- [**多键(数组)索引(Multikey Index)**](#多键(数组)索引(Multikey Index))
- [**Hash索引(Hashed Indexes)**](#Hash索引(Hashed Indexes))
- [**地理空间索引(Geospatial Index)**](#地理空间索引(Geospatial Index))
- [**全文索引(Text Indexes)**](#全文索引(Text Indexes))
- [**通配符索引(Wildcard Indexes)**](#通配符索引(Wildcard Indexes))
- 索引属性
-
- [唯一索引(Unique Indexes)](#唯一索引(Unique Indexes))
- [部分索引(Partial Indexes)](#部分索引(Partial Indexes))
- [**稀疏索引(Sparse Indexes)**](#稀疏索引(Sparse Indexes))
- [**TTL索引(TTL Indexes)**](#TTL索引(TTL Indexes))
- [**隐藏索引(Hidden Indexes)**](#隐藏索引(Hidden Indexes))
- MongoDB集群
- MongoDB分片集群
- MongoDB高级集群架构设计
- MongoDB存储原理&多文档事务详解
- MongoDB开发规范
- MongoDB调优
-
- 三大导致MongoDB性能不佳的原因
- [什么是 Chang Streams](#什么是 Chang Streams)
MongoDB是一个文档数据库(以 JSON 为数据模型),由C++语言编写,旨在为WEB应用提供可扩展的高性能数据存储解决方案。
文档来自于"JSON Document",并非我们一般理解的 PDF,WORD 文档。
MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,数据格式是BSON,一种类似JSON的二进制形式的存储格式,简称Binary JSON ,和JSON一样支持内嵌的文档对象和数组对象,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。原则上 Oracle 和 MySQL 能做的事情,MongoDB 都能做(包括 ACID 事务)。
MongoDB中的概念
-
数据库(database):最外层的概念,可以理解为逻辑上的名称空间,一个数据库包含多个不同名称的集合。
-
集合(collection):相当于SQL中的表,一个集合可以存放多个不同的文档。
-
文档(document):一个文档相当于数据表中的一行,由多个不同的字段组成。
-
字段(field):文档中的一个属性,等同于列(column)。
-
索引(index):独立的检索式数据结构,与SQL概念一致。
-
_id:每个文档中都拥有一个唯一的_id字段,相当于SQL中的主键(primary key)。
-
视图(view):可以看作一种虚拟的(非真实存在的)集合,与SQL中的视图类似。从MongoDB 3.4版本开始提供了视图功能,其通过聚合管道技术实现。
-
聚合操作($lookup):MongoDB用于实现"类似"表连接(tablejoin)的聚合操作符。
尽管这些概念大多与SQL标准定义类似,但MongoDB与传统RDBMS仍然存在不少差异,包括:
-
半结构化,在一个集合中,文档所拥有的字段并不需要是相同的,而且也不需要对所用的字段进行声明。因此,MongoDB具有很明显的半结构化特点。除了松散的表结构,文档还可以支持多级的嵌套、数组等灵活的数据类型,非常契合面向对象的编程模型。
-
弱关系,MongoDB没有外键的约束,也没有非常强大的表连接能力。类似的功能需要使用聚合管道技术来弥补。
MongoDB文档操作
插入文档
MongoDB提供了以下方法将文档插入到集合中:
-
db.collection.insertOne ():将单个文档插入到集合中。
-
db.collection.insertMany ():将多个文档插入到集合中。
设置 writeConcern 参数的示例
shell
db.emps.insertOne(
{ name: "fox", age: 35},
{
writeConcern: { w: "majority", j: true, wtimeout: 5000 }
}
)
writeConcern 是 MongoDB 中用来控制写入确认的选项。以下是 writeConcern 参数的一些常见选项:
-
w:指定写入确认级别。如果指定为数字,则表示要等待写入操作完成的节点数。如果指定为 majority,则表示等待大多数节点完成写入操作。默认为 1,表示等待写入操作完成的节点数为 1。
-
j:表示写入操作是否要求持久化到磁盘。如果设置为 true,则表示写入操作必须持久化到磁盘后才返回成功。如果设置为 false,则表示写入操作可能在数据被持久化到磁盘之前返回成功。默认为 false。
-
wtimeout:表示等待写入操作完成的超时时间,单位为毫秒。如果超过指定的时间仍然没有返回确认信息,则返回错误。默认为 0,表示不设置超时时间。
查询文档
查询集合中的若干文档
语法格式如下:
db.collection.find(query, projection)
-
query :可选,使用查询操作符指定查询条件
-
projection :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。投影时,_id为1的时候,其他字段必须是1;_id是0的时候,其他字段可以是0;如果没有_id字段约束,多个其他字段必须同为0或同为1。
如果查询返回的条目数量较多,mongosh则会自动实现分批显示。默认情况下每次只显示20条,可以输入it命令读取下一批。
查询集合中的第一个文档
语法格式如下:
db.collection.findOne(query, projection)
如果你需要以易读的方式来读取数据,可以使用pretty)方法,语法格式如下:
db.collection.find().pretty()
注意:pretty()方法以格式化的方式来显示所有文档
查询逻辑运算符
-
$lt: 存在并小于
-
$lte: 存在并小于等于
-
$gt: 存在并大于
-
$gte: 存在并大于等于
-
$ne: 不存在或存在但不等于
-
$in: 存在并在指定数组中
-
$nin: 不存在或不在指定数组中
-
$or: 匹配两个或多个条件中的一个
-
$and: 匹配全部条件
正则表达式匹配查询
MongoDB 使用 $regex 操作符来设置匹配字符串的正则表达式。
sh
//使用正则表达式查找type包含 so 字符串的book
db.books.find({type:{$regex:"so"}})
//或者
db.books.find({type:/so/})
排序
在 MongoDB 中使用 sort() 方法对数据进行排序
shell
#指定按收藏数(favCount)降序返回
db.books.find({type:"travel"}).sort({favCount:-1})
- 1 为升序排列,而 -1 是用于降序排列
分页
skip用于指定跳过记录数,limit则用于限定返回结果数量。可以在执行find命令的同时指定skip、limit参数,以此实现分页的功能。
比如,假定每页大小为8条,查询第3页的book文档:
db.books.find().skip(16).limit(8)
-
.skip(16) 表示跳过前面 16 条记录,即前两页的所有记录。
-
.limit(8) 表示返回 8 条记录,即第三页的所有记录。
更新文档
MongoDB提供了以下方法来更新集合中的文档:
-
db.collection.updateOne ():即使多个文档可能与指定的筛选器匹配,也只会更新第一个匹配的文档。
-
db.collection.updateMany ():更新与指定筛选器匹配的所有文档。
更新操作符
操作符 | 格式 | 描述 |
---|---|---|
$set | {$set:{field:value}} | 指定一个键并更新值,若键不存在则创建 |
$unset | {$unset : {field : 1 }} | 删除一个键 |
$inc | {$inc : {field : value } } | 对数值类型进行增减 |
$rename | {$rename : {old_field_name : new_field_name } } | 修改字段名称 |
$push | { $push : {field : value } } | 将数值追加到数组中,若数组不存在则会进行初始化 |
$pushAll | {$pushAll : {field : value_array }} | 追加多个值到一个数组字段内 |
$pull | {$pull : {field : _value } } | 从数组中删除指定的元素 |
$addToSet | {$addToSet : {field : value } } | 添加元素到数组中,具有排重功能 |
$pop | {$pop : {field : 1 }} | 删除数组的第一个或最后一个元素 |
findAndModify
findAndModify兼容了查询和修改指定文档的功能,findAndModify只能更新单个文档
//将某个book文档的收藏数(favCount)加1
db.books.findAndModify({
query:{_id:ObjectId("6457a39c817728350ec83b9d")},
update:{$inc:{favCount:1}}
})
该操作会返回符合查询条件的文档数据,并完成对文档的修改。
与findAndModify语义相近的命令如下:
-
findOneAndUpdate:更新单个文档并返回更新前(或更新后)的文档。
-
findOneAndReplace:替换单个文档并返回替换前(或替换后)的文档。
删除文档
deleteOne &deleteMany
官方推荐使用 deleteOne() 和 deleteMany() 方法删除文档,语法格式如下:
sh
db.books.deleteOne ({ type:"novel" }) //删除 type等于novel 的一个文档
db.books.deleteMany ({}) //删除集合下全部文档
db.books.deleteMany ({ type:"novel" }) //删除 type等于 novel 的全部文档
注意:remove、deleteMany命令需要对查询范围内的文档逐个删除,如果希望删除整个集合,则使用drop命令会更加高效
批量操作
bulkwrite()方法提供了执行批量插入、更新和删除操作的能力。
bulkWrite()支持以下写操作:
-
insertOne
-
updateOne
-
updateMany
-
replaceOne
-
deleteOne
-
deleteMany
每个写操作都作为数组中的文档传递给bulkWrite()。
Mongo数据类型
BSON在许多方面和JSON保持一致,其同样也支持内嵌的文档对象和数组结构。二者最大的区别在于JSON是基于文本的,而BSON则是二进制(字节流)编/解码的形式。在空间的使用上,BSON相比JSON并没有明显的优势。
MongoDB在文档存储、命令协议上都采用了BSON作为编/解码格式,主要具有如下优势:
-
类JSON的轻量级语义,支持简单清晰的嵌套、数组层次结构,可以实现模式灵活的文档结构。
-
更高效的遍历,BSON在编码时会记录每个元素的长度,可以直接通过seek操作进行元素的内容读取,相对JSON解析来说,遍历速度更快。
-
更丰富的数据类型,除了JSON的基本数据类型,BSON还提供了MongoDB所需的一些扩展类型,比如日期、二进制数据等,这更加方便数据的表示和操作。
BSON的数据类型
MongoDB中,一个BSON文档最大大小为16M,文档嵌套的级别不超过100
https://www.mongodb.com/docs/v6.0/reference/bson-types/
Type | Number | Alias | Notes |
---|---|---|---|
Double | 1 | "double" | |
String | 2 | "string" | |
Object | 3 | "object" | |
Array | 4 | "array" | |
Binary data | 5 | "binData" | 二进制数据 |
Undefined | 6 | "undefined" | Deprecated. |
ObjectId | 7 | "objectId" | 对象ID,用于创建文档ID |
Boolean | 8 | "bool" | |
Date | 9 | "date" | |
Null | 10 | "null" | |
Regular Expression | 11 | "regex" | 正则表达式 |
DBPointer | 12 | "dbPointer" | Deprecated. |
JavaScript | 13 | "javascript" | |
Symbol | 14 | "symbol" | Deprecated. |
JavaScript code with scope | 15 | "javascriptWithScope" | Deprecated in MongoDB 4.4. |
32-bit integer | 16 | "int" | |
Timestamp | 17 | "timestamp" | |
64-bit integer | 18 | "long" | |
Decimal128 | 19 | "decimal" | New in version 3.4. |
Min key | -1 | "minKey" | 表示一个最小值 |
Max key | 127 | "maxKey" | 表示一个最大值 |
ObjectId生成器
MongoDB集合中所有的文档都有一个唯一的_id字段,作为集合的主键。在默认情况下,_id字段使用ObjectId类型,采用16进制编码形式,共12个字节。
为了避免文档的_id字段出现重复,ObjectId被定义为3个部分:
-
4字节表示Unix时间戳(秒)。
-
5字节表示随机数(机器号+进程号唯一)。
-
3字节表示计数器(初始化时随机)。
固定集合
固定集合(capped collection)是一种限定大小的集合,其中capped是覆盖、限额的意思。跟普通的集合相比,数据在写入这种集合时遵循FIFO原则。可以将这种集合想象为一个环状的队列,新文档在写入时会被插入队列的末尾,如果队列已满,那么之前的文档就会被新写入的文档所覆盖。通过固定集合的大小,我们可以保证数据库只会存储"限额"的数据,超过该限额的旧数据都会被丢弃。
SpringBoot整合MongoDB
文档操作
相关注解
-
@Document
-
修饰范围: 用在类上
-
作用: 用来映射这个类的一个对象为mongo中一条文档数据。
-
属性:( value 、collection )用来指定操作的集合名称
-
@Id
-
修饰范围: 用在成员变量、方法上
-
作用: 用来将成员变量的值映射为文档的_id的值
-
@Field
-
修饰范围: 用在成员变量、方法上
-
作用: 用来将成员变量及其值映射为文档中一个key:value对。
-
属性:( name , value )用来指定在文档中 key的名称,默认为成员变量名
-
@Transient
-
修饰范围:用在成员变量、方法上
-
作用:用来指定此成员变量不参与文档的序列化
聚合操作
聚合操作允许用户处理多个文档并返回计算结果。聚合操作组值来自多个文档,可以对分组数据执行各种操作以返回单个结果。
聚合操作包含三类:单一作用聚合、聚合管道、MapReduce。
-
单一作用聚合:提供了对常见聚合过程的简单访问,操作都从单个集合聚合文档。MongoDB提供 db.collection.estimatedDocumentCount(), db.collection.countDocument(), db.collection.distinct() 这类单一作用的聚合函数。 所有这些操作都聚合来自单个集合的文档。虽然这些操作提供了对公共聚合过程的简单访问,但它们缺乏聚合管道和map-Reduce的灵活性和功能。
-
聚合管道是一个数据聚合的框架,模型基于数据处理流水线的概念。文档进入多级管道,将文档转换为聚合结果。
-
MapReduce操作具有两个阶段:处理每个文档并向每个输入文档发射一个或多个对象的map阶段,以及reduce组合map操作的输出阶段。从MongoDB 5.0开始,map-reduce操作已被弃用。聚合管道比映射-reduce操作提供更好的性能和可用性。
聚合管道
MongoDB 聚合框架(Aggregation Framework)是一个计算框架,它可以:
-
作用在一个或几个集合上;
-
对集合中的数据进行的一系列运算;
-
将这些数据转化为期望的形式;
从效果而言,聚合框架相当于 SQL 查询中的GROUP BY、 LEFT OUTER JOIN 、 AS等。
管道(Pipeline)和阶段(Stage)
整个聚合运算过程称为管道(Pipeline),它是由多个阶段(Stage)组成的, 每个管道:
-
接受一系列文档(原始数据);
-
每个阶段对这些文档进行一系列运算;
-
结果文档输出给下一个阶段;
通过将多个操作符组合到聚合管道中,用户可以构建出足够复杂的数据处理管道以提取数据并进行分析。
聚合管道操作语法
js
pipeline = [$stage1, $stage2, ...$stageN];
db.collection.aggregate(pipeline, {options})
-
pipelines 一组数据聚合阶段。除out、Merge和$geonear阶段之外,每个阶段都可以在管道中出现多次。
-
options 可选,聚合操作的其他参数。包含:查询计划、是否使用临时文件、 游标、最大操作时间、读写策略、强制索引等等
MongoDB索引详解
索引是一种用来快速查询数据的数据结构。B+Tree就是一种常用的数据库索引数据结构,MongoDB采用B+Tree 做索引,索引创建collections上。MongoDB不使用索引的查询,先扫描所有的文档,再匹配符合条件的文档。 使用索引的查询,通过索引找到文档,使用索引能够极大的提升查询效率。
思考:MongoDB索引数据结构是B-Tree还是B+Tree?
mongodb用的数据结构是B-Tree,具体来说是B+Tree
B-Tree说法来源于官方文档,然后就导致了分歧:有人说MongoDB索引数据结构使用的是B-Tree,有的人又说是B+Tree。
MongoDB官方文档:https://docs.mongodb.com/manual/indexes/
WiredTiger官方文档:https://source.wiredtiger.com/3.0.0/tune_page_size_and_comp.html
WiredTiger maintains a table's data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values.
WiredTiger数据文件在磁盘的存储结构
B+ Tree中的leaf page包含一个页头(page header)、块头(block header)和真正的数据(key/value),其中页头定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息;块头定义了此页的checksum、块在磁盘上的寻址位置等信息。
WiredTiger有一个块设备管理的模块,用来为page分配block。如果要定位某一行数据(key/value)的位置,可以先通过block的位置找到此page(相对于文件起始位置的偏移量),再通过page找到行数据的相对位置,最后可以得到行数据相对于文件起始位置的偏移量offsets。
索引类型
与大多数数据库一样,MongoDB支持各种丰富的索引类型,包括单键索引、复合索引,唯一索引等一些常用的结构。由于采用了灵活可变的文档类型,因此它也同样支持对嵌套字段、数组进行索引。通过建立合适的索引,我们可以极大地提升数据的检索速度。在一些特殊应用场景,MongoDB还支持地理空间索引、文本检索索引、TTL索引等不同的特性。
单键索引(Single Field Indexes)
在某一个特定的字段上建立索引 mongoDB在ID上建立了唯一的单键索引,所以经常会使用id来进行查询; 在索引字段上进行精确匹配、排序以及范围查找都会使用此索引
复合索引(Compound Index)
复合索引是多个字段组合而成的索引,其性质和单字段索引类似。但不同的是,复合索引中字段的顺序、字段的升降序对查询性能有直接的影响,因此在设计复合索引时则需要考虑不同的查询场景。
多键(数组)索引(Multikey Index)
在数组的属性上建立索引。针对这个数组的任意值的查询都会定位到这个文档,既多个索引入口或者键值引用同一个文档
Hash索引(Hashed Indexes)
不同于传统的B-Tree索引,哈希索引使用hash函数来创建索引。在索引字段上进行精确匹配,但不支持范围查询,不支持多键hash; Hash索引上的入口是均匀分布的,在分片集合中非常有用;
地理空间索引(Geospatial Index)
在移动互联网时代,基于地理位置的检索(LBS)功能几乎是所有应用系统的标配。MongoDB为地理空间检索提供了非常方便的功能。地理空间索引(2dsphereindex)就是专门用于实现位置检索的一种特殊索引。
全文索引(Text Indexes)
MongoDB支持全文检索功能,可通过建立文本索引来实现简易的分词检索。
通配符索引(Wildcard Indexes)
MongoDB的文档模式是动态变化的,而通配符索引可以建立在一些不可预知的字段上,以此实现查询的加速。MongoDB 4.2 引入了通配符索引来支持对未知或任意字段的查询。
索引属性
唯一索引(Unique Indexes)
在现实场景中,唯一性是很常见的一种索引约束需求,重复的数据记录会带来许多处理上的麻烦,比如订单的编号、用户的登录名等。通过建立唯一性索引,可以保证集合中文档的指定字段拥有唯一值。
部分索引(Partial Indexes)
部分索引仅对满足指定过滤器表达式的文档进行索引。通过在一个集合中为文档的一个子集建立索引,部分索引具有更低的存储需求和更低的索引创建和维护的性能成本。3.2新版功能。
稀疏索引(Sparse Indexes)
索引的稀疏属性确保索引只包含具有索引字段的文档的条目,索引将跳过没有索引字段的文档。
特性: 只对存在字段的文档进行索引(包括字段值为null的文档)
TTL索引(TTL Indexes)
在一般的应用系统中,并非所有的数据都需要永久存储。例如一些系统事件、用户消息等,这些数据随着时间的推移,其重要程度逐渐降低。更重要的是,存储这些大量的历史数据需要花费较高的成本,因此项目中通常会对过期且不再使用的数据进行老化处理。
隐藏索引(Hidden Indexes)
隐藏索引对查询规划器不可见,不能用于支持查询。通过对规划器隐藏索引,用户可以在不实际删除索引的情况下评估删除索引的潜在影响。如果影响是负面的,用户可以取消隐藏索引,而不必重新创建已删除的索引。4.4新版功能。
MongoDB集群
MongoDB复制集
Mongodb复制集(Replication Set)由一组Mongod实例(进程)组成,包含一个Primary节点和多个Secondary节点,Mongodb Driver(客户端)的所有数据都写入Primary,Secondary从Primary同步写入的数据,以保持复制集内所有成员存储相同的数据集,提供数据的高可用。复制集提供冗余和高可用性,是所有生产部署的基础。它的现实依赖于两个方面的功能:
-
数据写入时将数据迅速复制到另一个独立节点上
-
在接受写入的节点发生故障时自动选举出一个新的替代节点
三节点复制集模式
PSS模式(官方推荐模式)
PSS模式由一个主节点和两个备节点所组成,即Primary+Secondary+Secondary。
PSA模式
PSA模式由一个主节点、一个备节点和一个仲裁者节点组成,即Primary+Secondary+Arbiter
在复制集架构中,主节点与备节点之间是通过oplog来同步数据的,这里的oplog是一个特殊的固定集合,当主节点上的一个写操作完成后,会向oplog集合写入一条对应的日志,而备节点则通过这个oplog不断拉取到新的日志,在本地进行回放以达到数据同步的目的。
MongoDB复制集原理
在复制集架构中,主节点与备节点之间是通过oplog来同步数据的,这里的oplog是一个特殊的固定集合,当主节点上的一个写操作完成后,会向oplog集合写入一条对应的日志,而备节点则通过这个oplog不断拉取到新的日志,在本地进行回放以达到数据同步的目的。
什么是oplog
-
MongoDB oplog 是 Local 库下的一个集合,用来保存写操作所产生的增量日志(类似于 MySQL 中 的 Binlog)。
-
它是一个 Capped Collection(固定集合),即超出配置的最大值后,会自动删除最老的历史数据,MongoDB 针对 oplog 的删除有特殊优化,以提升删除效率。
-
主节点产生新的 oplog Entry,从节点通过复制 oplog 并应用来保持和主节点的状态一致;
MongoDB分片集群
为什么要使用分片?
MongoDB复制集实现了数据的多副本复制及高可用,但是一个复制集能承载的容量和负载是有限的。在你遇到下面的场景时,就需要考虑使用分片了:
-
存储容量需求超出单机的磁盘容量。
-
活跃的数据集超出单机内存容量,导致很多请求都要从磁盘读取数据,影响性能。
-
写IOPS超出单个MongoDB节点的写服务能力。
垂直扩容(Scale Up) VS 水平扩容(Scale Out):
垂直扩容 : 用更好的服务器,提高 CPU 处理核数、内存数、带宽等
水平扩容 : 将任务分配到多台计算机上
什么是chunk
chunk的意思是数据块,一个chunk代表了集合中的"一段数据",
chunk所描述的是范围区间,例如,db.users使用了userId作为分片键,那么chunk就是userId的各个值(或哈希值)的连续区间。集群在操作分片集合时,会根据分片键找到对应的chunk,并向该chunk所在的分片发起操作请求,而chunk的分布在一定程度上会影响数据的读写路径,这由以下两点决定:
-
chunk的切分方式,决定如何找到数据所在的chunk
-
chunk的分布状态,决定如何找到chunk所在的分片
分片算法
chunk切分是根据分片策略进行实施的,分片策略的内容包括分片键和分片算法。当前,MongoDB支持两种分片算法:
范围分片(range sharding)
假设集合根据x字段来分片,x的完整取值范围为[minKey, maxKey](x为整数,这里的minKey、maxKey为整型的最小值和最大值),其将整个取值范围划分为多个chunk,例如:
-
chunk1包含x的取值在[minKey,-75)的所有文档。
-
chunk2包含x取值在[-75,25)之间的所有文档,依此类推。
哈希分片(hash sharding)
哈希分片会先事先根据分片键计算出一个新的哈希值(64位整数),再根据哈希值按照范围分片的策略进行chunk的切分。适用于日志,物联网等高并发场景。
MongoDB高级集群架构设计
RPO&RTO
RPO(Recovery Point Objective):即数据恢复点目标,主要指的是业务系统所能容忍的数据丢失量。
RTO(Recovery Time Objective):即恢复时间目标,主要指的是所能容忍的业务停止服务的最长时间,也就是从灾难发生到业务系统恢复服务功能所需要的最短时间周期
MongoDB存储原理&多文档事务详解
存储引擎是数据库的组件,负责管理数据如何存储在内存和磁盘上。MongoDB支持多个存储引擎,因为不同的引擎对于特定的工作负载表现更好。选择合适的存储引擎可以显著影响应用程序的性能。
MongoDB从3.0开始引入可插拔存储引擎的概念,主要有MMAPV1、WiredTiger存储引擎可供选择。从MongoDB 3.2开始,WiredTiger存储引擎是默认的存储引擎。从4.2版开始,MongoDB删除了废弃的MMAPv1存储引擎。
WiredTiger读写模型
读缓存
理想情况下,MongoDB可以提供近似内存式的读写性能。WiredTiger引擎实现了数据的二级缓存,第一层是操作系统的页面缓存,第二层则是引擎提供的内部缓存。
写缓冲
当数据发生写入时,MongoDB并不会立即持久化到磁盘上,而是先在内存中记录这些变更,之后通过CheckPoint机制将变化的数据写入磁盘。为什么要这么处理?主要有以下两个原因:
-
如果每次写入都触发一次磁盘I/O,那么开销太大,而且响应时延会比较大。
-
多个变更的写入可以尽可能进行I/O合并,降低资源负荷。
MongoDB会丢数据吗
MongoDB单机下保证数据可靠性的机制包括以下两个部分:
CheckPoint(检查点)机制
快照(snapshot)描述了某一时刻(point-in-time)数据在内存中的一致性视图,而这种数据的一致性是WiredTiger通过MVCC(多版本并发控制)实现的。当建立CheckPoint时,WiredTiger会在内存中建立所有数据的一致性快照,并将该快照覆盖的所有数据变化一并进行持久化(fsync)。成功之后,内存中数据的修改才得以真正保存。默认情况下,MongoDB每60s建立一次CheckPoint,在检查点写入过程中,上一个检查点仍然是可用的。这样可以保证一旦出错,MongoDB仍然能恢复到上一个检查点。
Journal日志
Journal是一种预写式日志(write ahead log)机制,主要用来弥补CheckPoint机制的不足。如果开启了Journal日志,那么WiredTiger会将每个写操作的redo日志写入Journal缓冲区,该缓冲区会频繁地将日志持久化到磁盘上。默认情况下,Journal缓冲区每100ms执行一次持久化。此外,Journal日志达到100MB,或是应用程序指定journal:true,写操作都会触发日志的持久化。一旦MongoDB发生宕机,重启程序时会先恢复到上一个检查点,然后根据Journal日志恢复增量的变化。由于Journal日志持久化的间隔非常短,数据能得到更高的保障,如果按照当前版本的默认配置,则其在断电情况下最多会丢失100ms的写入数据。
MongoDB多文档事务详解
事务(transaction)是传统数据库所具备的一项基本能力,其根本目的是为数据的可靠性与一致性提供保障。而在通常的实现中,事务包含了一个系列的数据库读写操作,这些操作要么全部完成,要么全部撤销。例如,在电子商城场景中,当顾客下单购买某件商品时,除了生成订单,还应该同时扣减商品的库存,这些操作应该被作为一个整体的执行单元进行处理,否则就会产生不一致的情况。
数据库事务需要包含4个基本特性,即常说的ACID,具体如下:
-
原子性(atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
-
一致性(consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
-
隔离性(isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
-
持久性(durability):已被提交的事务对数据库的修改应该是永久性的。
在MongoDB中,对单个文档的操作是原子的。由于可以在单个文档结构中使用内嵌文档和数组来获得数据之间的关系,而不必跨多个文档和集合进行范式化,所以这种单文档原子性避免了许多实际场景中对多文档事务的需求。
对于那些需要对多个文档(在单个或多个集合中)进行原子性读写的场景,MongoDB支持多文档事务。而使用分布式事务,事务可以跨多个操作、集合、数据库、文档和分片使用。
MongoDB 虽然已经在 4.2 开始全面支持了多文档事务,但并不代表大家应该毫无节制地使用它。相反,对事务的使用原则应该是:能不用尽量不用。 通过合理地设计文档模型,可以规避绝大部分使用事务的必要性。
使用事务的原则:
-
无论何时,事务的使用总是能避免则避免;
-
模型设计先于事务,尽可能用模型设计规避事务;
-
不要使用过大的事务(尽量控制在 1000 个文档更新以内);
-
当必须使用事务时,尽可能让涉及事务的文档分布在同一个分片上,这将有效地提高效率;
writeConcern
https://docs.mongodb.com/manual/reference/write-concern/
writeConcern 决定一个写操作落到多少个节点上才算成功。MongoDB支持客户端灵活配置写入策略(writeConcern),以满足不同场景的需求。
语法格式:
js
{ w: <value>, j: <boolean>, wtimeout: <number> }
1)w: 数据写入到number个节点才向客户端确认
-
{w: 0} 对客户端的写入不需要发送任何确认,适用于性能要求高,但不关注正确性的场景
-
{w: 1} 默认的writeConcern,数据写入到Primary就向客户端发送确认
-
{w: "majority"} 数据写入到副本集大多数成员后向客户端发送确认,适用于对数据安全性要求比较高的场景,该选项会降低写入性能
2)j: 写入操作的journal持久化后才向客户端确认
- 默认为{j: false},如果要求Primary写入持久化了才向客户端确认,则指定该选项为true
3)wtimeout: 写入超时时间,仅w的值大于1时有效。
- 当指定{w: }时,数据需要成功写入number个节点才算成功,如果写入过程中有节点故障,可能导致这个条件一直不能满足,从而一直不能向客户端发送确认结果,针对这种情况,客户端可设置wtimeout选项来指定超时时间,当写入过程持续超过该时间仍未结束,则认为写入失败。
在读取数据的过程中我们需要关注以下两个问题:
-
从哪里读?
-
什么样的数据可以读?
第一个问题是是由 readPreference 来解决,第二个问题则是由 readConcern 来解决
readPreference
readPreference决定使用哪一个节点来满足正在发起的读请求。可选值包括:
-
primary: 只选择主节点,默认模式;
-
primaryPreferred:优先选择主节点,如果主节点不可用则选择从节点;
-
secondary:只选择从节点;
-
secondaryPreferred:优先选择从节点, 如果从节点不可用则选择主节点;
-
nearest:根据客户端对节点的 Ping 值判断节点的远近,选择从最近的节点读取。
合理的 ReadPreference 可以极大地扩展复制集的读性能,降低访问延迟。
readConcern
在 readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据哪些是可读的,类似于关系数据库的隔离级别。可选值包括:
-
available:读取所有可用的数据;
-
local:读取所有可用且属于当前分片的数据;
-
majority:读取在大多数节点上提交完成的数据;
-
linearizable:可线性化读取文档,仅支持从主节点读;
-
snapshot:读取最近快照中的数据,仅可用于多文档事务;
readConcern: linearizable
只读取大多数节点确认过的数据。和 majority 最大差别是保证绝对的操作线性顺序
-
在写操作自然时间后面的发生的读,一定可以读到之前的写
-
只对读取单个文档时有效;
-
可能导致非常慢的读,因此总是建议配合使用 maxTimeMS;
readConcern: snapshot
{readConcern: "snapshot"} 只在多文档事务中生效。将一个事务的 readConcern 设置为 snapshot,将保证在事务中的读:
-
不出现脏读;
-
不出现不可重复读;
-
不出现幻读。
因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放。
MongoDB开发规范
(1)命名原则。数据库、集合命名需要简单易懂,数据库名使用小写字符,集合名称使用统一命名风格,可以统一大小写或使用驼峰式命名。数据库名和集合名称均不能超过64个字符。
(2)集合设计。对少量数据的包含关系,使用嵌套模式有利于读性能和保证原子性的写入。对于复杂的关联关系,以及后期可能发生演进变化的情况,建议使用引用模式。
(3)文档设计。避免使用大文档,MongoDB的文档最大不能超过16MB。如果使用了内嵌的数组对象或子文档,应该保证内嵌数据不会无限制地增长。在文档结构上,尽可能减少字段名的长度,MongoDB会保存文档中的字段名,因此字段名称会影响整个集合的大小以及内存的需求。一般建议将字段名称控制在32个字符以内。
(4)索引设计。在必要时使用索引加速查询。避免建立过多的索引,单个集合建议不超过10个索引。MongoDB对集合的写入操作很可能也会触发索引的写入,从而触发更多的I/O操作。无效的索引会导致内存空间的浪费,因此有必要对索引进行审视,及时清理不使用或不合理的索引。遵循索引优化原则,如覆盖索引、优先前缀匹配等,使用explain命令分析索引性能。
(5)分片设计。对可能出现快速增长或读写压力较大的业务表考虑分片。分片键的设计满足均衡分布的目标,业务上尽量避免广播查询。应尽早确定分片策略,最好在集合达到256GB之前就进行分片。如果集合中存在唯一性索引,则应该确保该索引覆盖分片键,避免冲突。为了降低风险,单个分片的数据集合大小建议不超过2TB。
(6)升级设计。应用上需支持对旧版本数据的兼容性,在添加唯一性约束索引之前,对数据表进行检查并及时清理冗余的数据。新增、修改数据库对象等操作需要经过评审,并保持对数据字典进行更新。
(7)考虑数据老化问题,要及时清理无效、过期的数据,优先考虑为系统日志、历史数据表添加合理的老化策略。
(8)数据一致性方面,非关键业务使用默认的WriteConcern:1(更高性能写入);对于关键业务类,使用WriteConcern:majority保证一致性(性能下降)。如果业务上严格不允许脏读,则使用ReadConcern:majority选项。
(9)使用update、findAndModify对数据进行修改时,如果设置了upsert:true,则必须使用唯一性索引避免产生重复数据。
(10)业务上尽量避免短连接,使用官方最新驱动的连接池实现,控制客户端连接池的大小,最大值建议不超过200。
(11)对大量数据写入使用Bulk Write批量化API,建议使用无序批次更新。
(12)优先使用单文档事务保证原子性,如果需要使用多文档事务,则必须保证事务尽可能小,一个事务的执行时间最长不能超过60s。
(13)在条件允许的情况下,利用读写分离降低主节点压力。对于一些统计分析类的查询操作,可优先从节点上执行。
(14)考虑业务数据的隔离,例如将配置数据、历史数据存放到不同的数据库中。微服务之间使用单独的数据库,尽量避免跨库访问。
(15)维护数据字典文档并保持更新,提前按不同的业务进行数据容量的规划。
MongoDB调优
三大导致MongoDB性能不佳的原因
1) 慢查询
2) 阻塞等待
3)硬件资源不足
1,2通常是因为模型/索引设计不佳导致的
排查思路:按1-2-3依次排查
mongostat需要关注的指标主要有如下几个:
-
插入、删除、修改、查询的速率是否产生较大波动,是否超出预期。
-
qrw、arw:队列是否较高,若长时间大于0则说明此时读写速度较慢。
-
conn:连接数是否太多。
-
dirty:百分比是否较高,若持续高于10%则说明磁盘I/O存在瓶颈。
-
netIn、netOut:是否超过网络带宽阈值。
-
repl:状态是否异常,如PRI、SEC、RTR为正常,若出现REC等异常值则需要修复。
Profiler模块
Profiler模块可以用来记录、分析MongoDB的详细操作日志。默认情况下该功能是关闭的,对某个业务库开启Profiler模块之后,符合条件的慢操作日志会被写入该库的system.profile集合中。Profiler的设计很像代码的日志功能,其提供了几种调试级别:
级别 | 说明 |
---|---|
0 | 日志关闭,无任何输出 |
1 | 部分开启,仅符合条件(时长大于slowms)的操作日志会被记录 |
2 | 日志全开,所有的操作日志都被记录 |
什么是 Chang Streams
Change Stream指数据的变化事件流,MongoDB从3.6版本开始提供订阅数据变更的功能。
Change Stream 是 MongoDB 用于实现变更追踪的解决方案,类似于关系数据库的触发器,但原理不完全相同。
Change Stream 是基于 oplog 实现的,提供推送实时增量的推送功能。它在 oplog 上开启一个 tailable cursor 来追踪所有复制集上的变更操作,最终调用应用中定义的回调函数。
被追踪的变更事件主要包括:
-
insert/update/delete:插入、更新、删除;
-
drop:集合被删除;
-
rename:集合被重命名;
-
dropDatabase:数据库被删除;
-
invalidate:drop/rename/dropDatabase 将导致 invalidate 被触发, 并关闭 change stream;