一、技术背景

1-1、介绍

MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。MongoDB的数据模型和持久化策略的设计目标是提供高读写吞吐量,在易于伸缩的同时还能进行自动故障转移。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

灵活的文档模型JSON 格式存储最接近真实对象模型,对开发者友好,方便快速开发迭代高可用复制集满足数据高可靠、服务高可用的需求,运维简单,故障自动切换可扩展分片集群海量数据存储,服务能力水平扩展高性能mmapv1、wiredtiger、mongorocks(rocksdb)、in-memory 等多引擎支持满足各种场景需求强大的索引支持地理位置索引可用于构建 各种 O2O 应用、文本索引解决搜索的需求、TTL索引解决历史数据自动过期的需、Gridfs解决文件存储的需求aggregation & mapreduce解决数据分析场景需求,用户可以自己写查询语句或脚本,将请求都分发到 MongoDB 上完成。mongodb4.0支持多文档事务。

1-2、适用场景

以下是几个实际的应用案例。

游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、更新

物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来。

社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能

物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析

视频直播,使用 MongoDB 存储用户信息、礼物信息等

……

是否应该使用 MongoDB,从以下几点来做决策:

应用不需要复杂 join

新应用,需求会变,数据模型无法确定,想快速迭代开发

应用需要2000-3000以上的读写QPS(更高也可以)

应用需要TB甚至 PB 级别数据存储

应用发展迅速,需要能快速水平扩展

应用要求存储的数据不丢失

应用需要99.999%高可用

应用需要大量的地理位置查询、文本查询

二、主要特性

2-1、主要特性

MongoDB 特性 优势
事务支持 4.0之前MongoDB只支持单文档事务,4.0+支持多文档事务,必须要是副本集。4.2提供分布式事务功能,即在分片集群上的事务支持
灵活的文档模型 JSON 格式存储最接近真实对象模型,对开发者友好,方便快速开发迭代
高可用复制集 满足数据高可靠、服务高可用的需求,运维简单,故障自动切换
可扩展分片集群 海量数据存储,服务能力水平扩展
高性能 mmapv1、wiredtiger(当前默认)、mongorocks(rocksdb)、in-memory 等多引擎支持满足各种场景需求
强大的索引支持 地理位置索引可用于构建 各种 O2O 应用、文本索引解决搜索的需求、TTL索引解决历史数据自动过期的需求,4.2提供基于lucene引擎的全文搜索能力
Gridfs 解决文件存储的需求
aggregation & mapreduce 解决数据分析场景需求,用户可以自己写查询语句或脚本,将请求都分发到 MongoDB 上完成

2-2、各个版本支持核心特性

image.png

2-3、文档模型的优点

读写效率高

​ 由于文档模型把相关数据集中在一块,在普通机械盘上读数据的时候不用花太多时间去定位磁头,因此在IO性能上有先天独厚的优势;

可扩展能力强

​ 关系型数据库很难做分布式的原因就是多节点海量数据关联有巨大的性能问题。如果不考虑关联,数据分区分库,水平扩展就比较简单;

动态模式

​ 文档模型支持可变的数据模式,不要求每个文档都具有完全相同的结构。对很多异构数据场景支持非常好;

模型自然

​ 文档模型最接近于我们熟悉的对象模型。从内存到存储,无需经过ORM的双向转换,性能上和理解上都很自然易懂。

2-4、MongoDB 存储引擎:WiredTiger

wiredTiger支持snappy和zlib两种压缩模式。因此与MMAP相比,使用WiredTiger的MongoDB占用的磁盘空间要小很多。并且WiredTiger引擎本身有自己的写缓存(可配置)同时也能使用文件系统缓存。

WiredTiger和MMAPv1都用于持久化存储数据,相对而言,WiredTiger比MMAPv1更新,功能更强大。

1,文档级别的并发控制(Document-Level Concurrency Control)

MongoDB在执行写操作时,WiredTiger 在文档级别进行并发控制,就是说,在同一时间,多个写操作能够修改同一个集合中的不同文档;当多个写操作修改同一个文档时,必须以序列化方式执行;这意味着,如果该文档正在被修改,其他写操作必须等待,直到在该文档上的写操作完成之后,其他写操作相互竞争,获胜的写操作在该文档上执行修改操作。

对于大多数读写操作,WiredTiger使用乐观并发控制(optimistic concurrency control),只在Global,database和Collection级别上使用意向锁(Intent Lock),如果WiredTiger检测到两个操作发生冲突时,导致MongoDB将其中一个操作重新执行,这个过程是系统自动完成的。

2,检查点(Checkpoint)

在Checkpoint操作开始时,WiredTiger提供指定时间点(point-in-time)的数据库快照(Snapshot),该Snapshot呈现的是内存中数据的一致性视图。当向Disk写入数据时,WiredTiger将Snapshot中的所有数据以一致性方式写入到数据文件(Disk Files)中。一旦Checkpoint创建成功,WiredTiger保证数据文件和内存数据是一致性的,因此,Checkpoint担当的是还原点(Recovery Point),Checkpoint操作能够缩短MongoDB从Journal日志文件还原数据的时间。

当WiredTiger创建Checkpoint时,MongoDB将数据刷新到数据文件(Disk Files)中,在默认情况下,WiredTiger创建Checkpoint的时间间隔是60s,或产生2GB的Journal文件。在WiredTiger创建新的Checkpoint期间,上一个Checkpoint仍然是有效的,这意味着,即使MongoDB在创建新的Checkpoint期间遭遇到错误而异常终止运行,只要重启,MongoDB就能从上一个有效的Checkpoint开始还原数据。

