前言
根据一下思维导图对MongoDB进行学习
了解MongoDB
MongoDB概念
MongoDB是一个以Json为数据模型的文档,可扩展的、跨平台的非关系性数据库;
MongoDB常用术语
_id – 这是每个MongoDB文档中必填的字段。_id字段表示MongoDB文档中的唯一值。_id字段类似于文档的主键。如果创建的新文档中没有_id字段,MongoDB将自动创建该字段。
集合 – 这是MongoDB文档的分组。集合等效于在任何其他RDMS(例如Oracle或MS SQL)中创建的表。集合存在于单个数据库中。从介绍中可以看出,集合不强制执行任何结构。
游标 – 这是指向查询结果集的指针。客户可以遍历游标以检索结果。
数据库 – 这是像RDMS中那样的集合容器,其中是表的容器。每个数据库在文件系统上都有其自己的文件集。MongoDB服务器可以存储多个数据库。
文档 - MongoDB集合中的记录基本上称为文档。文档包含字段名称和值。
字段 - 文档中的名称/值对。一个文档具有零个或多个字段。字段类似于关系数据库中的列
MongoDB优缺点
优势:
1、文档结构的存储方式(json),更便于查询;
2、性能优越,大量级数据量,对索引字段进行查询不比myql慢,而对非索引字段的查询,性能比mysql好;
3、MongoDB使用与SQL一样强大的基于文档的查询语言支持对文档的动态查询。
4、全索引支持,扩展到内部对象和内嵌数组
劣势:占用空间大,吃内存 (为什么吃内存)
DB的数据逻辑结构
MongoDB 数据逻辑结构分为数据库(database)、集合(collection)、文档(document)三层 :
MongoDB支持的数据类型
常用的数据类型
Object ID | _id,Documents 自生成的 id |
String | {"key":"123"} |
Integer | {"key": 123} |
Double | {"key": 123.12} |
Boolean | {"key": true} |
Array | {"key":[{"k","123"}] } |
Object(内嵌文档) | {"key": {"k","123"} } |
null | {"key": null} |
Timestamp | {"key": new Timestamp()} |
正则 | {"key": /a/} |
MongoDB常用指令
显示数据库列表
show dbs
show databases
显示库中的集合
show tables
show collections
显示帮助信息
db.help() 显示数据库操作命令
db.user.help() 显示集合操作命令,user代指某个集合
切换数据库
语法:use [数据库名称]
备注:mongodb使用数据库之前,不需要事先创建库和表,也不需要指定表结构
直接use dbname,如果该库存在则进入,如果不存在,则自动创建
进入不存在的库后,如果未往里面添加数据就退出,则该库自动删除,反之则保留
集合也不需要手动创建,像不存在的集合插入数据后,集合自动创建
查看当前数据库
db.getName()
删除当前数据库
db.dropDatabase()
获取当前库下的所有集合
show collections
db.getCollectionNames()
删除集合中所有记录(删除表中全部数据)
db.user.remove({}) //user代表集合名称
删除集合(删除表)
db.user.drop()
集合中添加文档
语法:db.集合名称.insert(document)
插文档时, 如果不指定_id参数, MongoDB会为文档分配一个唯一的ObjectId类型的_id;
插入单条数据使用字典, 插入多条数据使用列表
例子:单条数据插入:db.user.insert({name:'luogang',age:22})
多条数据插入: db.user.insert([{name:'1',age:10},{name:'2',age:11}]) 或者 for(i=3;i<5;i++){db.log.insert({"name":i,"age":6})}
集合中修改文档信息
语法: db.集合名称.update([参数query] ,[参数update],{multi: })
参数query:查询条件
参数update:更新一条数据,加$set表示未更新数据保留,否则未更新数据丢弃
参数multi:可选, 默认false只更新找到的第⼀条记录,true表示把满⾜条件的⽂档全部更新
例子:db.user.update({name:3},{$set:{age:'777'}},{multi: true})
删除文档
语法: db.集合名称.remove([参数query],{justOne: })
*参数query:可选,删除的⽂档的条件,不加条件删除所有记录
* 参数justOne:可选,默认false表示删除多条, true或1则只删除⼀条
查询文档
查询全部数据
db.集合名称.find()
备注:默认每页显示20条记录,可以设置每页显示数据的大小,用DBQuery.shellBatchSize= 50;这样每页就显示50条记录了。
查询数据带条件
db.user.find({"age": 6}); //查询age = 6的记录 相当于: select * from user where age = 6;
db.user.find({age: {$gt: 6}}); //查询age > 6的记录 相当于:select * from user where age >6;
db.user.find({age: {$lt: 6}}); //查询age < 6的记录 相当于:select * from user where age <6;
db.user.find({age: {$gte: 7}}); //查询age >= 7的记录 相当于select * from user where age >= 7;
db.user.find({age: {$lte: 7}}); //查询age <= 7的记录
db.user.find({age: {$gte: 23, $lte: 26}}) //查询age >= 23 并且 age<26
db.user.find({name: 'abc', age: 1}); //相当于:select * from user where name = 'abc' and age = '1';
db.user.find({$or: [{age: 1}, {age: 2}]}); //相当于:select * from user where age = 1 or age = 2;
db.userInfo.find().limit(5); //查询前5条数据
db.userInfo.find().skip(10); //查询10条以后的数据
db.userInfo.find().limit(10).skip(5); //查询在5-10之间的数据 可用于分页,limit是pageSize,skip是第几页*pageSize
模糊查询数据
db.user.find({name: /m/}); //查询name中包含 m的数据 相当于select * from user where name like '%m%';
db.user.find({name: /^m/}); //查询name中以m开头的 select * from user where name like 'm%';
查询集合中指定的列数据
db.集合名称.find(query查询条件,{列名:1,列名:1})
例子:db.user.find({age:19}, {name: 1, age: 1}); 相当于:select name, age from userInfo; 当然name也可以用true或false,当用ture的情况下河name:1效果一样,如果用false就是排除name,显示name以外的列信息。
查询数据排序
升序:db.user.find().sort({age: 1});
降序:db.userIn.find().sort({age: -1})
查询某个结果集的记录条数
db.user.find({age: {$gte: 25}}).count();
相当于:select count(*) from userInfo where age >= 20;
索引
db.userInfo.ensureIndex({name: 1}); //创建索引
db.userInfo.ensureIndex({name: 1, ts: -1}); //创建索引
db.userInfo.getIndexes(); //查询当前聚集集合所有索引
db.userInfo.totalIndexSize(); //查看总索引记录大小
db.users.reIndex(); //读取当前集合的所有index信息
db.users.dropIndex("name_1"); //删除指定索引
db.users.dropIndexes(); //删除所有索引索引
事务
写操作事务
疑问:写操作如何判断写操作成功
writeConcern 决定一个写操作落到多少个节点上才算成功,writeConcern 的取值包括:
• 0:发起写操作,不关心是否成功;
• 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功;
• majority:写操作需要被复制到大多数节点上才算成功。
发起写操作的程序将阻塞到写操作到达指定的节点数为止。
a、不作任何特别设定(w: 0):
发起写操作写数据到Primary,由于writeConcern为默认值0,不考虑写操作是否成功,可能存在写操作把数据写到内存中,但是未写到磁盘中。如果此时Primary宕机,那么secondary1和secondary2无法同步到数据,会导致存在数据丢失;
x=1的黑色实线箭头代表一个写操作线程。虚线箭头代表另一个同步数据的线程
b、大多数节点确认模式(w:"majority")
有三个节点,那么只需要写入两个节点(Primary节点和任意一个secondary节点任意一个节点)就算写入成功。
c、全部节点确认模式(w:"all")
所有节点都需要写入才算成功。
d、journal(j:true)
writeConcern 可以决定写操作到达多少个节点才算成功,journal 则定义如何才算成
功。取值包括:
• true: 写操作落到 journal 文件中才算成功;
• false: 写操作到达内存即算作成功。
注意事项:
• 虽然多于半数的 writeConcern 都是安全的,但通常只会设置 majority,因为这是
等待写入延迟时间最短的选择;
• 不要设置 writeConcern 等于总节点数,因为一旦有一个节点故障,所有写操作都
将失败;
• writeConcern 虽然会增加写操作延迟时间,但并不会显著增加集群压力,因此无论
是否等待,写操作最终都会复制到所有节点上。设置 writeConcern 只是让写操作
等待复制后再返回而已;
• 应对重要数据应用 {w: “majority”},普通数据可以应用 {w: 1} 以确保最佳性能。
db.user.insert( {count: 1}, {writeConcern: {w: 3}})
db.user.insert( {count: 1}, {writeConcern: {w: 3, wtimeout:3000 }})
读操作事务
readPreference
疑问:
• 从哪里读?
• 什么样的数据可以读?
第一个问题是是由 readPreference 来解决;
第二个问题则是由 readConcern 来解决;
readPreference参数
readPreference 决定使用哪一个节点来满足正在发起的读请求。可选值包括:
• primary: 只选择主节点;
• primaryPreferred:优先选择主节点,如果不可用则选择从节点;
• secondary:只选择从节点;
• secondaryPreferred:优先选择从节点,如果从节点不可用则选择主节点;
• nearest:选择最近的节点;
readPreference应用场景
• 对数据时效性要求较高的——primary/primaryPreferred。
• 对数据时效性要求较低的——secondary/secondaryPreferred。
• 对数据时效性要求较低的同时资源需求量大——secondary。
• 对数据分发到全世界,让各地用户能够就近读取——nearest。
readPreference 与 Tag
readPreference 只能控制使用一类节点。Tag 则可以将节点选择控制到一个或几个节点。
Tag应用场景:副本集有多个节点,部分节点硬件配置比较高,另一部分硬件配置比较低。可以通过tag来标记区分这两类节点,配置高的节点设置成专门服务于线上用户,配置低的比如可以服务生成报表的。
readPreference配置
通过 MongoDB 的连接串参数:
mongodb://host1:27107,host2:27107/?replicaSet=rs&readPreference=secondary
通过 MongoDB 驱动程序 API:
MongoCollection.withReadPreference(ReadPreference readPref)
Mongo Shell:
db.collection.find({}).readPref( “secondary” )
注意事项:
• 指定 readPreference 时也应注意高可用问题。例如将 readPreference 指定 primary,则发生故障转移不存在 primary 期间将没有节点可读。如果业务允许,则应选择 primaryPreferred;
• 使用 Tag 时也会遇到同样的问题,如果只有一个节点拥有一个特定 Tag,则在这个节点失效时将无节点可读。线上节点失效,通常希望有替代节点;
• Tag 有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它成为主节点,则优先级应为 0。
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,观察各节点读取x的值
(虚线代表同步主节点数据线程)
图解:当前副本集有三个节点(Primary,Secondary1,Secondary2),设置majority 值代表至少需要两个节点x值更新成1,对应节点查询才能查出x=1;
前提条件: majority--只读取大多数据节点上都提交了的数据。
A->B->C->D
A->B 代表 secondary1 复制Primary节点的数据
B->C代表 secondary1 响应 Primary节点说它已经拿到x的数据 (Primary知道secondary1也已经拿到x的数据=2个节点)
C->D代表Primary响应secondary1(secondary1节点知道除了它本身Primary节点也获取到更新x值)
A->E->F->G
A->E 代表 secondary2 复制Primary节点的数据
E->F代表 secondary2 响应 Primary节点说它已经拿到x的数据 (Primary知道secondary2也已经拿到x的数据=2个节点)
F->G代表Primary响应secondary2(secondary2节点知道除了它本身Primary节点和secondary1 也获取到更新x值)
时间轴流程
t0时间:只有Primary知道x数据写进去,不满足前提条件,故3个节点查询处理的数据都是0;
t1时间:Primary和secondary1都只知道自己有x更新数据,不满足前提条件,故3个节点查询处理的数据都是0;
t2时间:Primary和secondary2都只知道自己有x更新数据,不满足前提条件,故3个节点查询处理的数据都是0;
t3时间:Primary得到secondary1响应,知道secondary1也更新数据,故Primary查询出来的数据为1,而Secondary1,Secondary2节点仍然只知道自己的数据自己的数据有更新故查询x仍然为0,
t4时间:Primary得到secondary2响应,知道secondary2也更新数据(t3时间知道secondary1更新数据),故满足前提条件Primary查询出来的数据为1,而Secondary1,Secondary2节点仍然只知道自己的数据自己的数据有更新故查询x仍然为0,
t5时间:Secondary1得到Primary响应,secondary1知道Primary也更新数据,故Primary,Secondary1查询出来的数据为1,而Secondary2节点仍然只知道自己的数据自己的数据有更新故查询x仍然为0,
t6时间:Secondary2得到Primary响应,Secondary2知道Primary和Secondary2也更新数据,故3个节点查询处理的数据都为1
如果在各节点上应用{readConcern: “majority”} 来读取x数据:
Primary | Secondary1 | Secondary2 | |
t0 | 0 | 0 | 0 |
t1 | 0 | 0 | 0 |
t2 | 0 | 0 | 0 |
t3 | 1 | 0 | 0 |
t4 | 1 | 0 | 0 |
t5 | 1 | 1 | 0 |
t6 | 1 | 1 | 1 |
readConcern: 如何实现安全的读写分离
考虑如下场景:
向主节点写入一条数据;立即从从节点读取这条数据。如何保证自己能够读到刚刚写入的数据?
使用 writeConcern + readConcern majority 来解决
//大多数节点都写入成功
db.orders.insert({ oid: 101, sku: "kiteboar", q: 1}, {writeConcern:{w: "majority”}})
//大多数节点都数据都提交成功
db.orders.find({oid:101}).readPref(“secondary”).readConcern("majority")
索引机制
MongoDB 索引类型
单键索引
组合索引
多值索引
地理位置索引
全文索引
TTL索引
部分索引
哈希索引
索引调优
使用explain()
db.col.find({name:1111}).explain(true)
索引的执行计划例子
坏的索引执行计划
{
"executionSuccess" : true,
"nReturned" : 1, //返回1条
"executionTimeMillis" : 58,
"totalKeysExamined" : 0,
"totalDocsExamined" : 99999, //扫描数据99999条
"executionStages" : {
"stage" : "COLLSCAN", //全文检索
"filter" : {"name" : {"$eq" : 1111}},
"nReturned" : 1,
"executionTimeMillisEstimate" : 53,
"works" : 100001,
"advanced" : 1,
"needTime" : 99999,
"needYield" : 0,
"saveState" : 783,
"restoreState" : 783,
"isEOF" : 1,
"invalidates" : 0,
"direction" : "forward",
"docsExamined" : 99999
}
好的索引执行计划
{
"executionSuccess" : true,
"nReturned" : 1, //返回一条
"executionTimeMillis" : 3, "totalKeysExamined" : 1,
"totalDocsExamined" : 1, //数据值扫描到1条 直接命中
"executionStages" : {
"stage" : "FETCH",
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"docsExamined" : 1,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN", //索引扫描
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 2,
"advanced" : 1,
… }
组合索引
组合索引的最佳方式:ESR原则
• 精确(Equal)匹配的字段放最前面
• 排序(Sort)条件放中间
• 范围(Range)匹配的字段放最后面
同样适用: ES, ER
工作模式:
左图,不遵循ESR原则, a索引精确匹配,b索引范围匹配,c索引需要先进行在内存排序在命中数据页
右图,遵循ESR原则,到C索引,不需要再进行排序操作
读写性能
一次数据库请求过程步骤
数据库-写请求步骤
数据库-读请求步骤
mongodb 再写入操作频繁,不仅需要考虑内存而且也要考虑磁盘io
集群
副本集(复制),主从复制(mongo4.0后不支持,不推荐使用),分片
副本集集群
为什么不推荐主从集群
副本集群替代了主从集群,副本集群支持自动故障转移(而主从集群:1.主服务器挂掉,从服务器不会自动取代主服务器。2.主服务器挂掉后重启,从服务器则会重新同步数据,之前同步过来的数据被初始化)
MongoDB的复制(副本集)
副本集是一组维护相同数据集合的 mongod实例。副本集包含多个数据承载节点和一个可选的仲裁节点(不存储数据,只参与投票)。在数据承载节点中,有且仅有一个成员为主节点,其他节点为从节点。
MongoDB复制工作原理
客户端连接到整个Mongodb副本集,主节点负责整个副本集的读写,副本集定期同步数据备份,
一但主节点挂掉,副本节点就会选举一个新的主服务器,副本集中的副本节点在主节点挂掉后通过
心跳机制检测到后,就会在集群内发起主节点的选举机制,自动选举一位新的主服务器。
官方推荐副本集机器至少需要多少个节点
3个 (如果具有大多数成员的数据中心宕机,则副本集会变为只读。例如一主一从,如果主节点挂了,从几点无法自动成为主节点,这样会导致副本集只可读,不能写。)
副本集最大可以有几个节点
50个,但只有7个有投票权
副本集节点类型
a、主节点(Primary):能够存储数据,可读可写
b、从节点(Secondary):能存储数据,可读不可写
c、仲裁者节点(Arbiter):不存储数据,只参与投票
d、隐藏节点(Hidden):能够存储数据,不能成为Primary节点并且在客户端不可见,不能参选,但是可以投票,主要作用是做延迟节点
副本集常见架构
a、PSS模式:由一个主节点和两个备节点所组成,即Primary+Secondary+Secondary
b、PSA模式由一个主节点、一个备节点和一个仲裁者节点组成,即Primary+Secondary+Arbiter
缺点:如果Secondary宕机了,会对主库造成一定的内存压力,正常只有3个节点情况下推荐使用PSS模式
c、PSH模式由一个主节点、一个备节点和一个隐藏节点组成,即Primary+Secondary+Hidden
副本集复制原理
MongoDB的复制功能是通过操作日志oplog实现的,操作日志包含了主节点的每一次写操作。
,备份节点通过查询主节点的oplog就可以知道需要进行复制的写操作。每个备份节点都维护着自己的oplog,记录着每一次从主节点复制数据的操作。这样,每个成员都可以作为同步源提供给其他成员使用(也就是说一个从节点不仅可以通过主节点的oplog同步数据,也能通过其他从节点的oplog同步数据,主节点和从节点都可以成为同步源),默认是以主节点作为同步源。
主节点触发选举条件
主节点故障
主节点网络不可达(默认心跳信息为10秒)
人工干预(rs.stepDown(600))
通过选举完成故障恢复
具有投票权的节点之间两两互相发送心跳;
当5次心跳未收到时判断为节点失联;
如果失联的是主节点,从节点会发起选举,选出新的主节点;
如果失联的是从节点则不会产生新的选举;
选举基于 RAFT一致性算法 实现,选举成功的必要条件是大多数投票节点存活
复制集节点有以下常见的选配项:
是否具有投票权(v 参数):有则参与投票;
优先级(priority 参数):优先级越高的节点越优先成为主节点。优先级为0的节点无法成为主节点;
隐藏(hidden 参数):复制数据,但对应用不可见。隐藏节点可以具有投票仅,但优先级必须为0;
延迟(slaveDelay 参数):复制 n 秒之前的数据,保持与主节点的时间差。
分片集群
MongoDB 的分片集群由如下三个部分组成:
Config:配置,本质上是一个 MongoDB 的副本集,负责存储集群的各种元数据和配置,如分片地址、chunks 等
Mongos:路由服务,不存具体数据,从 Config 获取集群配置讲请求转发到特定的分片,并且整合分片结果返回给客户端。
Mongod:一般将具体的单个分片叫 mongod,实质上每个分片都是一个单独的复制集群,具备负责集群的高可用特性。
分片集群较适用于数据超级大的场景;在mongo集群中,副本集集群使用的较多
MongoDB 支持两种分片算法来满足不同的查询需求:
区间分片:可以按 shardkey 做区间查询的分片算法,直接按照 shardkey 的值来分片。
hash分片:用的最多的分片算法,按 shardkey 的 hash 值来分片。hash 分片可以看作一种特殊的区间分片。
数据量达到多少适合分片
看你的硬件决定,硬件越好,可以存储的数据量就越大,常运维方便,建议数据量维持在一定量之内。这个量我们的经验值是每个节点2TB
数据节点mongod:以复制集为单位,横向扩展,最大1024片。分片之间数据不重复, 所有分片在一起才可以完整工作。
MongoDB日志
MongoDB日志分为系统日志,journal日志,oplog日志,慢查询日志
系统日志
MongoDB启动和停止的操作,以及服务器在运行过程中发生的任何异常信息。
日志配置方式:
配置系统日志的方法比较简单,在启动mongod时指定logpath参数即可
-logpath=/data/log/mongodb/serverlog.log -logappend
系统日志会向logpath指定的文件持续追加。
journal日志
journaling(日记) 日志功能则是 MongoDB 里面非常重要的一个功能 , 它保证了数据库服务器在意外断电 、 自然灾害等情况下数据的完整性。它通过预写式的redo日志为MongoDB增加了额外的可靠性保障。开启该功能时,MongoDB会在进行写入时建立一条Journal日志,其中包含了此次写入操作具体更改的磁盘地址和字节。因此一旦服务器突然停机,可在启动时对日记进行重放,从而重新执行那些停机前没能够刷新到磁盘的写入操作。
Journaling日志机制
运行MongoDB如果开启了journaling日志功能,MongoDB先在内存保存写操作,并记录journaling日志到磁盘,最后才会把数据改变刷入到磁盘上的数据文件。为了保证journal日志文件的一致性,写日志是一个原子操作。
如果开启了journal日志功能,MongoDB会在数据目录下创建一个journal文件夹,用来存放预写重放日志。同时这个目录也会有一个last-sequence-number文件。
如果MongoDB安全关闭的话,会自动删除此目录下的所有文件,如果是崩溃导致的关闭,不会删除日志文件。在MongoDB进程重启的过程中,journal日志文件用于自动修复数据到一个一致性的状态。
journal日志文件是一种往文件尾不停追加内容的文件,它命名以j._开头,后面接一个数字(从0开始)作为序列号。如果文件超过1G大小,MongoDB会新建一个journal文件j._1。
只要MongoDB把特定日志中的所有写操作刷入到磁盘数据文件,将会删除此日志文件。因为数据已经持久化,不再需要用它来重放恢复数据了。
在数据库宕机时 , 为保证 MongoDB 中数据的持久性,MongoDB 使用了 Write Ahead Logging 向磁盘上的 journal 文件预先进行写入。除了 journal 日志,MongoDB 还使用检查点(checkpoint)来保证数据的一致性,当数据库发生宕机时,我们就需要 checkpoint 和 journal 文件协作完成数据的恢复工作。
机制
运行 MongoDB 如果开启了 journaling 日志功能,MongoDB 先在内存保存写操作,并记journaling 日志到磁盘,然后才会把数据改变刷入到磁盘上的数据文件。为了保证 journal 日志文件的一致性,写日志是一个原子操作。
oplog日志
mongodb的复制功能是使用操作日志oplog实现的,操作日志包含了主节点的每一次写操作。
慢查询日志
查询日志,可以定位分析性能的瓶颈;
MongoDB的存储引擎
MongoDB存储引擎有 WiredTiger、MMAPv1、In-Memory,本文主要讲解WiredTiger引擎;
WiredTiger数据结构
WiredTiger存储引擎还可以支持B-Tree和LSM两种结构组织数据,但MongoDB在使用WiredTiger作为存储引擎时,目前默认配置是使用了B-Tree结构。
WiredTiger会按需将磁盘的数据以page为单位加载到内存,同时在内存会构造相应的B-Tree来存储这些数据。为了高效的支撑CRUD等操作以及将内存里面发生变化的数据持久化到磁盘上,WiredTiger也会在内存里面维护其它几种数据结构,如下图所示:
WiredTiger的Page生命周期
Page典型的生命周期
第一步:pages从磁盘读到内存;
第二步:pages在内存中被修改;
第三步:被修改的脏pages在内存被reconcile(从内存 page 写入磁盘的操作叫做 reconcile),完成后将discard这些pages。
第四步:pages被选中,加入淘汰队列,等待被evict线程淘汰出内存;
第五步:evict线程会将“干净“的pages直接从内存丢弃(因为相对于磁盘page来说没做任何修改),将经过reconcile处理后的磁盘映像写到磁盘再丢弃“脏的”pages。
WiredTiger的缓存机制
缓存机制叫eviction cache或者WT cache。
高速写入数据时WT引擎会间歇性写挂起:原因-->将页数据(page)由内存中写入磁盘上是需要写入时间,必定会和应用程序的高速不间断写产生竞争;
eviction cache是一个LRU cache,即页面置换算法缓冲区,LRU cache最早出现的地方是操作系统中关于虚拟内存和物理内存数据页的置换实现,后被数据库存储引擎引入解决内存和磁盘不对等的问题。
eviction cache原理:不过WT的eviction cache对数据页采用的是分段局部扫描和淘汰,而不是对内存中所有的数据页做全局管理。基本思路是一个线程阶段性的去扫描各个btree,并把btree可以进行淘汰的数据页添加到一个lru queue中,当queue填满了后记录下这个过程当前的btree对象和btree的位置(这个位置是为了作为下次阶段性扫描位置),然后对queue中的数据页按照访问热度排序,
最后各个淘汰线程按照淘汰优先级淘汰queue中的数据页,整个过程是周期性重复。WT的这个evict过程涉及到多个eviction thread和hazard pointer技术。
WiredTiger的checkpoint
本质上来说,Checkpoint相当于一个日志,记录了上次Checkpoint后相关数据文件的变化。
Checkpoint主要有两个目的: 一是将内存里面发生修改的数据写到数据文件进行持久化保存,确保数据一致性;二是实现数据库在某个时刻意外发生故障,再次启动时,缩短数据库的恢复时间;
Checkpoint执行的触发时机
触发checkpoint执行,通常有如下几种情况:
按一定时间周期:默认60s,执行一次checkpoint;
按一定日志文件大小:当Journal日志文件大小达到2GB(如果已开启),执行一次checkpoint;任何打开的数据文件被修改,关闭时将自动执行一次checkpoint。
参考文献
https://pdai.tech/md/db/nosql-mongo/mongo.html
https://www.mongodb.com/docs/manual/
https://mongoing.com/archives/2789
https://blog.csdn.net/Kiven_ch/article/details/119479513
https://zhuanlan.zhihu.com/p/497736109?utm_medium=social&utm_oi=639733965527846912