1.1 定义
MongoDB 是由 C++语言编写的, 是一个基于分布式文件存储的开源数据库系统。 在高负载的情况下, 添加更多的节点, 可以保证服务器性能。 MongoDB 旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。 MongoDB 将数据存储为一个文档, 数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。 字段值可以包含其他文档, 数组及文档数组。
MongoDB 是一个跨平台的,面向文档的数据库,是当前 NoSQL 数据库产品中最热门的一种。
1.2 特点
NoSQL是Not Only SQL的缩写。它指的是非关系型的数据库,是以key-value形式存储,NoSQL和传统的关系型数据库不一样。
先与关系型数据库做个比较:
1)关系型数据库的特点:扩展困难、读写慢、成本高、有限的支撑容量。
2)NoSQL:对数据高并发读写和海量数据的存储,在架构和模型上做了减法,而在扩展和并发方面做了加法。
优点
缺点
1)关系型数据库:MySQL数据库(database),表(table),记录(rows)三个层次概念组成。
2)非关系性感数据库:MongoDB数据库(database),集合(collection),文档对象(document)三个层次概念组成。
MongoDB里的集合对应关系型数据库里的表,但是集合中没有列、行和关系的概念。集合中只有文档,一个文档相当于一条记录。
1)MySQL数据结构存储
MySQL的每个数据库存放在一个与数据库同名的文件夹中。
MyISAM存储引擎,数据库文件类型包括:.frm、.MYD、.MYI
InnoDB存储引擎,数据库文件类型包括:.frm、.ibd
文件位置:/var/lib/mysql
2)MongoDB数据结构存储
MongoDB默认数据目录/data/db,负责存储所有的MongoDB数据文件。
MongoDB网址:https://www.mongodb.com/download-center#community
1)下载
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.2.6.tgz
2)解压
tar -zxvf mongodb-linux-x86_64-ubuntu1604-4.2.6.tgz
3)将压缩包拷贝到指定目录
mv mongodb-linux-x86_64-ubuntu1604-4.2.6 /usr/local/mongodb
创建数据库文件(默认的数据库文件的位置是/data/db,启动时会自动创建)。
提示:MongoDB没有具体的安装过程,解压文件包后,可以直接使用,非常高效和方便。
MongoDB安装后,包含服务端Mongod和客户端Mongo,默认情况下服务端是没有配置文件的,这样可能会导致非本机IP连接访问的时候无法连接,因此需要增加一个配置文件让服务端可以接收任意IP的连接访问。
1)创建文件夹或文件
sudo mkdir -p /data/db //创建数据库目录
sudo mkdir -p /usr/local/mongodb/logs //创建logs目录
sudo touch /usr/local/mongodb/logs/mongodb.log //创建空文件
2)在MongoDB安装位置/bin目录 sudo vi mongodb.conf
#数据文件存放目录
dbpath=/data/db
#日志文件存放目录
logpath=/usr/local/mongodb/logs/mongodb.log
#以守护程序的方式启动(在后台运行)
fork=true
#远程连接
bind_ip=0.0.0.0
1)启动服务
方式1:
sudo /usr/local/mongodb/ mongodb-linux-x86_64-ubuntu1604-4.2.6/bin/mongod
方式2(后台启动):
sudo /usr/local/mongodb/ mongodb-linux-x86_64-ubuntu1604-4.2.6/bin/mongod -config /usr/local/mongodb/mongodb-linux-x86_64-ubuntu1604-4.2.6/bin/ mongodb.conf
2)启动客户端
运行MongoDB命令:sudo/usr/local/mongodb/mongodb-linux-x86_64-ubuntu1604-4.2.6/bin/mongo
3)启动常用选项说明
- dbpath 指定数据库的目录
- port 指定数据库的端口,默认是27017
- bind_ip 绑定IP
- directoryperdb 为每个db创建一个独立子目录
- logpath 指定日志存放目录
- logappend 指定日志生成方式(追加/覆盖)
- pidfilepath 指定进程文件路径,如果不指定,将不产生进程文件
- keyFile 集群模式的关键标识
- journal 启用日志
- nssize 指定.ns文件的大小,单位MB,默认是16M,最大是2GB
- maxConns 最大的并发连接数
- notablescan 不允许进行表扫描
- noprealloc 关闭数据文件的预分配功能
- fork 以后台 Daemon形式运行服务
更多资源选项使用 mongo -help
进行查看。
1)查看MongDB运行信息
$ps aux|grep mongod
2)注意事项
不要使用 kill -9 PID 杀死MongoDB进程,这样可能会导致MongoDB的数据损坏。可以用kill -2 PID杀死进程。
知识点补充:kill的常用信号包括,
HUP(1) 终端断线
INT(2) 中断(Ctrl+C)
QUIT(3) 退出(Ctrl+\)
TERM(15) 终止
KILL(9) 强制终止
CONT(18) 继续(与STOP相反,fg/bg命令)
STOP(19) 暂停(Ctrl+Z)
只有第9种信号可以无条件终止进程,其他信号都有权利忽略。
第2种信号时程序在结束之前,能够保存相关数据,然后再退出。
use db //创建或切换到db数据库
db.version() //查看数据库版本
db.dropDatabase(); //删除数据库 (db的所有操作都是对当前数据库)
db.createCollection(“teacher”); //创建teacher集合
show collections; //显示所有集合
db.teacher.drop(); //删除集合
db.teacher.inert({name: “abc”, sex:1}); //集合添加数据
db.teacher.inert({name: “efg”, sex:1, age:18}); //插入数据到集合
db.teacher.find(); //查询集合数据
1)插入数据
db.teacher.inert({name: “efg”, sex:1, age:18});
2)修改数据
db.teacher.update({name:“efg”}, {$set:{age:30}});
3)删除数据
db.teacher.remove({name:“efg”}); //删除指定数据{name:”efg”}相当于SQL中的WHERE条件语句
4)条件查找-AND条件
格式:db.col.find({key1:value1, key2:value2}).pretty();
6)$ type操作符
db.teacher.find({name:{$ type:2}}) 等价于 db.teacher.find({name:{$type:“string”}})
$type操作符是基于BSON类型来检索集合中匹配的数据类型,并返回结果。
MongoDB 中可以使用的类型如下表所示:
7)limit
limit接受一个数字参数,表示要读取的记录条数。
语法:db.COLLECTION_NAME.find().limit(NUMBER)
如果设置的读取NUMBER数量超出了集合的数据总数,则返回集合的所有数据。
8)skip
使用skip()方法来跳过指定数量的数据,skip方法同样接受一个数字参数作为跳过的记录条数。
语法:db.COLLECTION_NAME.find().limit(NUMBER).skip(NUMBER)
9)sort方法
db.COLLECTION_NAME.find().sort({“key”:NUM})
NUM:1 升序,-1 降序。
10)aggregate方法
用于处理数据(诸如统计平均值,求和等),并返回计算结果。有点类似SQL语句中的count(*)。
语法:db. COLLECTION_NAME.aggregate(AGGREGATE_OPERATION)
一些聚合的表达式
1)MySQL字段事先定义好,大小固定。
2)MongoDB不需要事先定义号,大小不固定。
参考:https://mongoing.com/archives/2797
当你抱怨MongoDB集合查询效率低的时候,可能就需要考虑使用索引了。MongoDB索引机制:
当往某各个集合插入多个文档后,每个文档在经过底层的存储引擎持久化后,会有一个位置信息,通过这个位置信息就能从存储引擎里读出该文档。
比如:mmapv1引擎里,位置信息是[文件id+文件内offset],在writetiger存储引擎(一个KV存储引擎里),位置是writetiger在存储文档时生成的一个key,通过这个key能访问到对应的文档;为了方便介绍,同一用pos(position的缩写)来代表位置信息。
比如上面的例子,person集合里面包含插入了4个文档,假设其存储后位置信息如下(为方便描述,文档省去_id字段)
假设现在有个查询db.person.find({age:18}),查询所有年龄为18岁的人,这时需要遍历所有的文档(『全表扫描』),根据位置信息读出文档,对比age字段是否为18。当然如果只有4个文档,全表扫描的开销并不大,但如果集合文档数量到百万、甚至千万上亿的时候,对集合进行全表扫描开销是非常大的,一个查询耗费数十秒甚至几分钟都有可能。
如果想加速db.person.find({age:18}),就可以考虑对person表的age字段建立索引。
db.person.createIndex( {age: 1} ) // 按age字段创建升序索引
建立索引后,MongoDB会额外存储一份按age字段升序排序的索引数据,索引结构类似如下,索引通常采用类似btree的结构持久存储,以保证从索引里快速(O(logN)的时间复杂度)找出某个age值对应的位置信息,然后根据位置信息就能读取出对应的文档。
简单的说,索引就是将文档按照某个(或某些)字段顺序组织起来,以便能够根据该字段高效的查询。
有了索引,至少能够优化如下场景的效率:
MongoDB默认会为插入的文档生成 _id 字段(如果应用本身没有指定该字段),** _id是文档唯一的标识**,为了保证能够根据文档id快速查询文档,MongoDB默认会为集合创建_id字段的索引。
MongoDB支持多种类型的索引,包括单字段索引、复合索引、多key索引、文本索引等,每种类型的索引有不同的使用场合。
1)单字段索引(Single Field Index)
db.person.createIndex( {age: 1} )
上述语句针对age创建了单字段索引,其能加速对age字段的各种查询请求,是最常见的索引形式,MongoDB默认创建的id索引也是这种类型。
{age: 1} 代表升序索引,也可以通过{age: -1}来指定降序索引,对于单字段索引,升序/降序效果是一样的。
2) 复合索引(Compound Index)
复合索引是Single Field Index的升级版本,它针对多个字段联合创建索引,先按第一个字段排序,第一个字段相同的文档按第二个字段排序,依次类推,如下针对age,name这2个字段创建一个符合索引。
db.person.createIndex({age:1, name:1});
上述索引对应的数据组织类似下表,与{age:1}索引不同的是,当age字段相同时,再根据name字段进行排序,所以pos5对应的文档排在pos3之前。
复合索引能满足的查询场景比单字段索引更丰富,不但能满足多个字段组合起来的查询,比如:db.person.find( {age:18, name: “jack”} ) ,也能满足所有能匹配符合索引前缀的查询,这里{age:1}即为{age:1, name:”jack”}的前缀,所以类似db.person.find( {age:18} ),也能通过索引来加速;但db.person.find( {name:”jack”} ) 则无法使用该符合索引。如果经常需要根据 [name字段] 以及 [name和age字段组合]来查询,则应该创建如下的复合索引。
db.person.createIndex( {name: 1, age: 1} )
除了查询的需求能够影响索引的顺序,字段的值分布也是一个重要的考量因素,即使person集合所有的查询都是『name和age字段组合』(指定特定的name和age),字段的顺序也是有影响的。
age字段的取值很有限,即拥有相同age字段的文档会有很多;而name字段的取值则丰富很多,拥有相同name字段的文档很少;显然先按name字段查找,再在相同name的文档里查找age字段更为高效。
3) 多key索引(Multikey Index)
当索引的字段为数组时,创建出的索引称为多key索引,多key索引会为数组的每个元素建立一条索引,比如person表加入一个habbit字段(数组)用于描述兴趣爱好,需要查询有相同兴趣爱好的人就可以利用habbit字段的多key索引。
{“name” : “jack”, “age” : 19, habbit: [“football, runnning”]}
db.person.createIndex( {habbit: 1} ) // 自动创建多key索引
db.person.find( {habbit: “football”} )
4)其他类型索引
5)索引其他额外属性
MongoDB除了支持多种不同类型的索引,还能对索引定制一些特殊的属性
db profiling
MongoDb支持对DB的请求进行profiling,目前支持三种级别的profiling。
通常,生产环境建议使用1级别的profiling,并根据自身需求配置合理的阈值,用于监测慢请求的情况,并及时的做索引优化。
如果能在集合创建的时候就能『根据业务查询需求决定应该创建哪些索引』,当然是最佳的选择;但由于业务需求多变,要根据实际情况不断的进行优化。索引并不是越多越好,集合的索引太多,会影响写入、更新的性能,每次写入都需要更新所有索引的数据;所以你system.profile里的慢请求可能是索引建立的不够导致,也可能是索引过多导致。
查询计划
索引已经建立了,但查询还是很慢怎么破?这时就得深入的分析下索引的使用情况了,可通过查看下详细的查询计划来决定如何优化。通过执行计划可以看出如下问题。
1)根据某个/些字段查询,但没有建立索引。
2)根据某个/些字段查询,但建立了多个索引,执行查询时没有使用预期的索引。
建立索引前,db.person.find({age:18})必须执行COLLSCAN(全盘扫描)。
建立索引后,通过查询计划可以看出,先进行[IXSCAN]从索引中查找,然后FETCH,读取出满足条件的文档。
1)创建索引
语法:db.collection.createIndex(keys, options)
Key值为要创建的索引字段;options:1 指定按升序创建索引,-1按降序创建。
2)查看集合索引db.col.getIndexes()
3)查看集合索引大小db.col.totalIndexSize()
4)删除集合所有索引db.col.dropIndexes()
5)删除集合指定索引 db.col.dropIndex("索引名称")
1)条件中用or,即使其中有条件带索引,也不会使用索引查询(这就是查询尽量不要用or的原因,用in吧)
注意:使用or,又想索引生效,只能将or条件中的每个列都加上索引。
2)对于多列索引,不是使用的第一部分,则不会使用索引。类似电话薄。
3)like的模糊查询,以%开头,索引失效。
4)存在索引列的数据类型隐形转换,则用不上索引,比如列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不会使用索引。
5)where子句里有对索引列上有数学运算,用不上索引。
7)如果mysql估计使用全表扫描要比使用索引快,则不使用索引。比如数据量极少的表。
1)唯一性差的字段不要使用索引
比如性别,只有两种可能。意味着索引的二叉树级别少,多是平级。这样的二叉树无异于全盘扫描。
2)频繁更新的字段(更新索引消耗)
比如login count登录次数,频繁变化导致索引也频繁变化,增大数据库工作量,降低效率。
3)字段不在where条件中
字段不在where语句中出现时,不要添加索引,如果where后含IS NULL / IS NOT NULL / like ‘%输入符%’ 等条件,不建议使用索引。
只有在where语句出现,mysql才会去使用索引。
4)索引使用 < > 时,效果一般。
可以通过explain来查看语句的执行计划,是否是全局扫描,如果是全局扫描可以对语句进行优化。
explain:提供了查询信息,使用索引及查询统计等。有利于我们对索引的优化。
如在查询语句中使用explain:
db.users.find({gender:“M”},{user_name:1,_id:0}).explain()
返回结果:
{
"cursor" : "BtreeCursor gender_1_user_name_1",
"isMultiKey" : false,
"n" : 1,
"nscannedObjects" : 0,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 0,
"nscannedAllPlans" : 1,
"scanAndOrder" : false,
"indexOnly" : true,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"gender" : [
[
"M",
"M"
]
],
"user_name" : [
[
{
"$minElement" : 1
},
{
"$maxElement" : 1
}
]
]
}
}
字段分析:
使用hint来强制MongoDB使用一个指定的索引。
这种方法某些情形下会提升性能。 一个有索引的 collection 并且执行一个多字段的查询(一些字段已经索引了)。
如下查询实例指定了使用 gender 和 user_name 索引字段来查询:
db.users.find({gender:“M”},{user_name:1,_id:0}).hint({gender:1,user_name:1})
可以使用 explain() 函数来分析以上查询:
db.users.find({gender:“M”},{user_name:1,_id:0}).hint({gender:1,user_name:1}).explain()
Wiredtiger的Cache采用Btree的方式组织,每个Btree节点为一个page,root page是btree的根节点,internal page是btree的中间索引节点,leaf page是真正存储数据的叶子节点;btree的数据以page为单位按需从磁盘加载或写入磁盘。
btree的每个PAGE以文件里的extent形式(由文件offset + size标识)存储。
参考:https://mp.weixin.qq.com/s/Wuzh47jsBh5QonBrZxUnjg
存储引擎要做的事无外乎是将磁盘上的数据读取到内存并返回给应用,或者是将应用修改的数据由内存写到磁盘。
如何设计一种高效的数据结构和算法是所有存储引擎要考虑的根本问题,目前大多数流行的存储引擎是基于B-tree或LSM(LogStructured merge)Tree这两种数据结构来设计的。
像Oracle、SQL Server、DB2、MySQL(InnoDB)和Postgresql这些传统的关系数据库依赖的底层存储引擎是基于b-tree开发的,而像Cassandra、Elasticsearch (Lucene)、Google Bigtable、Apache HBase、LevelDB和RocksDB这些当前比较流行的NoSQL数据库存储引擎是基于LSM开发的。当然有些数据库采用了插件式的存储引擎架构,实现了Server层和存储引擎层的解耦,可以支持多种存储引擎,如MySQL既可以支持B-Tree结构的InnoDB存储引擎,还可以支持LSM结构的RocksDB存储引擎。
对于MongoDB来说,也采用了插件式存储引擎架构,底层的WiredTiger存储引擎还可以支持B-Tree和LSM两种结构组织数据,但MongoDB在使用WiredTiger作为存储引擎时,目前默认配置是使用了B-tree 结构。
B-Tree是为磁盘或其它辅助存储设备而设计的一种数据结构,目的是为了在查找数据的过程中减少磁盘I/O的次数,一个典型的B-Tree结构如下图所示:
在整个B-Tree中,从上往下依次为Root结点、内部结点和叶子结点,每个结点就是一个Page,数据以Page为单位在内存和磁盘间进行调度,每个Page的大小决定了相应结点的分支数量,每条索引记录会包含一个数据指针,指向一条数据记录所在文件的偏移量。
如上图,假设每个结点100个分支,那么所有叶子结点合起来可以包含100万个键值(等于100100100)。通常情况下Root结点和内部结点的Page会驻留在内存中,所以查找一条数据可能只需2次磁盘I/O。但随着数据不断的插入、删除,会涉及到B-Tree结点的分裂、位置提升及合并等操作,因此维护一个B-Tree的平衡也是比较耗时的。
对于WiredTiger存储引擎来说,集合所在的数据文件和相应的索引文件都是按B-Tree结构来组织的,不同之处在于数据文件对应的B-Tree叶子结点上除了存储键名外(keys),还会存储真正的集合数据(values),所以数据文件的存储结构也可以认为是一种B+Tree,其整体结构如下图所示:
从上图可以看到,B+ Tree中的leaf page包含一个页头(page header)、块头(block header)和真正的数据(key/value),其中页头定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息;块头定义了此页的checksum、块在磁盘上的寻址位置等信息。
WiredTiger有一个块设备管理的模块,用来为page分配block。如果要定位某一行数据(key/value)的位置,可以先通过block的位置找到此page(相对于文件起始位置的偏移量),再通过page找到行数据的相对位置,最后可以得到行数据相对于文件起始位置的偏移量offsets。由于offsets是一个8字节大小的变量,所以WiredTiger磁盘文件的大小,其最大值可以非常大(264bit)。
WiredTiger会按需将磁盘的数据以page为单位加载到内存,同时在内存中会构造相应的B-Tree来存储这些数据。为了高效的支撑CRUD等操作以及将内存里面发生变化的数据持久化到磁盘上,WiredTiger也会在内存里面维护其他几种数据结构,如下图:
上图是WiredTiger在内存里面的大概布局图,通过它我们可以梳理清楚存储引擎是如何将数据加载到内存,然后如何通过相应数据结构来支持查询、插入、修改操作的。
下图是一个跳转链表的插入示例:
假如现在插入一个16,最终结果:
如果是一个普通的链表,寻找合适的插入位置时,需要经过:
开始结点2581020的比较。
对于跳转链表来说只需要经过:开始结点51020的比较。可以看到比在普通链表上寻找插入位置时需要的比较步骤少,所以,通过跳转链表的数据结构能够提升插入操作的效率。
page的其他数据结构
对于一个面向行存储的leaf page来说,包含的数据结构除了上面提到的WT_ROW(keys/values)、WT_UPDATE(修改数据)、WT_INSERT_HEAD(插入数据)外,还有如下几种重要的数据结构:
WT_PAGE_MODIFY
保存page上事务、脏数据字节大小等与page修改相关的信息;
read_gen
page的read generation值作为evict page时使用,具体来说对应page在LRU队列中的位置,决定page被evict server选中淘汰出去的先后顺序。
WT_PAGE_LOOKASIDE
page关联的lookasidetable数据。当对一个page进行reconcile时,如果系统中还有之前的读操作正在访问此page上修改的数据,则会将这些数据保存到lookasidetable;当page再被读时,可以利用lookasidetable中的数据重新构建内存page。
WT_ADDR
page被成功reconciled后,对应的磁盘上块的地址,将按这个地址将page写到磁盘,块是最小磁盘上文件的最小分配单元,一个page可能有多个块。
checksum
page的校验和,如果page从磁盘读到内存后没有任何修改,比较checksum可以得到相等结果,那么后续reconcile这个page时,不会将这个page的再重新写入磁盘。
1)安装openssl
sudo apt-get install pkg-config libssl-dev libsasl2-dev
2)安装mongo-c-driver-1.16.2
下载:wget https://github.com/mongodb/mongo-c-driver/releases/download/1.16.2/mongo-c-driver-1.16.2.tar.gz
解压:tar -xvf mongo-c-driver-1.6.2.tar.gz
进入目录:cd mongo-c-driver-1.6.2
创建cmake-bulid:mkdir cmake-build
进入目录:cd cmake-build
保存后更新资源:cmake -DENABLE_AUTOMATIC_INIT_AND_CLEANUP=OFF ..
编译:sudo make & make install
Libmongoc API信息 :http://mongoc.org/libmongoc/1.16.2
参考:https://blog.csdn.net/chenjiayi_yun/article/details/50779284