当MongoDB以原子方式更新WiredTiger的元数据表,使其引用新的Checkpoint时,表明新的Checkpoint创建成功,MongoDB将老的Checkpoint占用的Disk空间释放。使用WiredTiger 存储引擎,如果没有记录数据更新的日志,MongoDB只能还原到上一个Checkpoint;如果要还原在上一个Checkpoint之后执行的修改操作,必须使用Jounal日志文件。

3,预先记录日志(Write-ahead Transaction Log)

WiredTiger使用预写日志的机制,在数据更新时,先将数据更新写入到日志文件,然后在创建Checkpoint操作开始时,将日志文件中记录的操作,刷新到数据文件,就是说,通过预写日志和Checkpoint,将数据更新持久化到数据文件中,实现数据的一致性。WiredTiger 日志文件会持久化记录从上一次Checkpoint操作之后发生的所有数据更新,在MongoDB系统崩溃时,通过日志文件能够还原从上次Checkpoint操作之后发生的数据更新。

3,内存使用

WiredTiger 利用系统内存资源缓存两部分数据:

内部缓存(Internal Cache)

文件系统缓存(Filesystem Cache)

从MongoDB 3.2 版本开始,WiredTiger内部缓存的使用量,默认值是:1GB 或 60% of RAM - 1GB,取两值中的较大值;文件系统缓存的使用量不固定,MongoDB自动使用系统空闲的内存,这些内存不被WiredTiger缓存和其他进程使用,数据在文件系统缓存中是压缩存储的。

4,数据压缩(Data Compression)

wiredTiger支持snappy和zlib两种压缩模式。WiredTiger压缩存储集合(Collection)和索引(Index),压缩减少Disk空间消耗,但是消耗额外的CPU执行数据压缩和解压缩的操作。

默认情况下,WiredTiger使用块压缩(Block Compression)算法来压缩Collections,使用前缀压缩(Prefix Compression)算法来压缩Indexes,Journal日志文件也是压缩存储的。对于大多数工作负载(Workload),默认的压缩设置能够均衡(Balance)数据存储的效率和处理数据的需求,即压缩和解压的处理速度是非常高的。

前缀压缩概念:先完全保存索引块中的第一个值,然后将其他值和第一个值进行比较得到相同前缀的字节数和剩余的不同后缀部分,把这部分存储起来即可。例如,索引块中的第一个值是“perform“,第二个值是”performance“,那么第二个值的前缀压缩后存储的是类似”7,ance“这样的形式。

5,Disk空间回收

当从MongoDB中删除文档(Documents)或集合(Collections)后,MongoDB不会将Disk空间释放给OS,MongoDB在数据文件(Data Files)中维护Empty Records的列表。当重新插入数据后,MongoDB从Empty Records列表中分配存储空间给新的Document,因此,不需要重新开辟空间。为了更新有效的重用Disk空间,必须重新整理数据碎片。

WiredTiger使用compact 命令,移除集合(Collection)中数据和索引的碎片,并将unused的空间释放,调用语法:

db.runCommand ( { compact: '<collection>' } )

在执行compact命令时,MongoDB会对当前的database加锁,阻塞其他操作。在compact命令执行完成之后,mongod会重建集合的所有索引。

2-5、GridFS

GridFS是MongoDB中存储和查询超过BSON文件大小限制(16M)的规范,不像BSON文件那样在一个单独的文档中存储文件,GridFS将文件分成多个块,每个块作为一个单独的文档。默认情况下,每个GridFS块是255kB,意味着除了最后一个块之外(根据剩余的文件大小),文档被分成多个255kB大小的块存储。

GridFS使用两个集合保存数据,一个集合存储文件块,另外一个存储文件元数据。当从GridFS中获取文件时,MongoDB的驱动程序负责将多个块组装成完整文件,你可以通过GridFS进行范围查询,可以访问文件的任意部分(例如跳到视频文件或者音频文件的任意位置)。

三、mongodb shell

使用docker搭建mongodb副本集

使用数据库

连接验证
$ docker exec -it 24ccf34f8d4b mongo --host mongohost --port 37017

use cbb

db.auth('root','wego2020')

创建集合
db.createCollection('note')

插入
rs:PRIMARY> db.note.insert({'title':'大秦帝国','author':DBRef('user',ObjectId('5dff0a3d26e2c74988808d81')),'time':ISODate("2019-12-30 11:05:01"),'tags':['历史','学习'],'content':'上将白起,长平之战...'})

WriteResult({ "nInserted" : 1 })

rs:PRIMARY> db.note.save({'_id':'001','title':'非暴力沟通','author':DBRef('user',ObjectId('5dff0a51a6e65352c48fec76')),'time':ISODate("2019-12-30 11:19:01"),'tags':['交流','学习'],'content':'讲事实、谈感受、提要求,不做结论性描述'})

WriteResult({ "nInserted" : 1 })

查询
查询所有
rs:PRIMARY> db.note.find()

{ "_id" : ObjectId("5e096c1ab990869fca386b01"), "title" : "大秦帝国", "author" : DBRef("user", ObjectId("5dff0a3d26e2c74988808d81")), "time" : ISODate("2019-12-30T11:05:01Z"), "tags" : [ "历史", "学习" ], "content" : "上将白起,长平之战..." }

{ "_id" : "001", "title" : "非暴力沟通", "author" : DBRef("user", ObjectId("5dff0a51a6e65352c48fec76")), "time" : ISODate("2019-12-30T11:19:01Z"), "tags" : [ "交流", "学习" ], "content" : "讲事实、谈感受、提要求,不做结论性描述" }

rs:PRIMARY> db.note.count()

条件查询
rs:PRIMARY> db.note.find({'title':'非暴力沟通'})

{ "_id" : "001", "title" : "非暴力沟通", "author" : DBRef("user", ObjectId("5dff0a51a6e65352c48fec76")), "time" : ISODate("2019-12-30T11:19:01Z"), "tags" : [ "交流", "学习" ], "content" : "讲事实、谈感受、提要求,不做结论性描述" }

rs:PRIMARY> db.note.find({tags:{$in:['历史']}})

{ "_id" : ObjectId("5e096c1ab990869fca386b01"), "title" : "大秦帝国", "author" : DBRef("user", ObjectId("5dff0a3d26e2c74988808d81")), "time" : ISODate("2019-12-30T11:05:01Z"), "tags" : [ "历史", "学习" ], "content" : "上将白起,长平之战..." }

rs:PRIMARY> db.note.find( {'author':DBRef("user", ObjectId("5dff0a51a6e65352c48fec76"))}, {content: 0 } )

{ "_id" : "001", "title" : "非暴力沟通", "author" : DBRef("user", ObjectId("5dff0a51a6e65352c48fec76")), "time" : ISODate("2019-12-30T11:19:01Z"), "tags" : [ "交流", "学习" ] }

查询关联对象
rs:PRIMARY> db.note.findOne({title:'大秦帝国'}).author.fetch()

{

​ "_id" : ObjectId("5dff0a3d26e2c74988808d81"),

​ "login" : "admin",

​ "passwordHash" : "$2a$10$G0hFgHIH5EEOeuZKoGp39u/ystyvxl2wHHr7p73ARQdITp8QNhxLW",

​ "firstName" : "admin",

​ "lastName" : "zz",

​ "email" : "yuji@11.com",

​ "imageUrl" : "string",

​ "activated" : true,

​ "langKey" : "cn",

​ "createdBy" : "web register",

​ "createdDate" : ISODate("2019-12-22T06:16:29.715Z"),

​ "_class" : "com.iflytek.cloudbaseserver.model.user.User",

​ "roles" : [

​ {

​ "_id" : 1,

​ "roleName" : "role_admin"

​ }

​ ]

}

查询部分字段
rs:PRIMARY> db.note.find({},{'title':1} )

{ "_id" : ObjectId("5e096c1ab990869fca386b01"), "title" : "大秦帝国" }

{ "_id" : "001", "title" : "非暴力沟通" }

rs:PRIMARY> db.note.find({},{'author':0,'time':0,'content':0} )

{ "_id" : ObjectId("5e096c1ab990869fca386b01"), "title" : "大秦帝国", "tags" : [ "历史", "学习" ] }

{ "_id" : "001", "title" : "非暴力沟通", "tags" : [ "交流", "学习" ] }

分页查询
rs:PRIMARY> db.note.find({}).skip(2).limit(2).sort(
)

{ "_id" : 3, "title" : "test3", "time" : ISODate("2019-12-30T11:14:44Z") }

{ "_id" : 2, "title" : "test2", "time" : ISODate("2019-12-30T11:14:40Z") }

模糊查询-正则匹配
rs:PRIMARY> db.note.find({'title':/^大秦/})

{ "_id" : ObjectId("5e096c1ab990869fca386b01"), "title" : "大秦帝国", "author" : DBRef("user", ObjectId("5dff0a3d26e2c74988808d81")), "time" : ISODate("2019-12-30T11:05:01Z"), "tags" : [ "历史", "学习" ], "content" : "上将白起,长平之战..." }

修改
rs:PRIMARY> db.note.update({_id:'001'},{$set:{title : "非暴力沟通-阅读"}})

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

rs:PRIMARY> db.note.find({_id:'001'})

{ "_id" : "001", "title" : "非暴力沟通-阅读", "author" : DBRef("user", ObjectId("5dff0a51a6e65352c48fec76")), "time" : ISODate("2019-12-30T11:19:01Z"), "tags" : [ "交流", "学习" ], "content" : "讲事实、谈感受、提要求,不做结论性描述" }

rs:PRIMARY> db.note.update({_id:'001'},{title : "非暴力沟通-阅读"})

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

rs:PRIMARY> db.note.find({_id:'001'})

{ "_id" : "001", "title" : "非暴力沟通-阅读" }

更新多个

db.note.update({title:/^test/},{$set:{tags:['测试']}},)

数组push
rs:PRIMARY> db.note.update({_id:'001'},{$push:{tags : "技巧"}})

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

rs:PRIMARY> db.note.find({_id:'001'})

{ "_id" : "001", "title" : "非暴力沟通", "author" : DBRef("user", ObjectId("5dff0a51a6e65352c48fec76")), "time" : ISODate("2019-12-30T11:19:01Z"), "tags" : [ "交流", "学习", "技巧" ], "content" : "讲事实、谈感受、提要求,不做结论性描述" }

数组pop
rs:PRIMARY> db.note.update({_id:'001'},{$pop:
})

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

rs:PRIMARY> db.note.find({_id:'001'})

{ "_id" : "001", "title" : "非暴力沟通", "author" : DBRef("user", ObjectId("5dff0a51a6e65352c48fec76")), "time" : ISODate("2019-12-30T11:19:01Z"), "tags" : [ "交流", "学习" ], "content" : "讲事实、谈感受、提要求,不做结论性描述" }

添加字段:
rs:PRIMARY> db.note.update({_id:'001'},{'$set':
})

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

rs:PRIMARY> db.note.find({_id:'001'})

{ "_id" : "001", "title" : "非暴力沟通", "author" : DBRef("user", ObjectId("5dff0a51a6e65352c48fec76")), "time" : ISODate("2019-12-30T11:19:01Z"), "tags" : [ "交流", "学习" ], "content" : "讲事实、谈感受、提要求,不做结论性描述", "readNo" : 1 }

删除字段
rs:PRIMARY> db.note.update({_id:'001'},{$unset:
})

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

rs:PRIMARY> db.note.find({_id:'001'})

{ "_id" : "001", "title" : "非暴力沟通", "author" : DBRef("user", ObjectId("5dff0a51a6e65352c48fec76")), "time" : ISODate("2019-12-30T11:19:01Z"), "tags" : [ "交流", "学习" ], "content" : "讲事实、谈感受、提要求,不做结论性描述" }

删除文档
rs:PRIMARY> db.note.remove({'title':'test'},1)

WriteResult({ "nRemoved" : 1 })

rs:PRIMARY> db.note.remove({'title':'test'})

WriteResult({ "nRemoved" : 2 })

创建索引
普通索引

db.note.createIndex({title:1,author:1})

唯一索引、后台创建

db.collection.ensureIndex(,, )

执行计划
rs:PRIMARY> db.note.find({"title" : "大秦帝国"}).explain( "executionStats" )

{

​ "queryPlanner" : {

​ "plannerVersion" : 1,

​ "namespace" : "cbb.note",

​ "indexFilterSet" : false,

​ "parsedQuery" : {

​ "title" : {

​ "$eq" : "大秦帝国"

​ }

​ },

​ "winningPlan" : {

​ "stage" : "FETCH",

​ "inputStage" : {

​ "stage" : "IXSCAN",

​ "keyPattern" : {

​ "title" : 1

​ },

​ "indexName" : "title_1",

​ "isMultiKey" : false,

​ "multiKeyPaths" : {

​ "title" : [ ]

​ },

​ "isUnique" : false,

​ "isSparse" : false,

​ "isPartial" : false,

​ "indexVersion" : 2,

​ "direction" : "forward",

​ "indexBounds" : {

​ "title" : [

​ "["大秦帝国", "大秦帝国"]"

​ ]

​ }

​ }

​ },

​ "rejectedPlans" : [ ]

​ },

​ "executionStats" : {

​ "executionSuccess" : true,

​ "nReturned" : 1,

​ "executionTimeMillis" : 0,

​ "totalKeysExamined" : 1,

​ "totalDocsExamined" : 1,

​ "executionStages" : {

​ "stage" : "FETCH",

​ "nReturned" : 1,

​ "executionTimeMillisEstimate" : 0,

​ "works" : 2,

​ "advanced" : 1,

​ "needTime" : 0,

​ "needYield" : 0,

​ "saveState" : 0,

​ "restoreState" : 0,

​ "isEOF" : 1,

​ "docsExamined" : 1,

​ "alreadyHasObj" : 0,

​ "inputStage" : {

​ "stage" : "IXSCAN",

​ "nReturned" : 1,

​ "executionTimeMillisEstimate" : 0,

​ "works" : 2,

​ "advanced" : 1,

​ "needTime" : 0,

​ "needYield" : 0,

​ "saveState" : 0,

​ "restoreState" : 0,

​ "isEOF" : 1,

​ "keyPattern" : {

​ "title" : 1

​ },

​ "indexName" : "title_1",

​ "isMultiKey" : false,

​ "multiKeyPaths" : {

​ "title" : [ ]

​ },

​ "isUnique" : false,

​ "isSparse" : false,

​ "isPartial" : false,

​ "indexVersion" : 2,

​ "direction" : "forward",

​ "indexBounds" : {

​ "title" : [

​ "["大秦帝国", "大秦帝国"]"

​ ]

​ },

​ "keysExamined" : 1,

​ "seeks" : 1,

​ "dupsTested" : 0,

​ "dupsDropped" : 0

​ }

​ }

​ },

​ "serverInfo" : {

​ "host" : "24ccf34f8d4b",

​ "port" : 27017,

​ "version" : "4.2.1",

​ "gitVersion" : "edf6d45851c0b9ee15548f0f847df141764a317e"

​ },

​ "ok" : 1,

​ "$clusterTime" : {

​ "clusterTime" : Timestamp(1577691688, 1),

​ "signature" : {

​ "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),

​ "keyId" : NumberLong(0)

​ }

​ },

​ "operationTime" : Timestamp(1577691688, 1)

}

executionStats.executionTimeMillis: query的整体查询时间。

executionStats.nReturned: 查询返回的条目。

executionStats.totalKeysExamined: 索引扫描条目。

executionStats.totalDocsExamined: 文档扫描条目。

理想状态:
nReturned=totalKeysExamined & totalDocsExamined=0

例如:

db.note.find({"title" : "大秦帝国"},{'_id':0,title:1}).sort().explain( "executionStats" )

rs:PRIMARY> db.note.find({'title':/^大秦/},{'_id':0,title:1}).sort().explain( "executionStats" )

如果索引包含了所有需要查询字段,则不用扫描文档

Stage状态分析
stage 描述
COLLSCAN 全表扫描
IXSCAN 扫描索引
FETCH 根据索引去检索指定document
SHARD_MERGE 将各个分片返回数据进行merge
SORT 表明在内存中进行了排序
LIMIT 使用limit限制返回数
SKIP 使用skip进行跳过
IDHACK 针对_id进行查询
SHARDING_FILTER 通过mongos对分片数据进行查询
COUNT 利用db.coll.explain().count()之类进行count运算
COUNTSCAN count不使用Index进行count时的stage返回
COUNT_SCAN count使用了Index进行count时的stage返回
SUBPLA 未使用到索引的$or查询的stage返回
TEXT 使用全文索引进行查询时候的stage返回
PROJECTION 限定返回字段时候stage的返回
关联查询
rs:PRIMARY> db.note.aggregate([{$lookup:{from:'user',localField:'author',foreignField:'login',as:'users'}},{​$project:{'users._id':0,'users.passwordHash':0,'users.roles':0,'users.appIds':0}},{$match:{"users.login":'yuji'}}]);

{ "_id" : 6, "title" : "test6", "tags" : [ "测试" ], "time" : ISODate("2019-12-30T13:14:44Z"), "author" : "yuji", "users" : [ { "login" : "yuji", "firstName" : "yuji", "lastName" : "zz", "email" : "yuji@11.com", "imageUrl" : "string", "activated" : true, "langKey" : "cn", "createdBy" : "web register", "createdDate" : ISODate("2019-12-22T06:16:49.535Z"), "_class" : "com.iflytek.cloudbaseserver.model.user.User" } ] }

聚合指令map reduce
查询每个标签多少篇文章

rs:PRIMARY> map =function(){ this.tags.forEach(function(item) { emit(item, 1) }) }

function(){ this.tags.forEach(function(item) { emit(item, 1) }) }

rs:PRIMARY> reduce = function(key,values){ return Array.sum(values) }

function(key,values){ return Array.sum(values) }

等义

reduce = function(key,values){

var sum =0
values.forEach(function(value){
    sum+=value
})
return sum
}

rs:PRIMARY> db.note.mapReduce(map, reduce, {query:{}, out: 'tagcount'})

{

​ "result" : "tagcount",

​ "timeMillis" : 72,

​ "counts" : {

​ "input" : 7,

​ "emit" : 9,

​ "reduce" : 2,

​ "output" : 4

​ },

​ "ok" : 1,

​ "$clusterTime" : {

​ "clusterTime" : Timestamp(1577695637, 6),

​ "signature" : {

​ "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),

​ "keyId" : NumberLong(0)

​ }

​ },

​ "operationTime" : Timestamp(1577695637, 6)

}

rs:PRIMARY> db.tagcount.find()

{ "_id" : "交流", "value" : 1 }

{ "_id" : "历史", "value" : 1 }

{ "_id" : "学习", "value" : 2 }

{ "_id" : "测试", "value" : 5 }

四、数据库设计

4-1、Bson

​ BSON是一种类json的一种二进制形式的存储格式,简称Binary JSON,它和JSON一样,支持内嵌的文档对象和数组对象

1.更快的遍历速度

  对json格式来说,太大的json结构会导致数据遍历非常慢。在json中,要跳过一个文档进行数据读取,需要对此文档进行扫描才行,需要进行麻烦的数据结构匹配,比如括号的匹配。
  而bson对json的一大改进就是,它会将json的每一个元素的长度存在元素的头部,这样你只需要读取到元素长度就能直接seek到指定的点上进行读取了。

2.操作更简易

  对json来说,数据存储是无类型的,比如你要修改基本一个值,从9到10,由于从一个字符变成了两个,所以可能其后面的所有内容都需要往后移一位才可以。
  而使用bson,你可以指定这个列为数字列,那么无论数字从9长到10还是100,我们都只是在存储数字的那一位上进行修改,不会导致数据总长变大。
  当然,在mongoDB中,如果数字从整形增大到长整型,还是会导致数据总长变大的。

3.增加了额外的数据类型

​ json是一个很方便的数据交换格式,但是其类型比较有限。 mongoDB相对json可以支持更多的数据类型,并且将其作为存储结构。Double、String、Array、Binary data(二进制数据)、Undefined、objectId、regex等

  bson在其基础上增加了“byte array”数据类型。这使得二进制的存储不再需要先base64转换后再存成json,大大减少了计算开销和数据大小。
  当然,在有的时候,bson相对json来说也并没有空间上的优势,比如对{“field”:7},在json的存储上7只使用了一个字节,而如果用bson,那就是至少4个字节(32位)

4-2、文档模式设计

如何考虑MongoDB 文档模式设计的基本策略呢?

一般建议的是先考虑内嵌, 直接按照你的对象模型来设计你的数据模型。如果你的对象模型数量不多,关系不是很复杂,那么恭喜你,可能直接一种对象对应一个集合就可以了。

内嵌是文档模型的特色,可以充分利用MongoDB的富文档功能来享受我们刚才谈到的一些文档模型的性能和扩展性等特性。一般的一对一、一对多关系,比如说一个人多个地址多个电话等等都可以放在一个文档里用内嵌来完成。

但是有一些时候,使用引用则难以避免。比如说, 一个明星的博客可能有几十万或者几百万的回复,这个时候如果把comments放到一个数组里,可能会超出16M的限制。这个时候你可以考虑使用引用的方式,在主表里存储一个id值,指向另一个表中的 id 值。使用引用要注意的就是:从性能上讲,一般我们可能需要两次以上才能把需要的数据取回来。

image.png
4-3、MongoDB的模式设计思路:
为应用程序服务,而不是为了存储优化

为实现最佳性能而设计

设计案例参考

结论:
在确定理想的数据模型前,必须问无数个与应用程序有关的问题。读写比是多少?需要何种查询?数据是如何更新的?能想到什么并发问题?数据的结构化程度如何?

最好的Schema设计总是源于对正在使用的数据库的深入理解、对应用程序需求的准确判断以及过去的经验。

4-4、索引类型:

MongoDB支持多种类型的索引,包括单字段索引、复合索引、多key索引、文本索引等,每种类型的索引有不同的使用场合。

1、单键索引 (Single Field Index)

db.person.createIndex({age: 1})

上述语句针对age字段创建了单字段索引,其能加速对age字段的各种查询请求,是最常见的索引形式,MongoDB默认创建的id索引也是这种类型。

{age: 1}代表升序索引,也可以通过{age: -1}来指定降序索引,对于单字段索引,升序/降序效果是一样的。

2、复合索引(Compound Index)

复合索引针对多个字段联合创建索引,先按第一个字段排序,第一个字段相同的文档按第二个字段排序,依次类推。

db.person.createIndex({age: 1, name: 1})

复合索引能满足的查询场景比单字段索引更丰富,不光能满足多个字段组合起来的查询,比如db.person.find({age: 18, name: "jack"}),也能满足所以能匹配符合索引前缀的查询,这里{age: 1}即为{age: 1, name: 1}的前缀,所以类似db.person.find( {age: 18} )的查询也能通过该索引来加速;但db.person.find({name: "jack"} )则无法使用该复合索引。如果经常需要根据『name字段』以及『name和age字段组合』来查询,则应该创建如下的复合索引

db.person.createIndex({name: 1, age: 1})

3、多key索引(Multikey Index)

当索引的字段为数组时,创建出的索引称为多key索引,多key索引会为数组的每个元素建立一条索引。

{"name": "jack", "age": 19, habbit: ["football, runnning"]}

db.person.createIndex({habbit: 1}) // 自动创建多key索引

db.person.find({habbit: "football"})

4、其他类型索引

哈希索引(Hashed Index)是指按照某个字段的hash值来建立索引,目前主要用于MongoDB Sharded Cluster的Hash分片,hash索引只能满足字段完全匹配的查询,不能满足范围查询。

地理位置索引(Geospatial Index)能很好的解决O2O的应用场景,比如『查找附近的美食』、『查找某个区域内的车站』等。

文本索引(Text Index)能解决快速文本查找的需求,比如有一个博客文章集合,需要根据博客的内容来快速查找,则可以针对博客内容建立文本索引。

5、索引额外属性

MongoDB除了支持多种不同类型的索引,还能对索引定制一些特殊的属性。

1、唯一索引 (unique index):保证索引对应的字段不会出现相同的值,比如_id索引就是唯一索引。

2、TTL索引:可以针对某个时间字段,指定文档的过期时间(经过指定时间后过期或在某个时间点过期)。

3、部分索引 (partial index):只针对符合某个特定条件的文档建立索引,3.2版本才支持该特性。

4、稀疏索引 (sparse index):只针对存在索引字段的文档建立索引,可看做是部分索引的一种特殊情况。

4-5、索引构建:

后台索引:
如果是在生产环境里,经不住这样暂停数据库访问的情况,可以指定在后台构建索引。虽然索引构建仍会占用写锁,但构建任务会停下来允许其他读写操作访问数据库。如果应用程序大量使用MongoDB,后台索引会降低性能,但在某些情况下这是可接受的。
要在后台构建索引,声明索引时需要指定

db.values.ensureIndex({open: 1, close: 1},
)

离线索引:
如果生产数据集太大,无法在几小时内完成索引,这时就需要其他方案了。通常这会涉及让一个副本节点下线,在该节点上构建索引,随后让其上的数据与主节点同步。一旦完成数据同步,将该节点提升为主节点,再让另一个从节点下线,构建它自己 的索引。

备份
mongodump 和mongorestore这些工具仅保存了集合和索引声明。也就是说,当运行mongorestore 时,所备份的所有集合上声明的索引都会被重新创建一遍。如果想要在备份中包含索引,需要直接备份MongoDB的数据文件。

压紧
如果应用程序会大量更新现有数据,或者执行很多大规模删除,其结果就是索引的碎片化程度很高。虽说B树会自己合并,但这并非总能抵消大量删除的影响。碎片过多的索引大小远超你对指定数据集大小的预期,也会让索引使用更多内存。这些情况下,你可能希望重建一个或多个索引:可以删除并重新创建单个索引,或者运行reIndex 命令(它会重建指定集合上的所有索引):
db.values.reIndex();

4-6、查询与优化

识别慢查询

分析慢查询explain

MongoDB的查询优化器与hint(),hint() 能强迫查询优化器使用某个特定索引

五、复制

由一个主节点、一个从节点和一个仲裁节点组成的基本副本集
image.png

5-1、MongoDB复制集简介

  一组Mongodb复制集,就是一组mongod进程,这些进程维护同一个数据集合。复制集提供了数据冗余和高等级的可靠性,这是生产部署的基础。

5-2、复制集的目的

  保证数据在生产部署时的冗余和可靠性,通过在不同的机器上保存副本来保证数据的不会因为单点损坏而丢失。能够随时应对数据丢失、机器损坏带来的风险。

  换一句话来说,还能提高读取能力,用户的读取服务器和写入服务器在不同的地方,而且,由不同的服务器为不同的用户提供服务,提高整个系统的负载。

5-3、简单介绍

  一组复制集就是一组mongod实例掌管同一个数据集,实例可以在不同的机器上面。实例中包含一个主导,接受客户端所有的写入操作,其他都是副本实例,从主服务器上获得数据并保持同步。

  主服务器很重要,包含了所有的改变操作(写)的日志。但是副本服务器集群包含有所有的主服务器数据,因此当主服务器挂掉了,就会在副本服务器上重新选取一个成为主服务器。

  每个复制集还有一个仲裁者,仲裁者不存储数据,只是负责通过心跳包来确认集群中集合的数量,并在主服务器选举的时候作为仲裁决定结果。
image.png

5-4、选举机制

复制集通过replSetInitiate命令(或mongo shell的rs.initiate())进行初始化,初始化后各个成员间开始发送心跳消息,并发起Priamry选举操作,获得大多数成员(投票成员n个,n/2+1为大多数)投票支持的节点,会成为Primary,其余节点成为Secondary。

5-5、复制原理

主节点记录在其上的所有操作oplog,从节点定期轮询主节点获取这些操作,然后对自己的数据副本执行这些操作,从而保证从节点的数据与主节点一致。和mysql binlog类似。

注意事项:主从复制的问题,从节点会有数据同步延迟。可通过配置降低延迟,需要在mongo服务性能和延迟之间做均衡。

六、分片

分片(sharding)是MongoDB用来将大型集合分割到不同服务器(或者说一个集群)上所采用的方法。尽管分片起源于关系型数据库分区,但MongoDB分片完全又是另一回事。和MySQL分区方案相比,MongoDB的最大区别在于它几乎能自动完成所有事情,只要告诉MongoDB要分配数据,它就能自动维护数据在不同服务器之间的均衡。

6-1、分片的目的

  高数据量和吞吐量的数据库应用会对单机的性能造成较大压力,大的查询量会将单机的CPU耗尽,大的数据量对单机的存储压力较大,最终会耗尽系统的内存而将压力转移到磁盘IO上。

  为了解决这些问题,有两个基本的方法: 垂直扩展和水平扩展。

    垂直扩展:增加更多的CPU和存储资源来扩展容量。

    水平扩展:将数据集分布在多个服务器上。水平扩展即分片。

分片为应对高吞吐量与大数据量提供了方法。使用分片减少了每个分片需要处理的请求数,因此,通过水平扩展,集群可以提高自己的存储容量和吞吐量。举例来说,当插入一条数据时,应用只需要访问存储这条数据的分片.

  使用分片减少了每个分片存储的数据。

6-2、分片机制提供了如下三种优势

1.对集群进行抽象,让集群“不可见”

  MongoDB自带了一个叫做mongos的专有路由进程。mongos就是掌握统一路口的路由器,其会将客户端发来的请求准确无误的路由到集群中的一个或者一组服务器上,同时会把接收到的响应拼装起来发回到客户端。

2.保证集群总是可读写

  MongoDB通过多种途径来确保集群的可用性和可靠性。将MongoDB的分片和复制功能结合使用,在确保数据分片到多台服务器的同时,也确保了每分数据都有相应的备份,这样就可以确保有服务器换掉时,其他的从库可以立即接替坏掉的部分继续工作。

3.使集群易于扩展

  当系统需要更多的空间和资源的时候,MongoDB使我们可以按需方便的扩充系统容量。

6-3、分片集群架构

组件 说明
Config Server 存储集群所有节点、分片数据路由信息。默认需要配置3个Config Server节点。
Mongos 提供对外应用访问,所有操作均通过mongos执行。一般有多个mongos节点。数据迁移和数据自动平衡。
Mongod 存储应用数据记录。一般有多个Mongod节点,达到数据分片目的。
image.png

分片集群的构造

(1)mongos :数据路由,和客户端打交道的模块。mongos本身没有任何数据,他也不知道该怎么处理这数据,去找config server

(2)config server:所有存、取数据的方式,所有shard节点的信息,分片功能的一些配置信息。可以理解为真实数据的元数据。

(3)shard:真正的数据存储位置,以chunk为单位存数据。

 Mongos本身并不持久化数据,Sharded cluster所有的元数据都会存储到Config Server,而用户的数据会议分散存储到各个shard。Mongos启动后,会从配置服务器加载元数据,开始提供服务,将用户的请求正确路由到对应的碎片。
Mongos的路由功能
  当数据写入时,MongoDB Cluster根据分片键设计写入数据。

  当外部语句发起数据查询时,MongoDB根据数据分布自动路由至指定节点返回数据。

6-4、数据区分

分片键shard key
  MongoDB中数据的分片是、以集合为基本单位的,集合中的数据通过片键(Shard key)被分成多部分。其实片键就是在集合中选一个键,用该键的值作为数据拆分的依据。

  所以一个好的片键对分片至关重要。片键必须是一个索引,通过sh.shardCollection加会自动创建索引(前提是此集合不存在的情况下)。一个自增的片键对写入和数据均匀分布就不是很好,因为自增的片键总会在一个分片上写入,后续达到某个阀值可能会写到别的分片。但是按照片键查询会非常高效。

  随机片键对数据的均匀分布效果很好。注意尽量避免在多个分片上进行查询。在所有分片上查询,mongos会对结果进行归并排序。

  对集合进行分片时,你需要选择一个片键,片键是每条记录都必须包含的,且建立了索引的单个字段或复合字段,MongoDB按照片键将数据划分到不同的数据块中,并将数据块均衡地分布到所有分片中。

  为了按照片键划分数据块,MongoDB使用基于范围的分片方式或者 基于哈希的分片方式。

注意:

分片键是不可变。

分片键必须有索引。

分片键大小限制512bytes。

分片键用于路由查询。

MongoDB不接受已进行collection级分片的collection上插入无分片

键的文档(也不支持空值插入)

以范围为基础的分片Sharded Cluster

  Sharded Cluster支持将单个集合的数据分散存储在多shard上,用户可以指定根据集合内文档的某个字段即shard key来进行范围分片(range sharding)。

image.png

对于基于范围的分片,MongoDB按照片键的范围把数据分成不同部分。

  假设有一个数字的片键:想象一个从负无穷到正无穷的直线,每一个片键的值都在直线上画了一个点。MongoDB把这条直线划分为更短的不重叠的片段,并称之为数据块,每个数据块包含了片键在一定范围内的数据。在使用片键做范围划分的系统中,拥有”相近”片键的文档很可能存储在同一个数据块中,因此也会存储在同一个分片中。

基于哈希的分片

  分片过程中利用哈希索引作为分片的单个键,且哈希分片的片键只能使用一个字段,而基于哈希片键最大的好处就是保证数据在各个节点分布基本均匀。
image.png
 对于基于哈希的分片,MongoDB计算一个字段的哈希值,并用这个哈希值来创建数据块。在使用基于哈希分片的系统中,拥有”相近”片键的文档很可能不会存储在同一个数据块中,因此数据的分离性更好一些。

  Hash分片与范围分片互补,能将文档随机的分散到各个chunk,充分的扩展写能力,弥补了范围分片的不足,但不能高效的服务范围查询,所有的范围查询要分发到后端所有的Shard才能找出满足条件的文档。

分片键选择建议

  • 递增的sharding key

数据文件挪动小。(优势)

因为数据文件递增,所以会把insert的写IO永久放在最后一片上,造成最后一片的写热点。同时,随着最后一片的数据量增大,将不断的发生迁移至之前的片上。

  • 随机的sharding key

数据分布均匀,insert的写IO均匀分布在多个片上。(优势)

大量的随机IO,磁盘不堪重荷。

  • 混合型key

大方向随机递增,小范围随机分布。

为了防止出现大量的chunk均衡迁移,可能造成的IO压力。我们需要设置合理分片使用策略(片键的选择、分片算法(range、hash))

分片注意:

分片键是不可变、分片键必须有索引、分片键大小限制512bytes、分片键用于路由查询。

MongoDB不接受已进行collection级分片的collection上插入无分片键的文档(也不支持空值插入)

6-5、分片集群方案

image.png

由路由器、配置服务器、副本集组成的分片
image.png
部署在四台机器上的两分片集群

七、框架集成

在springboot中添加 spring-boot-starter-data-mongodb 依赖即可

CURD测试

继承MongoRepository,使用相关方法

mongoTemplate

事物支持

单个 mongodb服务不支持事务,必须副本集。如果仅仅部署一个mongodb服务,可以创建一个单节点的副本集以支持事务。因为事务使用了复制集写库记录的一部分功能。

使用一主一从一仲裁节点,测试事务成功

使用默认主键还是自定义自增主键

插入数据时如果没有指定_id,mongoDB的驱动包中是会自动添加ObjectId的。如果指定手动生成主键值需要维护唯一性,一般可以通过创建函数实现自增。

db.counters.insert({_id:"productid",sequence_value:0})
function getNextSequenceValue(sequenceName){
   var sequenceDocument = db.counters.findAndModify(
      {
         query:{_id: sequenceName },
         update: {$inc:{sequence_value:1}},
         "new":true
      });
   return sequenceDocument.sequence_value;
}
db.products.insert({
   "_id":getNextSequenceValue("productid"),
   "product_name":"Samsung S3",
   "category":"mobiles"})

ObjectId生成规则:bjectID使用12字节的存储空间,是一个由24个16进制数字组成的字符串。

0|1|2|3|4|5|6|7|8|9|10|11
时间戳 |机器 |PID |计数器

八、针对应用现有问题

不方便扩展磁盘

不方便添加业务字段

查询性能存在问题

磁盘冗余度高

问题 mysql mongodb
不方便扩展磁盘 分库分表,路由需要自己设计 添加分片,自动分片,自动迁移均衡磁盘数据
不方便添加业务字段 改动数据库表结构和业务代码 bean添加字段,更改字段名称,db.note.update({$rename:{url:'site'}});需要时间执行
查询性能存在问题 索引优化、数据结构优化,执行计划、慢日志排查问题 不适合做大量join操作。索引优化、数据结构优化,执行计划、慢日志排查问题。分片集群可水平拓展,解决单点数据库服务压力。充分使用内存缓存热点数据。(对比:列出mysql中高频查询sql,将mysql表中数据导入mongodb进行查询测试,得到性能对比)
磁盘冗余度高(占用磁盘空间) 2w条订单数据2576.00K 2w条订单数据1168k(块压缩、前缀压缩)
总结:

如果选用mysql:

需要做分库分表设计,路由需要自己设计,例如查询某些数据需要映射到某些库表。

分库分表需要处理分布式事务,需要处理跨库表查询。

mysql5.7支持了json类型,由于功能支持限制,不建议在关系型数据库中使用json格式。

join查询操作方便。

团队成员熟悉。

如果选用mongodb:

文档型数据结构比较灵活,轻松拓展业务功能。

提供mongos路由功能,自动分片,自动迁移数据,剔除故障。容易添加分片,磁盘不会成为数据存储的瓶颈。

需要设计好文档数据模型,适当冗余字段以满足复杂查询。

最新版本才有分布式事务支持。mongodb4.2 (2019 年 6 月 20 日)

本文内容、数据来源:

个人测试、使用总结

《Mongodb实战》

官方文档、论坛

相关博客、论坛

billyu
2020.1.1