1 MongoDB体系结构
配置文件参考博客:【https://blog.csdn.net/yuanwei1144/article/details/103779345】
适用场景
对于MongoDB实际应用来讲,是否使用MongoDB需要根据项目的特定特点进行甄别,这就需要我们对MongoDB适用和不适用的场景有一定的了解。
根据MongoDB 官网的说明,MongoDB 的适用场景如下:
1.网站实时数据:mongoDB非常适合实时的插入,更新与查询,并具备网站实时数据存储所需的复制及高度伸缩性。
由于MongoDB独特的数据处理方式,可以将热点数据加载到内存,故而对查询来讲,会非常快(当然也会非常消耗内存);同时由于采用了BSON的方式存储数据,故而对JSON格式数据具有非常好的支持性以及友好的表结构修改性,文档式的存储方式,数据友好可见;数据库的分片集群负载具有非常好的扩展性以及非常不错的自动故障转移(大赞)。
2.数据缓存:由于性能很高,MongoDB 也适合作为信息基础设施的缓存层。在系统重启之后,由MongoDB搭建的持久化缓存层可以避免下层的数据源过载。
3.大尺寸、低价值数据存储:使用传统的关系型数据库存储一些数据时可能会比较昂贵,在此之前,很多时候程序员往往会选择传统的文件进行存储。
4.高伸缩性场景:MongoDB 非常适合由数十或数百台服务器组成的数据库。MongoDB 的路线图中已经包含对MapReduce 引擎的内置支持。
5.对象或JSON 数据存储:MongoDB 的BSON 数据格式非常适合文档化格式的存储及查询。
不适用场景:
1.高度事务性系统:例如银行或会计系统。传统的关系型数据库目前还是更适用于需要大量原子性复杂事务的应用程序。
2.传统的商业智能应用:针对特定问题的BI 数据库会对产生高度优化的查询方式。对于此类应用,数据仓库可能是更合适的选择。
3.需要复杂SQL查询的问题。
如何抉择:
应用特征 | Yes / No |
---|---|
应用不需要事务及复杂 join 支持 | 必须 Yes |
新应用,需求会变,数据模型无法确定,想快速迭代开发 | ? |
应用需要2000-3000以上的读写QPS(更高也可以) | ? |
应用需要TB甚至 PB 级别数据存储 | ? |
应用发展迅速,需要能快速水平扩展 | ? |
应用要求存储的数据不丢失 | ? |
应用需要99.999%高可用 | ? |
应用需要大量的地理位置查询、文本查询 | ? |
写操作MongoDB比传统数据库快的根本原因是Mongo使用的内存映射技术 - 写入数据时候只要在 内存里完成就可以返回给应用程序,这样并发量自然就很高。而保存到硬体的操作则在后台异步完 成。注意MongoDB在2.4就已经是默认安全写了(具体实现在驱动程序里),所以楼上有同学的回答 说是”默认不安全“应该是基于2.2或之前版本的。
读操作MongoDB快的原因是:
1)MongoDB的设计要求你常用的数据(working set)可以在内存里 装下。这样大部分操作只需要读内存,自然很快。
2)文档性模式设计一般会是的你所需要的数据都 相对集中在一起(内存或硬盘),大家知道硬盘读写耗时最多是随机读写所产生的磁头定位时间, 数据集中在一起则减少了关系性数据库需要从各个地方去把数据找过来(然后Join)所耗费的随机读 时间 另外一个就是Mongo是分布式集群所以可以平行扩展。目前一般的百万次并发 量都是通过几十上百个节点的集群同时实现。这一点MySQL基本无法做到(或者要花很大定制的代 价)
MongoDB可以对数据进行分片,因此在大数据量(大部分场景)都不需要对数据进行分表存储。
1.1 Nosql和MongoDB
NoSQL=Not Only SQL,支持类似SQL的功能, 与Relational Database相辅相成。其性能较高, 不使用SQL意味着没有结构化的存储要求(SQL为结构化的查询语句),没有约束之后架构更加灵 活。
NoSQL数据库四大家族:
- 列存储 Hbase
- 键值(Key-Value)存储 Redis
- 图形存储 Neo4j
- 文档存储 MongoDB
MongoDB 是一个基于分布式文件存储的数据库,由 C++ 编写,可以为 WEB 应用提供可扩展、 高性能、易部署的数据存储解决方案。 MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库中功能最丰富、 最像关系数据库的。在高负载的情况下,通过添加更多的节点,可以保证服务器性能。
1.2 体系结构
1.3 MongoDB和RBMS对比
文档是一组键值(key-value)对(即 BSON)。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。
需要注意:
- 文档中的键/值对是有序的。
- 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)。
- MongoDB区分类型和大小写。
- MongoDB的文档不能有重复的键。
- 文档的键是字符串。除了少数例外情况,键可以使用任意UTF-8字符。
文档键命名规范:
① 键不能含有\0 (空字符)。这个字符用来表示键的结尾。
②.和$有特别的意义,只有在特定环境下才能使用。
③ 以下划线"_"开头的键是保留的(不是严格要求的)。
1.3 BSON
BSON是一种类json的一种二进制形式的存储格式,简称Binary JSON,它和JSON一样,支持内嵌的文 档对象和数组对象,但是BSON有JSON没有的一些数据类型,如Date和Binary Data类型。
BSON可以 做为网络数据交换的一种存储形式,是一种schema-less的存储形式它的优点是灵活性高,但它的缺点是空间利用率不是很理想(需要保存数据类型)
{key:value,key2:value2} 这是一个BSON的例子,其中key是字符串类型,后面的value值,它的类型一般 是字符串,double,Array,ISODate等类型。
BSON有三个特点:轻量性、可遍历性、高效性
1.4 Mongo数据类型
数据类型 | 描述 |
---|---|
String | 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。 |
Integer | 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。 |
Boolean | 布尔值。用于存储布尔值(真/假)。 |
Double | 双精度浮点值。用于存储浮点值。 |
Min/Max keys | 将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比。 |
Array | 用于将数组或列表或多个值存储为一个键。 |
Timestamp | 时间戳。记录文档修改或添加的具体时间。 |
Object | 用于内嵌文档。 |
Null | 用于创建空值。 |
Symbol | 符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。 |
Date或者ISODate | 日期时间(格林威治时间)。用 UNIX 时间格式来存储当前日期或时间。你可以指定自己的日期时间:创建 Date 对象,传入年月日信息。 |
Object ID | 对象 ID。用于创建文档的 ID。 |
Binary Data | 二进制数据。用于存储二进制数据。 |
Code | 代码类型。用于在文档中存储 JavaScript 代码。{x:function(){}} |
Regular expression | 正则表达式类型。用于存储正则表达式。 |
File | 二进制转码(Base64)后存储 (<16M) 2、 GridFS(>16M) GridFS 用两个集合来存储一个文件:fs.files与 fs.chunks 真正存储需要使用mongofiles -d gridfs put song.mp3 |
GeoData | : { type: , coordinates: } location: { type: “Point”, coordinates: [-73.856077, 40.848447] } 参考文档 https://docs.mongodb.com/manual/geospatial-queries/#geospatial-data |
几种重要的数据类型:
ObjectId
ObjectId 类似唯一主键,可以很快的去生成和排序,包含 12 bytes,含义是:
- 前 4 个字节表示创建 unix 时间戳,格林尼治时间 UTC 时间,比北京时间晚了 8 个小时
- 接下来的 3 个字节是机器标识码
- 紧接的两个字节由进程 id 组成 PID
- 最后三个字节是随机数
MongoDB 中存储的文档必须有一个 _id 键。这个键的值可以是任何类型的,默认是个 ObjectId 对象
由于 ObjectId 中保存了创建的时间戳,所以你不需要为你的文档保存时间戳字段,你可以通过 getTimestamp 函数来获取文档的创建时间
> var newObject = ObjectId()
> newObject.getTimestamp()
ISODate("2017-11-25T07:21:10Z")
ObjectId 转为字符串
> newObject.str
5a1919e63df83ce79df8b38f
字符串
BSON 字符串都是 UTF-8 编码。
时间戳
BSON 有一个特殊的时间戳类型用于 MongoDB 内部使用,与普通的 日期 类型不相关。 时间戳值是一个 64 位的值。其中:
- 前32位是一个 time_t 值(与Unix新纪元相差的秒数)
- 后32位是在某秒中操作的一个递增的
序数
在单个 mongod 实例中,时间戳值通常是唯一的。
在复制集中, oplog 有一个 ts 字段。这个字段中的值使用BSON时间戳表示了操作时间。
BSON 时间戳类型主要用于 MongoDB 内部使用。在大多数情况下的应用开发中,你可以使用 BSON 日期类型。
日期
表示当前距离 Unix新纪元(1970年1月1日)的毫秒数。日期类型是有符号的, 负数表示 1970 年之前的日期。
> var mydate1 = new Date() //格林尼治时间
> mydate1
ISODate("2018-03-04T14:58:51.233Z")
> typeof mydate1
object
> var mydate2 = ISODate() //格林尼治时间
> mydate2
ISODate("2018-03-04T15:00:45.479Z")
> typeof mydate2
object
这样创建的时间是日期类型,可以使用 JS 中的 Date 类型的方法。
返回一个时间类型的字符串:
> var mydate1str = mydate1.toString()
> mydate1str
Sun Mar 04 2018 14:58:51 GMT+0000 (UTC)
> typeof mydate1str
string
或者
> Date()
Sun Mar 04 2018 15:02:59 GMT+0000 (UTC)
2 MongoDB安装
我这里使用的是docker安装centos镜像,然后在centos里面安装MongoDB的。
第一步:启动centos
拉取centos镜像 docker pull centos
启动centos
12017 主要是用作mongodb绑定端口 1221为ssh server绑定端口
启动使用 /usr/sbin/init 命令主要是要用systemctl命令docker run --name centos-v1 -itd -p 12017:12017 1221:22 centos:latest /usr/sbin/init
第二步:安装必要的工具包
- 进入centos 容器
docker exec -it centos-v1 bash
- 安装open_server
yum install openssh-server.x86_64
- 安装net-tools
yum install net-tools
- 安装vim
yum install vim
- 安装passwd命令
yum install passwd
- 安装远程下载,上传工具
yum install lrzsz
第三步:创建mongo目录并上传
- 在/usr/local目录下创建mongo目录
cd /usr/local
mkdir mongo
- 下载mongo server安装包
使用cat /etc/redhat-release查看centos版本 我这里是[root@8477691e110c mongodb_4.4.1]# cat /etc/redhat-release
CentOS Linux release 8.2.2004 (Core)
所以下载的是8.0版本的mongo。我这里是先下载到我本地,然后通过rz命令上传到/usr/local/mongo目录下。下载下来为mongodb-linux-x86_64-rhel80-4.4.1.tgz
- 然后rz命令上传
rz
然后解压重命名
tar -zxvf mongodb-linux-x86_64-rhel80-4.4.1.tgz
mv mongodb-linux-x86_64-rhel80-4.4.1 mongodb_4.4.1
然后在mongodb_4.4.1创建 conf,data,logs目录。分别放配置文件,数据文件,日志文件
第四步:配置启动
- 在conf目录下创建mongod.conf
touch mongod.conf
- 然后将以下配置copy到里面即可
```yaml
storage:
存放数据路径
dbPath: /usr/local/mongo/mongodb_4.4.1/data类似于redolog
journal: enabled: true directoryPerDB: true engine: wiredTiger systemLog: destination: file日志存放文件
path: /usr/local/mongo/mongodb_4.4.1/logs/mongod.log logAppend: true
net: port: 12017 bindIp: 0.0.0.0
security:
访问集合是否需要登录权限校验
authorization: disabled
processManagement:
后台进程运行
fork: true
-
启动
<br />在/usr/local/mongo/mongod_4.4.1下执行命令
./bin/mongod —config=/usr/local/mongo/mongodb_4.4.1/conf/mongod.conf
以上,mongod安装工作就大功告成了。
我们可以通过 ./bin/mongo --host localhost --port 12017连接就可以了
<a name="d45a720c"></a>
# 3 MongoDB连接
当我使用客户端url来连接MongoDB服务器时,注意以下参数的作用。
标准url连接:
mongodb://[username:password@]host1[:port1][,host2[:port2],…[,hostN[:portN]]][/[database][?options]]
- **mongodb://** 这是固定的格式,必须要指定。
- **username:password@** 可选项,如果设置,在连接数据库服务器之后,驱动都会尝试登陆这个数据库
- **host1** 必须的指定至少一个host, host1 是这个URI唯一要填写的。它指定了要连接服务器的地址。如果要连接复制集,请指定多个主机地址。
- **portX** 可选的指定端口,如果不填,默认为27017
- **/database** 如果指定username:password@,连接并验证登陆指定数据库。若不指定,默认打开 test 数据库。
- **?options** 是连接选项。**如果不使用/database,则前面需要加上/**。所有连接选项都是键值对name=value,键值对之间通过&或;(分号)隔开
| 选项 | 描述 |
| :--- | :--- |
| replicaSet=name | 验证replica set的名称。 Impliesconnect=replicaSet. |
| slaveOk=true|false | true:在connect=direct模式下,驱动会连接第一台机器,即使这台服务器不是主。在connect=replicaSet模式下,驱动会发送所有的写请求到主并且把读取操作分布在其他从服务器。false: 在 connect=direct模式下,驱动会自动找寻主服务器. 在connect=replicaSet 模式下,驱动仅仅连接主服务器,并且所有的读写命令都连接到主服务器。 |
| connect=direct|replicaSet | 默认是replicaSet |
| safe=true|false | true: 在执行更新操作之后,驱动都会发送getLastError命令来确保更新成功。(还要参考 wtimeoutMS).false: 在每次更新之后,驱动不会发送getLastError来确保更新成功。默认true |
| w=n | 驱动添加 { w : n } 到getLastError命令. 应用于safe=true。至少需要n个服务器成功写入 |
| wtimeoutMS=ms | 驱动添加 { wtimeout : ms } 到 getlasterror 命令. 应用于 safe=true. 超时时间。 |
| fsync=true|false | true: 驱动添加 { fsync : true } 到 getlasterror 命令.应用于 safe=true.false: 驱动不会添加到getLastError命令中。 |
| journal=true|false | 如果设置为 true, 同步到 journal (在提交到数据库前写入到实体中). 应用于 safe=true |
| connectTimeoutMS=ms | 可以打开连接的超时时间。 |
| socketTimeoutMS=ms | 发送和接受sockets的超时时间 |
使用用户名和密码连接登陆到指定数据库,格式如下:
mongodb://admin:123456@localhost:27017,ip2:port2/test
连接本地数据库服务器,端口是默认的。
mongodb://localhost
使用用户名fred,密码foobar登录localhost的admin数据库。
mongodb://fred:foobar@localhost
使用用户名fred,密码foobar登录localhost的baz数据库。
mongodb://fred:foobar@localhost/baz
连接 replica pair, 服务器1为example1.com服务器2为example2。
mongodb://example1.com:27017,example2.com:27017
连接 replica set 三台服务器 (端口 27017, 27018, 和27019):
mongodb://localhost,localhost:27018,localhost:27019
连接 replica set 三台服务器, 写入操作应用在主服务器 并且分布查询到从服务器。
mongodb://host1,host2,host3/?slaveOk=true
直接连接第一个服务器,无论是replica set一部分或者主服务器或者从服务器。
mongodb://host1,host2,host3/?connect=direct;slaveOk=true
当你的连接服务器有优先级,还需要列出所有服务器,你可以使用上述连接方式。
安全模式连接到localhost:
mongodb://localhost/?safe=true
以安全模式连接到replica set,并且等待至少两个复制服务器成功写入,超时时间设置为2秒。
mongodb://host1,host2,host3/?safe=true;w=2;wtimeoutMS=2000
<a name="e76c38e6"></a>
# 3 使用
可以像mysql 那样 使用 use dbName切换数据库。即使不存在也可以切换。只要创建了集合这个数据库就存在了。如果不存在集合,那么使用 show dbs(查看所有数据库)是没有的。
show tables/show collections 查看所有集合
删除数据库: db.dropDataBase()
删除集合: db.collectionName.drop() 比如db.user.drop()
如果安装的MongoDB服务需要鉴权。
还需要 db.auth(user,pwd),不过使用db.auth()的时候需要先创建用户db.createUser()可以参考博文【[https://www.cnblogs.com/pl-boke/p/10063351.html](https://www.cnblogs.com/pl-boke/p/10063351.html%5D)】
更多命令:db.help()
地理位置的相关CRUD,参考文档【https://docs.mongodb.com/manual/geospatial-queries/#geospatial-data】
<a name="f222a526"></a>
## 4.1 创建集合
db.createCollection(name,options)
可选参数说明:
| 字段 | 类型 | 描述 |
| :--- | :--- | :--- |
| capped | 布尔 | (可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 **当该值为 true 时,必须指定 size 参数。** |
| autoIndexId | 布尔 | 3.2 之后不再支持该参数,会默认为_id列创建索引。(可选)如为 true,自动在 _id 字段创建索引。默认为 false。 |
| size | 数值 | (可选)为固定集合指定一个最大值,**以千字节计(KB**)。 **如果 capped 为 true,也需要指定该字段。** |
| max | 数值 | (可选)指定固定集合中包含文档的最大数量。 |
在插入文档时,MongoDB 首先检查固定集合的 size 字段,然后检查 max 字段。
比如:
db.createCollection(“mycol”, { capped : true, autoIndexId : true, size : 6142800, max : 10000 } ) { “ok” : 1 }
```
但是MongoDB不需要显示的创建集合,当插入文档数据时会自动创建:
> db.mycol2.insert({"name" : "w王豪"})
> show collections
mycol2
删除文档:
db.myCol2.drop()
我们也可以不通过createCollection创建。db.colName.insert()就可以,自动创建集合。
4.2 插入文档
集合名可以不存在,会自动创建。
db.colName.insert()
若插入的数据主键已经存在,则会抛 org.springframework.dao.DuplicateKeyException 异常,提示主键重复,不保存当前数据。
可以插入一条数据,也可以插入多条数据。
插入多条数据:
可以先创建一个数组,然后插入
>var arr = [{num:1},{num:2}];
>db.mycol2.insert(arr)
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
>db.mycol2.find()
{ "_id" : ObjectId("5f2953a06e9504b5b351f1d0"), "name" : "w王豪" }
{ "_id" : ObjectId("5f2955ad6e9504b5b351f1d1"), "name" : "w王豪" }
{ "_id" : ObjectId("5f295a806e9504b5b351f1d2"), "num" : 1 }
{ "_id" : ObjectId("5f295a806e9504b5b351f1d3"), "num" : 2 }
> var o = {_id:5,num:5}
> db.mycol2.insert(o)
WriteResult({ "nInserted" : 1 })
> db.mycol2.insert(o)
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: first.mycol2 index: _id_ dup key: { _id: 5.0 }"
}
})
3.2 版本之后新增了 db.collection.insertOne() 和 db.collection.insertMany()。
如果主键重复也会报错的。
db.collection.insertOne() 用于向集合插入一个新文档,语法格式如下:
db.collection.insertOne(
<document>,
{
writeConcern: <document>
}
)
db.collection.insertMany() 用于向集合插入一个多个文档,语法格式如下:
db.collection.insertMany(
[ <document 1> , <document 2>, ... ],
{
writeConcern: <document>,
ordered: <boolean>
}
)
参数说明:
document:要写入的文档。
writeConcern:
{ w: <value>, j: <boolean>, wtimeout: <number> }
w : 该选项要求确认操作已经传播到指定数量的mongod实例或指定标签的mongod实例
w:1(应答式写入)
要求确认操作已经传播到指定的单个mongod实例或副本集主实例(缺省为1)
w:0(非应答式写入)
不返回任何响应,所以无法知道写入是否成功
但是对于尝试向已关闭的套接字写入或者网络故障会返回异常信息
w:>1(用于副本集环境)
该值用于设定写入节点的数目,包括主节点。如果value大于replica set中节点数也不会
阻塞。
"majority"(大多数)
当数据写入到replica set的大多数节点之后向客户端返回,对于这种情况,一般是配合
read-concern使用
<tag set>
要求写入操作已经传递到指定tag标记副本集中的成员后进行应答
j : 该选项要求确认写操作已经写入journal日志之后应答客户端(需要开启journal功能)
则在意外重启,宕机等情形下可以通过journal来进行数据恢复
写入journal操作必须等待直到下次提交日志时完成写入
为降低延迟,MongoDB可以通过增加commit journal的频率来加快journal写入
对于w>1, 需要所有实例都写到journal之后才返回
j:true时会强制journal日志刷盘。默认时100ms从log buffer刷盘一次
wtimeout:
该选项指定一个时间限制,以防止写操作无限制被阻塞导致无法应答给客户端
wtimeout的单位为ms,当w值大于1时生效,该参数即仅适用于集群环境
当某个节点写入时超出指定wtimeout之后,mongod将返回一个错误
在捕获到超时之前,mongod并不会撤销其他节点已成功完成的写入
wtimeout值为0时等同于没有配置wtimeout选项,容易导致由于某个节点挂起而无法应答
对于单实例应答的情形,是将数据写入到内存后开始应答,除非j:true,则保证掉电后不会丢失数据
1、非应答式写入图示

MongoDB不对客户端进行应答,驱动会检查套接字,网络错误等。
mongos> db.version()
3.2.9
mongos> db.version()
3.2.9
mongos> db
test
mongos> db.blogs.insert({ename:"leshami",url:"http://blog.csdn.net/leshami"},{writeConcern:{w:0}})
WriteResult({ }) //此处应答为空
mongos> db.blogs.find({},{_id:0})
{ "ename" : "leshami", "url" : "http://blog.csdn.net/leshami" }
2、应答式写入图示

应答式写入是默认值
MongoDB会在收到写入操作并且确认该操作在内存中应用后进行应答,但不会确认数据是否已写入磁盘
同时允许客户端捕捉网络、重复key等等错误
mongos> db.blogs.insert({ename:"john",url:"http://blog.csdn.net/john"},{writeConcern:{w:1}})
WriteResult({ "nInserted" : 1 }) //此处应答信息显示为1个文档已插入
mongos> db.blogs.find({},{_id:0})
{ "ename" : "leshami", "url" : "http://blog.csdn.net/leshami" }
{ "ename" : "john", "url" : "http://blog.csdn.net/john" }
3、带journal应答式写入图示

确认写操作已经写入journal日志之后应答客户端,必须允许了日志功能,才能生效。
写入journal操作必须等待直到下次提交日志时完成写入
提供通过journal来进行数据恢复
4、副本集应答写入图示
<img src="assets/20161024173434123" alt="这里写图片描述" style="zoom:67%;" />
对于使用副本集的场景,缺省情况下仅仅从主(首选)节点进行应答
建议修改缺省的应答情形为特定数目或者majority来保证数据的可靠
如下示例,w值为2,超时为5s
db.products.insert(
{ item: “envelopes”, qty : 100, type: “Clasp” },
{ writeConcern: { w: 2, wtimeout: 5000 } }
)
如果不希望每次在增删改时添加writeConcern,可以通过设置settings.getLastErrorDefaults
如下示例,
cfg = rs.conf()<br />
cfg.settings = {}<br />
cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }<br />
rs.reconfig(cfg)<br />
3.2 之后 conf有一个属性 writeConcernMajorityJournalDefault可以直接进行配置。
cfg.writeConcernMajorityJournalDefault=true
而且在 在MongoDB3.4中,加入了[`writeConcernMajorityJournalDefault`](https://docs.mongodb.com/manual/reference/replica-configuration/#rsconf.writeConcernMajorityJournalDefault).这么一个选项,使得w,j在不同的组合下情况下不同:

四、小结
1、write concern用于控制写入安全的级别,可以分为应答式写入以及非应答式写入
2、write concern是一个性能和数据强一致性的权衡,应根据业务场景进行设定
3、对于强一致性场景,建议w>1或者等于majority,以及journal为true,否则w=0
4、在副本集的情形下,建议通过配置文件来修改w以及设置wtimeout,以避免由于某个节点挂起导致无法应答
比如:
var document = db.collection.insertOne({“a”: 3})
document
{
“acknowledged” : true,
“insertedId” : ObjectId(“571a218011a82a1d94c02333”)
}
插入多条数据
var res = db.collection.insertMany([{“b”: 3}, {‘c’: 4}])
res
{
“acknowledged” : true,
“insertedIds” : [
ObjectId(“571a22a911a82a1d94c02337”),
ObjectId(“571a22a911a82a1d94c02338”)
]
}
我们也可以先创建一个集合然后再使用insertMany()命令插入。
## 3.3 更新文档
db.collection.update()
db.collection.updateOne()
db.collection.updateMany()
可以set不存在的属性,如果不存在会添加属性。
语法:
db.collection.update(
,
,
{
upsert: ,
multi: ,
writeConcern:
}
)
**参数说明:**
- **query** : update的查询条件,类似sql update查询内where后面的。
- **update** : update的对象和一些更新的操作符(如$,$inc...)等,也可以理解为sql update查询内set后面的
- **upsert** : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
- **multi** : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
- **writeConcern** :可选,参考上面
db.col.insert({
title: ‘MongoDB 教程’,
description: ‘MongoDB 是一个 Nosql 数据库’,
by: ‘王豪’,
url: ‘http://www.trust.com‘,
tags: [‘mongodb’, ‘database’, ‘NoSQL’],
likes: 100
})
更新语句:
db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB',likes:38}})
db.col.update({‘title’:’MongoDB 教程’},{$set:{‘title’:’MongoDB’,likes:38}})
WriteResult({ “nMatched” : 1, “nUpserted” : 0, “nModified” : 1 })
db.col.find()
{ “_id” : ObjectId(“5f2960446e9504b5b351f1d4”), “title” : “MongoDB”, “description” : “MongoDB 是一个 Nosql 数据库”, “by” : “王豪”, “url” : “http://www.trust.com“, “tags” : [ “mongodb”, “database”, “NoSQL” ], “likes” : 38 }
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
返回结果:nMatched匹配数量 nUpserted插入数量 nModified更新数量
如果要更新满足多个条件的文档可以:
db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB',likes:38}},{multi:true})
如果更新的文档不存在则插入:
db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB',likes:380}},{upsert:true})
likes加100
db.col.update({title:'MongoDB教程},$incr:{likes:100})
注意点如果不加更新操作符 `$set`,`$unset`,`$incr` 是相当于替换整个文档。
还有一个更新操作命令
db.collection.updateOne(query,update,[options]) 这个是只更新一条数据
db.collection.updateMany(query,update,[options]) 这个是更新多条数据
name为赵六的不存在 会添加一条 name=赵六,age=23的数据
db.col2.updateMany({name:”赵六”},{%0A%23name%E4%B8%BA%E8%B5%B5%E5%85%AD%E7%9A%84%E4%B8%8D%E5%AD%98%E5%9C%A8%20%E4%BC%9A%E6%B7%BB%E5%8A%A0%E4%B8%80%E6%9D%A1%20name%3D%E9%99%88%E4%B8%83%EF%BC%8Cage%3D23%E7%9A%84%E6%95%B0%E6%8D%AE%0Adb.col2.updateMany(%7Bname%3A%22%E8%B5%B5%E5%85%AD%22%7D%2C%7B#card=math&code=set%3A%7B%22age%22%3A23%7D%7D%2C%7Bupsert%3Atrue%7D%29%0A%23name%E4%B8%BA%E8%B5%B5%E5%85%AD%E7%9A%84%E4%B8%8D%E5%AD%98%E5%9C%A8%20%E4%BC%9A%E6%B7%BB%E5%8A%A0%E4%B8%80%E6%9D%A1%20name%3D%E9%99%88%E4%B8%83%EF%BC%8Cage%3D23%E7%9A%84%E6%95%B0%E6%8D%AE%0Adb.col2.updateMany%28%7Bname%3A%22%E8%B5%B5%E5%85%AD%22%7D%2C%7B)set:{“age”:23,name:”陈七”}},{upsert:true})
## 4.4 删除文档
语法:
db.collection.remove(
,
{
justOne: ,
writeConcern:
}
)
**参数说明:**
- **query** :(可选)删除的文档的匹配条件。
- **justOne** : (可选)如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。
- **writeConcern** : 看前面
db.col.remove({likes:{$lt:39}})
db.col.find()
{ “_id” : ObjectId(“5f2960446e9504b5b351f1d4”), “title” : “MongoDB”, “description” : “MongoDB 是一个 Nosql 数据库”, “by” : “王豪”, “url” : “http://www.trust.com“, “tags” : [ “mongodb”, “database”, “NoSQL” ], “likes” : 38 }
{ “_id” : ObjectId(“5f29626d64050204c4d9abaa”), “title” : “MongoDB”, “likes” : 38 }
db.col.remove({likes:{%0AWriteResult(%7B%20%22nRemoved%22%20%3A%200%20%7D)%0Adb.col.remove(%7Blikes%3A%7B#card=math&code=lt%3A38%7D%7D%29%0AWriteResult%28%7B%20%22nRemoved%22%20%3A%200%20%7D%29%0Adb.col.remove%28%7Blikes%3A%7B)lt:39}})
WriteResult({ “nRemoved” : 2 })
db.col.find()
**remove方法已经过时了,而且remove不会真正的释放空间,需要执行命令来回收磁盘空间:**
db.repairDatabase()或者db.runCommand({repairDatabase:1})
现在推荐的命令:
db.collection.deleteMany({condition}) 删除满足条件的所有文档,如果db.collection.deleteMany({})会删除真个文档。
db.collection.deleteOne({condition})删除满足条件的第一个文档
## 3.5 查询文档
语法:
db.collection.find(query, projection)
- **query** :可选,使用查询操作符指定查询条件
- **projection** :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。
假如user表中有_id,name,age三个字段
比如只查询name,不查询age字段 如果不显式指定不查询_id,默认都会查询
db.user.find().pretty()
{ “_id” : ObjectId(“5f27f757da04113c34da38c9”), “name” : “王豪”, “age” : 23 }
{ “_id” : 2, “name” : “张三”, “age” : 24 }
{ “_id” : 3, “name” : “王五”, “age” : 25 }
{ “_id” : 4, “name” : “李四”, “age” : 26 }
{ “_id” : 5, “name” : “赵六”, “age” : “27岁” }
db.user.find({},{name:1}).pretty()
{ “_id” : ObjectId(“5f27f757da04113c34da38c9”), “name” : “王豪” }
{ “_id” : 2, “name” : “张三” }
{ “_id” : 3, “name” : “王五” }
{ “_id” : 4, “name” : “李四” }
{ “_id” : 5, “name” : “赵六” }
如果不查询_id
db.user.find({},{_id:0}).pretty()
db.user.find({},{_id:0}).pretty()
{ “name” : “王豪”, “age” : 23 }
{ “name” : “张三”, “age” : 24 }
{ “name” : “王五”, “age” : 25 }
{ “name” : “李四”, “age” : 26 }
{ “name” : “赵六”, “age” : “27岁” }
如果需要美观输出:
db.collection.find(query, projection).pretty()
db.col.find()
{ “_id” : ObjectId(“5f2968c06e9504b5b351f1d5”), “title” : “MongoDB 教程”, “description” : “MongoDB 是一个 Nosql 数据库”, “by” : “王豪”, “url” : “http://www.trust.com“, “tags” : [ “mongodb”, “database”, “NoSQL” ], “likes” : 100 }
db.col.find().pretty()
{
“_id” : ObjectId(“5f2968c06e9504b5b351f1d5”),
“title” : “MongoDB 教程”,
“description” : “MongoDB 是一个 Nosql 数据库”,
“by” : “王豪”,
“url” : “http://www.trust.com“,
“tags” : [
“mongodb”,
“database”,
“NoSQL”
],
“likes” : 100
}
testDat:
for (var i = 1000; i <= 2000; i++) {
db.testData.insert( { x : i , name: “MACLEAN” , name1:”MACLEAN”, name2:”MACLEAN”, name3:”MACLEAN”} )
}
**常用操作符**
如果你熟悉常规的 SQL 数据,通过下表可以更好的理解 MongoDB 的条件语句查询:
| 操作 | 格式 | 范例 | RDBMS中的类似语句 |
| :----------- | :----------- | :-------------------------------------------- | :--------------------------- |
| 等于= | `{:`} | db.testData.find({"name":"MACLEAN"}).pretty() | `where by = '菜鸟教程'` |
| 小于< | `{:{$lt:}}` | db.testData.find({"x":{$lt:50}}).pretty() | `where x < 50` |
| 小于或等于<= | `{:{$lte:}}` | db.testData.find({"x":{$lte:50}}).pretty() | `where x <= 50` |
| 大于> | `{:{$gt:}}` | db.testData.find({"x":{$gt:50}}).pretty() | `where x > 50` |
| 大于或等于>= | `{:{$gte:}}` | db.testData.find({"x":{$gte:50}}).pretty() | `where x >= 50` |
| 不等于<> | `{:{$ne:}}` | db.testData.find({"x":{$ne:50}}).pretty() | where x!=50或者 where x<> 50 |
如果多个条件and 可以用分号隔开,如下
db.testData.find({“name”:”MACLEAN”,x:{lt:1090}}).pretty()
{ “_id” : ObjectId(“5f2952366e9504b5b351edf9”), “x” : 1018, “name” : “MACLEAN”, “name1” : “MACLEAN”, “name2” : “MACLEAN”, “name3” : “MACLEAN” }
{ “_id” : ObjectId(“5f2952366e9504b5b351edfa”), “x” : 1019, “name” : “MACLEAN”, “name1” : “MACLEAN”, “name2” : “MACLEAN”, “name3” : “MACLEAN” }
Type “it” for more
db.testData.find({“name”:”MACLEAN”,x:{
lt:1090}}).pretty()
{
“_id” : ObjectId(“5f2952366e9504b5b351ee3f”),
“x” : 1088,
“name” : “MACLEAN”,
“name1” : “MACLEAN”,
“name2” : “MACLEAN”,
“name3” : “MACLEAN”
}
{
“_id” : ObjectId(“5f2952366e9504b5b351ee40”),
“x” : 1089,
“name” : “MACLEAN”,
“name1” : “MACLEAN”,
“name2” : “MACLEAN”,
“name3” : “MACLEAN”
}
**or的语法**:
db.col.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()
可以and 和 or一起用
类似于sql的
where x > 1010 and x < 1090 and (name = '哈哈‘ or name1= ’哟哟‘)
db.testData.find({x:{lt:1090},$or:[{name:’哈哈’},{name1:’哟哟’}]}).pretty()
db.testData.find({x:{
lt:1090},$or:[{name:’哈哈’},{name1:’哟哟’}]}).pretty()
{
“_id” : ObjectId(“5f2952366e9504b5b351edfa”),
“x” : 1019,
“name” : “哈哈”,
“name1” : “MACLEAN”,
“name2” : “MACLEAN”,
“name3” : “MACLEAN”
}
{
“_id” : ObjectId(“5f2952366e9504b5b351ee04”),
“x” : 1029,
“name” : “MACLEAN”,
“name1” : “哟哟”,
“name2” : “MACLEAN”,
“name3” : “MACLEAN”
}
student的数据:

也可以实现类似 select * from student where major="电子信息" or (school='武汉纺织大学' and major='服装设计')
db.student.find({$or:[{school:’武汉纺织大学’,major:’服装设计’},{major:’电子信息’}]})

**模糊查询:** 注意//中间是没有单引号或者双引号的
db.user.find().pretty()
{ “_id” : ObjectId(“5f27f757da04113c34da38c9”), “name” : “王豪”, “age” : 23 }
{ “_id” : 2, “name” : “张三”, “age” : 24 }
{ “_id” : 3, “name” : “王五”, “age” : 25 }
{ “_id” : 4, “name” : “李四”, “age” : 26 }
name包含王的文档
db.user.find({name:/王/}).pretty()
db.user.find({name:/王/}).pretty()
{ “_id” : ObjectId(“5f27f757da04113c34da38c9”), “name” : “王豪”, “age” : 23 }
{ “_id” : 3, “name” : “王五”, “age” : 25 }
name以张开头的文档
db.user.find({name:/^张/}).pretty()
db.user.find({name:/^张/}).pretty()
{ “_id” : 2, “name” : “张三”, “age” : 24 }
name以四结尾的文档
db.user.find({name:/四$/}).pretty()
db.user.find({name:/四$/}).pretty()
{ “_id” : 4, “name” : “李四”, “age” : 26 }
in 查询
db.user.find({name:{$in:[“张三”,”李四”]}}).pretty()
all 查询
$all:满足所有元素的数据符合列表里面元素条件就可以 显示数据
db.user.find({hobby:{$all:[“足球”] } })
{ “_id” : ObjectId(“5ca7a4c4219efd687462f968”), “id” : 4, “name” : “xiaogang”, “age” : 34, “hobby” : [ “羽毛球”, “篮球”, “足球” ] }db.user.find({hobby:{$all:[“足球”,”羽毛球”] } })
{ “_id” : ObjectId(“5ca7a4c4219efd687462f968”), “id” : 4, “name” : “xiaogang”, “age” : 34, “hobby” : [ “羽毛球”, “篮球”, “足球” ] }db.user.find({hobby:{$all:[“足球”,”桌球”] } })
$type操作符:
因为MongoDB对相同字段的每个文档的数据类型不做限制,因此我们可以通过数据类型进行筛选。
| **类型** | **数字** | **备注** |
| :---------------------- | :------- | :--------------- |
| Double | 1 | |
| String | 2 | |
| Object | 3 | |
| Array | 4 | |
| Binary data | 5 | |
| Undefined | 6 | 已废弃。 |
| Object id | 7 | |
| Boolean | 8 | |
| Date | 9 | |
| Null | 10 | |
| Regular Expression | 11 | |
| JavaScript | 13 | |
| Symbol | 14 | |
| JavaScript (with scope) | 15 | |
| 32-bit integer | 16 | |
| Timestamp | 17 | |
| 64-bit integer | 18 | |
| Min key | 255 | Query with `-1`. |
| Max key | 127 | |
db.user.find().pretty() ))
{ “_id” : ObjectId(“5f27f757da04113c34da38c9”), “name” : “王豪”, “age” : 23 }
{ “_id” : 2, “name” : “张三”, “age” : 24 }
{ “_id” : 3, “name” : “王五”, “age” : 25 }
{ “_id” : 4, “name” : “李四”, “age” : 26 }
{ “_id” : 5, “name” : “赵六”, “age” : “27岁” }
db.user.find({age:{%0A%E6%88%96%E8%80%85%0Adb.user.find(%7Bage%3A%7B#card=math&code=type%3A2%7D%7D%29%0A%E6%88%96%E8%80%85%0Adb.user.find%28%7Bage%3A%7B)type:’string’}})
db.user.find({age:{$type:’string’}})
{ “_id” : 5, “name” : “赵六”, “age” : “27岁” }
## 4.6 limit与skip
这里结合使用limit和skip方法可以达到关系型数据库的分析效果。
limit(n) 只查询前n条数据,如果不指定n,查询所有文档。

skip(m)跳过前m个文档,如果不指定m,默认为0

## 4.7 排序
在 MongoDB 中使用 sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,**其中 1 为升序排列,而 -1 是用于降序排列**。
如果将sort(),skip(),limit()一起执行的时候,先执行sort()再执行skip()再执行limit()。
按照_id倒序排序,跳过第一个,查询3条数据。
db.user.find().limit(3).skip(1).sort({_id:-1})

**组合排序**
db.user.find().limit(3).sort({age:-1,_id:1})
db.user.find().limit(3).sort({_id:1,age:-1})

我们看到是可以组合排序的,先根据前面的排序,如果后面的排序字段相等,则按照后面指定的排序规则排序。
## 3.8 索引
索引实现原理【https://www.cnblogs.com/coder2012/p/5309197.html】
MongoDB中当然也是支持索引的,来提高查询效率,如果不使用索引,会扫描整个文档来找到满足查询条件的记录。
db.collection.createIndex({keys},{options})
如果keys里面指定多项,就是**复合索引**了(类似于Mysql的复合索引)。比如:
db.collection.createIndex( { “字段名1” : 排序方式, “字段名2” : 排序方式 },options)
key对应的值如果指定1则按升序创建索引,如果-1降序创建索引。
如果是**全文索引**,key值为“text",如db.user.createIndex({"desp":"text"})。一个集合仅支持最多一个Text Index,中文分词不理想 推荐ES。只支持string类型。
db.collection.createIndex({“field”: “text”})
全文索引语法
db.collection.find({search:”searchWord”}})
db.user.find({search:”two”}})
如果属性值为数组,,MongoDB支持针对数组中每一个element创建索引,支持strings,numbers和nested documents。叫做**多键索引**。
针对地理空间坐标数据创建索引,叫做**地理空间索引**。`2dsphere`索引,用于存储和查找球面上的点;`2d`索引,用于存储和查找平面上的点。我们在地理空间位置上创建索引,应该使用2dsphere。一个平面坐标应该使用2d。
查询地理位置详情可以查看官方文档。
db.company.insert(
{
loc : { type: “Point”, coordinates: [ 116.482451, 39.914176 ] },
name: “大望路地铁”,
category : “Parks”
}
)
db.company.createIndex( { loc : “2dsphere” } )
参数不是1或-1,为2dsphere 或者 2d。还可以建立组合索引。
查询以[116.482451,39.914176]五公里内的坐标
db.company.find({
“loc” : {
“center”:[[116.482451,39.914176],0.05]
}
}
})
**哈希索引 Hashed Index**针对属性的哈希值进行索引查询,当要使用Hashed index时,MongoDB能够自动的计算hash值,无 需程序计算hash值。注:hash index仅支持等于查询,不支持范围查询。只能在单个字段建立哈希索引。
db.collection.createIndex({“field”: “hashed”})
options可选参数:
| Parameter | Type | Description |
| :----------------- | :------------ | :----------------------------------------------------------- |
| background | Boolean | "background" 默认值为**false**,建索引过程会阻塞其它数据库操作。,background可指定以后台方式创建索引,设为true,在后台建立索引的时候,不能对建立索引的collection进行一些坏灭型的操作,如:运行`repairDatabase,drop,compat,当你在建立索引的时候运行这些操作的会报错。` |
| unique | Boolean | 建立的索引是否唯一。指定为true创建唯一索引。默认值为**false**. |
| name | string | 索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。 |
| dropDups | Boolean | **3.0+版本已废弃。**在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 **false**. |
| sparse | Boolean | 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 **false**. 可以在复合索引开启稀疏索引。 |
| expireAfterSeconds | integer | 指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。 |
| v | index version | 索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。 |
| weights | document | 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。 |
| default_language | string | 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语 |
| language_override | string | 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language. |
1、查看集合索引
db.col.getIndexes()
2、查看集合索引大小
db.col.totalIndexSize()
3、删除集合所有索引
db.col.dropIndexes()
_id的索引是不会删除的。
4、删除集合指定索引
db.col.dropIndex(“索引名称”)
利用 TTL 集合对存储的数据进行失效时间设置:经过指定的时间段后或在指定的时间点过期,MongoDB 独立线程去清除数据。类似于设置定时自动删除任务,可以清除历史记录或日志等前提条件,设置 Index 的关键字段为日期类型 new Date()。
**例如数据记录中 createDate 为日期类型时:**
- 设置时间180秒后自动清除。
- 设置在创建记录后,180 秒左右删除。
db.col.createIndex({“createDate”: 1},{expireAfterSeconds: 180})
**由记录中设定日期点清除。**
设置 A 记录在 2019 年 1 月 22 日晚上 11 点左右删除,A 记录中需添加 "c_date": new Date('Jan 22, 2019 23:00:00'),且 Index中expireAfterSeconds 设值为 0。其实也可以设置为任意字段名,只要为日期格式就可以。
db.col.createIndex({“c_date”: 1},{expireAfterSeconds: 0})
其他注意事项:
- 索引关键字段必须是 Date 类型。
- 非立即执行:扫描 Document 过期数据并删除是独立线程执行,默认 60s 扫描一次,删除也不一定是立即删除成功。
- 单字段索引,混合索引不支持。
其实对于MongoDB集群创建索引是有点麻烦的,参考文档:【https://www.imooc.com/article/291056?block_id=tuijian_wz】
在使用MongoDB时,在创建索引会涉及到在复制集(replication)以及分片(Shard)中创建,为了最大限度地减少构建索引的影响,在副本和分片中创建索引,使用滚动索引构建过程。如果不使用滚动索引构建过程:
- **主服务器上的前台索引构建需要数据库锁定。它复制为副本集辅助节点上的前台索引构建,并且复制工作程序采用全局数据库锁定,该锁定将读取和写入排序到索引服务器上的所有数据库。**
- **主要的后台索引构建复制为后台索引构建在辅助节点上。复制工作程序不会进行全局数据库锁定,并且辅助读取不会受到影响。**
- **对于主服务器上的前台和后台索引构建,副本集辅助节点上的索引操作在主节点完成构建索引之后开始。**
- **在辅助节点上构建索引所需的时间必须在oplog的窗口内,以便辅助节点可以赶上主节点。
那么该如何创建呢?具体步骤呢?请看接下来的具体过程。**
**在副本集创建索引**
准备
**必须在索引构建期间停止对集合的所有写入,否则可能会在副本集成员中获得不一致的数据。**
具体过程
在副本集中以滚动方式构建唯一索引包括以下过程:
①停止一个Secondary节点(从节点)并以单机模式重新启动,可以使用配置文件更新配置以单机模式重新启动:
- 注释掉replication.replSetName选项。
- 将net.port更改为其他端口。将原始端口设置注释掉。
- 在setParameter部分中将参数disableLogicalSessionCacheRefresh设置为true。
②创建索引:在单机模式下进行索引创建
③重新开启Replica Set 模式:索引构建完成后,关闭mongod实例。撤消作为独立启动时所做的配置 更改,以返回其原始配置并作为副本集的成员重新启动。
④在其他从节点中重复1、2、3步骤的过程操作。
⑤主节点创建索引,当所有从节点都有新索引时,降低主节点,使用上述过程作为单机模式重新启动它,并在原主节点上构建索引:
使用mongo shell中的rs.stepDown()方法来降低主节点为从节点,
成功降级后,当前主节点成为从节点,副本集成员选择新主节点,并进行从节点创建方式进行创建索引。
**在分片集群创建唯一索引**
创建唯一索引,必须在索引构建期间停止对集合的所有写入。 否则,您可能会在副本集成员中获得不一致的数据。如果无法停止对集合的所有写入,请不要使用以下过程来创建唯一索引。
具体过程
①停止Balancer:将mongo shell连接到分片群集中的mongos实例,然后运行sh.stopBalancer()以禁用Balancer。如果正在进行迁移,系统将在停止平衡器之前完成正在进行的迁移。
②确定Collection的分布:刷新该mongos的缓存路由表,以避免返回该Collection旧的分发信息。刷新后,对要构建索引的集合运行db.collection.getShardDistribution()。
③在包含集合Chunks的分片创建索引
C1.停止从节点,并以单机模式重新启动:对于受影响的分片,停止从节点与其中一个分区相关联的mongod进 程,进行配置文件/命令模式更新后重新启动。
配置文件:
将net.port更改为其他端口。 注释到原始端口设置。
注释掉replication.replSetName选项。
注释掉sharding.clusterRole选项。
在setParameter部分中将参数skipShardingConfigurationChecks设置为true。
在setParameter部分中将参数disableLogicalSessionCacheRefresh设置为true。
C2.创建索引:直接连接到在新端口上作为独立运行的mongod实例,并为此实例创建新索引。
C3.恢复C1的配置,并作为 Replica Set成员启动:索引构建完成后,关闭mongod实例。 撤消作为单机模式时 所做的配置更改,以返回其原始配置并重新启动。
C4.其他从节点分片重复C1、C2、C3过程创建索引。
C5.主节点创建索引:当所有从节点都有新索引时,降低主节点,使用上述过程作为单机模式重新启动它,并在 原主节点上构建索引
使用mongo shell中的rs.stepDown()方法来降低主节点为从节点,成功降级后,当前主节点成为从节点,副 本集成员选择新主节点,并进行从节点创建方式进行创建索引。
在其他受影响的分片重复C步骤;
重启Balancer,一旦全部分片创建完索引,重启Balancer:sh.startBalancer()。
## 3.9 聚合
**单目聚合操作**
count()和distinct()
db.collection.find().count();
db.collection.count(query)
db.collection.distinct(fieldName,query,options)
官方文档:【https://docs.mongodb.com/manual/reference/method/db.collection.distinct/】
比如有数据
var arr = [
{ “_id”: 1, “dept”: “A”, “item”: { “sku”: “111”, “color”: “red” }, “sizes”: [ “S”, “M” ] },
{ “_id”: 2, “dept”: “A”, “item”: { “sku”: “111”, “color”: “blue” }, “sizes”: [ “M”, “L” ] },
{ “_id”: 3, “dept”: “B”, “item”: { “sku”: “222”, “color”: “blue” }, “sizes”: “S” },
{ “_id”: 4, “dept”: “A”, “item”: { “sku”: “333”, “color”: “black” }, “sizes”: [ “S” ] }];
db.inventory.insertMany(arr);
db.inventory.distinct( “dept” )
[ “A”, “B” ]
db.inventory.distinct( “item.sku” )
[ “111”, “222”, “333” ]
db.inventory.distinct( “item.sku” ,{“dept”:”A”})
[ “111”, “333” ]
**聚合管道**
语法:
db.COLLECTION_NAME.aggregate(AGGREGATE_OPERATION)
集合中的数据如下:
var array = [{
title: ‘MongoDB Overview’,
description: ‘MongoDB is no sql database’,
by_user: ‘runoob.com’,
url: ‘http://www.runoob1.com‘,
tags: [‘mongodb’, ‘database’, ‘NoSQL’],
likes: 100
},
{
title: ‘NoSQL Overview’,
description: ‘No sql database is very fast’,
by_user: ‘runoob.com’,
url: ‘http://www.runoob2.com‘,
tags: [‘mongodb’, ‘database’, ‘NoSQL’],
likes: 10
},
{
title: ‘Neo4j Overview’,
description: ‘Neo4j is no sql database’,
by_user: ‘Neo4j’,
url: ‘http://www.neo4j.com‘,
tags: [‘neo4j’, ‘database’, ‘NoSQL’],
likes: 750
}]
| 表达式 | 描述 | 实例 |
| :-------- | :--------------------------------------------- | :----------------------------------------------------------- |
| $sum | 计算总和。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : "$likes"}}}])<br>结果<br>{ "_id" : "Neo4j", "num_tutorial" : 750 }<br/>{ "_id" : "runoob.com", "num_tutorial" : 110 } |
| $avg | 计算平均值 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$avg : "$likes"}}}])<br>结果<br>{ "_id" : "runoob.com", "num_tutorial" : 55 }<br/>{ "_id" : "Neo4j", "num_tutorial" : 750 } |
| $min | 获取集合中所有文档对应值得最小值。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$min : "$likes"}}}])<br>结果<br>{ "_id" : "runoob.com", "num_tutorial" : 10 }<br/>{ "_id" : "Neo4j", "num_tutorial" : 750 } |
| $max | 获取集合中所有文档对应值得最大值。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$max : "$likes"}}}])<br>结果<br>{ "_id" : "Neo4j", "num_tutorial" : 750 }<br/>{ "_id" : "runoob.com", "num_tutorial" : 100 } |
| $push | 在结果文档中插入值到一个数组中。 | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$push: "$url"}}}])<br>结果<br>`{ "_id" : "Neo4j", "url" : [ "http://www.neo4j.com" ] }{ "_id" : "runoob.com", "url" : [ "http://www.runoob2.com", "http://www.runoob1.com" ] }` |
| $addToSet | 在结果文档中插入值到一个数组中,但不创建副本。 | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$addToSet: "$url"}}}])<br>结果<br>`{ "_id" : "Neo4j", "url" : [ "http://www.neo4j.com" ] }{ "_id" : "runoob.com", "url" : [ "http://www.runoob2.com", "http://www.runoob1.com" ] }` |
| $first | 根据资源文档的排序获取第一个文档数据。 | db.mycol.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}])<br>结果<br>`{ "_id" : "Neo4j", "first_url" : "http://www.neo4j.com" }{ "_id" : "runoob.com", "first_url" : "http://www.runoob1.com" }` |
| $last | 根据资源文档的排序获取最后一个文档数据 | db.mycol.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"},likes:{$push:'$likes'}}}])<br>结果<br>`{ "_id" : "Neo4j", "last_url" : "http://www.neo4j.com", "likes" : [ 750 ] }{ "_id" : "runoob.com", "last_url" : "http://www.runoob2.com", "likes" : [ 100, 10 ] }` |
group 多个字段
db.mycol.aggregate([{title”,g2:”$by_user”}}}])
相当于sql select title as g1,by_user as g2 from mycol group by g1,g2
如果不需要进行分组 但是想计算 sum,avg,min,max 可以指定$group的_id为null
比如:
db.mycol.aggregate([{
sum : “
%0A%7B%20%22_id%22%20%3A%20null%2C%20%22likeSum%22%20%3A%20860%20%7D%0Adb.mycol.aggregate(%5B%7B#card=math&code=likes%22%7D%7D%7D%5D%29%0A%7B%20%22_id%22%20%3A%20null%2C%20%22likeSum%22%20%3A%20860%20%7D%0Adb.mycol.aggregate%28%5B%7B)group : {_id : null, likemin : {
likes”}}}])
{ “_id” : null, “likemin” : 10 }
当然还有一个分组函数:参考文档【https://www.jianshu.com/p/206f036bdfc5】
db.collection.group({ key, reduce, initial [, keyf] [, cond] [, finalize] })
前三个是必备参数,“[]”中是可选参数
参数说明:
key
可以放用来分组的字段,并且会返回其中字段(group by 后面的字段)
reduce
是在分组操作期间对文档进行操作的聚合函数。可以返回总和或计数。该函数有两个参数:当前文档;该组的聚合结果文档。
initial
对结果中文档,字段进行初始化
cond
对数据筛选的条件,相当于where
管道的概念
管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。
MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。
表达式:处理输入文档并输出。表达式是无状态的,只能用于计算当前聚合管道的文档,不能处理其它的文档。
这里我们介绍一下聚合框架中常用的几个操作:
match:用于过滤数据,只输出符合条件的文档。
limit:用来限制MongoDB聚合管道返回的文档数。
unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
sort:将输入文档排序后输出。
$geoNear:输出接近某一地理位置的有序文档。
$match
db.mycol.aggregate([{$match:{by_user:”Neo4j”}}])
结果:
db.mycol.aggregate([{$match:{by_user:”Neo4j”}}]).pretty()
{
“_id” : ObjectId(“5f2a131f6e9504b5b351f1da”),
“title” : “Neo4j Overview”,
“description” : “Neo4j is no sql database”,
“by_user” : “Neo4j”,
“url” : “http://www.neo4j.com“,
“tags” : [
“neo4j”,
“database”,
“NoSQL”
],
“likes” : 750
}
$project 可以隐藏字段,指定显示字段
db.article.aggregate(
{ $project : {
title : 1 ,
author : 1 ,
}}
);
这样的话结果中就只还有_id,tilte和author三个字段了,默认情况下_id字段是被包含的,如果要想不包含_id话可以这样:
db.article.aggregate(
{ $project : {
_id : 0 ,
title : 1 ,
author : 1
}});
数据

```ja
db.student.aggregate([{$group:{_id:"$school",avg_age:{$avg: "$age"}}},{$match:{avg_age:{$gte:20}}}])
先根据school进行分组求平均年龄,然后过滤平均年龄大于20的数据,然后将avg_age命名为avgAge
db.student.aggregate([{$group:{_id:"$school",avg_age:{$avg: "$age"}}},{$match:{avg_age:{$gte:20}}},{$project: {avgAge:"$avg_age"}},])
3.10 MapReduce
官方文档:【https://docs.mongodb.com/manual/reference/method/db.collection.mapReduce/】
Pipeline查询速度快于MapReduce,但是MapReduce的强大之处在于能够在多台Server上并行执行复 杂的聚合逻辑。MongoDB不允许Pipeline的单个聚合操作占用过多的系统内存,如果一个聚合操作消 耗20%以上的内存,那么MongoDB直接停止操作,并向客户端输出错误消息。
MapReduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结 果合并成最终结果(REDUCE)。
>db.collection.mapReduce(
function() {emit(key,value);}, //map 函数 key是用来分组的。values用来
function(key,values) {return reduceFunction}, //reduce 函数
{
out: collection,
query: document,
sort: document,
limit: number,
finalize: <function>,
verbose: <boolean>
}
)
MapReduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结 果合并成最终结果(REDUCE)。
参数说明:
- map:是JavaScript 函数,负责将每一个输入文档转换为零或多个文档,生成键值对序列,作为 reduce 函数参数
- reduce:是JavaScript 函数,对map操作的输出做合并的化简的操作(将key-value变成keyvalues,也就是把values数组变成一个单一的值value)
- out:统计结果存放集合
- query: 一个筛选条件,只有满足条件的文档才会调用map函数。
- sort: 和limit结合的sort排序参数(也是在发往map函数前给文档排序),可以优化分组机制
- limit: 发往map函数的文档数量的上限(要是没有limit,单独使用sort的用处不大)
- finalize:可以对reduce输出结果再一次修改
- verbose:是否包括结果信息中的时间信息,默认为fasle
其实执行完MapReduce之后,会以out指定的名字创建一个集合。
> show collections;
majorAvgAge
student
>
然后 db.majorAvgAge.find()
数据:
db.student.mapReduce(function(){emit(this.major,this.age)},
function(key,values){return Array.avg(values)},
{
out:"majorAvgAge"
})
db.student.mapReduce(function(){emit(this.major,this.age)},
function(key,values){return Array.avg(values)},
{
out:"majorAvgAge",
finalize:function(key,value){
return value+3;
}
})
db.student.mapReduce(function(){emit(this.major,this.age)},
function(key,values){return Array.avg(values)},
{
out:"majorAvgAge",
query:{school:"武汉纺织大学"},
finalize:function(key,value){
return value+3;
}
})
5 高级
5.1 执行计划
我们利用js循环插入100万条数据来模拟大批量数据。
for(let i=1;i<=1000000;i++){
db.test_employee.insert({id:i,name:"test_"+i,salary:(Math.random()*20000).toFixed(2)})
}
explain()也接收不同的参数,通过设置不同参数我们可以查看更详细的查询计划。
- queryPlanner:queryPlanner是默认参数,具体执行计划信息参考下面的表格。
- executionStats:executionStats会返回执行计划的一些统计信息(有些版本中和 allPlansExecution等同)。
- allPlansExecution:allPlansExecution用来获取所有执行计划,结果参数基本与上文相同。
db.collection.find({}).explain()
db.collection.find({}).explain("queryPlanner")
explain响应信息解析:
对queryPlanner分析
queryPlanner : queryPlanner 的返回
queryPlanner.plannerVersion:查询计划版本
queryPlanner.namespace
: 该值返回的是该query所查询的表queryPlanner.indexFilterSet
: 针对该query是否有indexfilterqueryPlanner.winningPlan
: 查询优化器针对该query所返回的最优执行计划的详细内容。queryPlanner.winningPlan.stage
: 最优执行计划的stage,这里返回是FETCH,可以理解为通过返回的index位置去检索具体的文档(stage有数个模式,将在后文中进行详解)。queryPlanner.winningPlan.inputStage
: 用来描述子stage,并且为其父stage提供文档和索引关键字。queryPlanner.winningPlan.stage
的child stage
,此处是IXSCAN,表示进行的是index scanning。queryPlanner.winningPlan.keyPattern
: 所扫描的index内容,此处是did : 1,status : 1,modify_time : -1与scid : 1queryPlanner.winningPlan.indexName
:winning plan所选用的index。queryPlanner.winningPlan.isMultiKey
是否是Multikey,此处返回是false,如果索引建立在array上,此处将是true。queryPlanner.winningPlan.direction
:此query的查询顺序,此处是forward,如果用了.sort({modify_time : -1})将显示backward。queryPlanner.winningPlan.indexBounds
: winningplan所扫描的索引范围,如果没有制定范围就是[MaxKey, MinKey],这主要是直接定位到mongodb的chunck中去查找数据,加快数据读取。queryPlanner.rejectedPlans
:其他执行计划(非最优而被查询优化器reject的)的详细返回,其中具体信息与winningPlan的返回中意义相同,故不在此赘述。
对 executionStats 返回逐层分析
第一层,executionTimeMillis
最为直观 explain 返回值是 executionTimeMillis
值,指的是我们这条语句的执行时间,这个值当然是希望越少越好。
其中有3个 executionTimeMillis,分别是:
executionStats.executionTimeMillis
该query的整体查询时间。executionStats.executionStages.executionTimeMillisEstimate
该查询根据index去检索document获得2001条数据的时间。executionStats.executionStages.inputStage.executionTimeMillisEstimate
该查询扫描2001行index所用时间。
第二层,index 与 document 扫描数与查询返回条目数
这个主要讨论3个返回项:nReturned
、totalKeysExamined
、totalDocsExamined
,分别代表该条查询返回的条目、索引扫描条目、文档扫描条目。这些都是直观地影响到 executionTimeMillis
,我们需要扫描的越少速度越快。
对于一个查询,我们最理想的状态是:
nReturned = totalKeysExamined = totalDocsExamined
第三层,stage状态分析
那么又是什么影响到了 totalKeysExamined
和 totalDocsExamined
?是 stage 的类型。类型列举如下:
COLLSCAN
:全表扫描IXSCAN
:索引扫描FETCH
:根据索引去检索指定documentSHARD_MERGE
:将各个分片返回数据进行mergeSORT
:表明在内存中进行了排序LIMIT
:使用limit限制返回数SKIP
:使用skip进行跳过IDHACK
:针对_id进行查询HARDING_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的返回
对于普通查询,我希望看到 stage 的组合(查询的时候尽可能用上索引):
- Fetch+IDHACK
- Fetch+ixscan
- Limit+(Fetch+ixscan)
- PROJECTION+ixscan
- SHARDING_FITER+ixscan
- COUNT_SCAN
不希望看到包含如下的 stage:
- COLLSCAN(全表扫描)
- SORT(使用sort但是无index)
- 不合理的SKIP
- SUBPLA(未用到index的$or)
- COUNTSCAN(不使用index进行count)
执行以下命令:
db.test_employee.find({id:{$gt:500000}}).sort({id:-1}).limit(100).explain("executionStats")
返回:
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "lagou_learn.test_employee",
"indexFilterSet" : false,
"parsedQuery" : {
"id" : {
"$gt" : 500000
}
},
"winningPlan" : {
"stage" : "LIMIT",
"limitAmount" : 100,
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"id" : 1
},
"indexName" : "id_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"id" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "backward",
"indexBounds" : {
"id" : [
"[inf.0, 500000.0)"
]
}
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 100,
"executionTimeMillis" : 0,
"totalKeysExamined" : 100,
"totalDocsExamined" : 100,
"executionStages" : {
"stage" : "LIMIT",
"nReturned" : 100,
"executionTimeMillisEstimate" : 0,
"works" : 101,
"advanced" : 100,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"limitAmount" : 100,
"inputStage" : {
"stage" : "FETCH",
"nReturned" : 100,
"executionTimeMillisEstimate" : 0,
"works" : 100,
"advanced" : 100,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 0,
"docsExamined" : 100,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 100,
"executionTimeMillisEstimate" : 0,
"works" : 100,
"advanced" : 100,
"needTime" : 0,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 0,
"keyPattern" : {
"id" : 1
},
"indexName" : "id_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"id" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "backward",
"indexBounds" : {
"id" : [
"[inf.0, 500000.0)"
]
},
"keysExamined" : 100,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0
}
}
}
},
"serverInfo" : {
"host" : "8477691e110c",
"port" : 12017,
"version" : "4.4.1",
"gitVersion" : "ad91a93a5a31e175f5cbf8c69561e788bbc55ce1"
},
"ok" : 1
}
5.2 慢查询
开启内置的查询分析器,记录读写操作效率 db.setProfilingLevel(n,m) n的取值可选0,1,2
- 0表示不记录
- 1表示记录慢速操作,如果值为1,m必须赋值单位为ms,用于定义慢速查询时间的阈值
- 2表示记录所有的读写操作
2.查询监控结果 db.system.profile.find().sort({millis:-1}).limit(3)
3.分析慢速查询 应用程序设计不合理、不正确的数据模型、硬件配置问题,缺少索引等
4.解读explain结果 确定是否缺少索引
// 1
{
"op": "query",
"ns": "first.test_employee",
"command": {
"find": "test_employee",
"filter": {
"salary": {
"$in": [
"1253.74",
"6750.84"
]
}
},
"lsid": {
"id": UUID("05a83af1-44e2-4152-9d7f-8b84db1451fb")
},
"$db": "first"
},
"keysExamined": NumberInt("0"),
"docsExamined": NumberInt("100000"),
"cursorExhausted": true,
"numYield": NumberInt("781"),
"nreturned": NumberInt("2"),
"queryHash": "CD2B9A1D",
"planCacheKey": "CD2B9A1D",
"locks": {
"ParallelBatchWriterMode": {
"acquireCount": {
"r": NumberLong("1")
}
},
"ReplicationStateTransition": {
"acquireCount": {
"w": NumberLong("783")
}
},
"Global": {
"acquireCount": {
"r": NumberLong("783")
}
},
"Database": {
"acquireCount": {
"r": NumberLong("782")
}
},
"Collection": {
"acquireCount": {
"r": NumberLong("782")
}
},
"Mutex": {
"acquireCount": {
"r": NumberLong("1")
}
}
},
"flowControl": { },
"storage": { },
"responseLength": NumberInt("228"),
"protocol": "op_msg",
"millis": NumberInt("115"),
"planSummary": "COLLSCAN",
"execStats": {
"stage": "COLLSCAN",
"filter": {
"salary": {
"$in": [
"1253.74",
"6750.84"
]
}
},
"nReturned": NumberInt("2"),
"executionTimeMillisEstimate": NumberInt("1"),
"works": NumberInt("100002"),
"advanced": NumberInt("2"),
"needTime": NumberInt("99999"),
"needYield": NumberInt("0"),
"saveState": NumberInt("781"),
"restoreState": NumberInt("781"),
"isEOF": NumberInt("1"),
"direction": "forward",
"docsExamined": NumberInt("100000")
},
"ts": ISODate("2021-04-10T14:43:38.217Z"),
"client": "127.0.0.1",
"appName": "MongoDB Shell",
"allUsers": [ ],
"user": ""
}
// 2
{
"op": "query",
"ns": "first.test_employee",
"command": {
"find": "test_employee",
"filter": {
"salary": {
"$in": [
"1253.74",
"6750.84"
]
}
},
"lsid": {
"id": UUID("05a83af1-44e2-4152-9d7f-8b84db1451fb")
},
"$db": "first"
},
"keysExamined": NumberInt("0"),
"docsExamined": NumberInt("100000"),
"cursorExhausted": true,
"numYield": NumberInt("781"),
"nreturned": NumberInt("2"),
"queryHash": "CD2B9A1D",
"planCacheKey": "CD2B9A1D",
"locks": {
"ParallelBatchWriterMode": {
"acquireCount": {
"r": NumberLong("1")
}
},
"ReplicationStateTransition": {
"acquireCount": {
"w": NumberLong("783")
}
},
"Global": {
"acquireCount": {
"r": NumberLong("783")
}
},
"Database": {
"acquireCount": {
"r": NumberLong("782")
}
},
"Collection": {
"acquireCount": {
"r": NumberLong("782")
}
},
"Mutex": {
"acquireCount": {
"r": NumberLong("1")
}
}
},
"flowControl": { },
"storage": { },
"responseLength": NumberInt("228"),
"protocol": "op_msg",
"millis": NumberInt("113"),
"planSummary": "COLLSCAN",
"execStats": {
"stage": "COLLSCAN",
"filter": {
"salary": {
"$in": [
"1253.74",
"6750.84"
]
}
},
"nReturned": NumberInt("2"),
"executionTimeMillisEstimate": NumberInt("3"),
"works": NumberInt("100002"),
"advanced": NumberInt("2"),
"needTime": NumberInt("99999"),
"needYield": NumberInt("0"),
"saveState": NumberInt("781"),
"restoreState": NumberInt("781"),
"isEOF": NumberInt("1"),
"direction": "forward",
"docsExamined": NumberInt("100000")
},
"ts": ISODate("2021-04-10T14:43:18.045Z"),
"client": "127.0.0.1",
"appName": "MongoDB Shell",
"allUsers": [ ],
"user": ""
}
5.3 备份恢复
MongoDB数据备份
在Mongodb中我们使用mongodump命令来备份MongoDB数据。该命令可以导出所有数据到指定目录中。
mongodump命令可以通过参数指定导出的数据量级转存的服务器。
语法
mongodump命令脚本语法如下:
>mongodump -h dbhost -d dbname -o dbdirectory
-h:
MongDB所在服务器地址,例如:127.0.0.1,当然也可以指定端口号:127.0.0.1:27017-d:
需要备份的数据库实例,例如:test-o:
备份的数据存放位置,例如:c:\data\dump,当然该目录需要提前建立,在备份完成后,系统自动在dump目录下建立一个test目录,这个目录里面存放该数据库实例的备份数据。
实例
在本地使用 27017 启动你的mongod服务。打开命令提示符窗口,进入MongoDB安装目录的bin目录输入命令mongodump:
>mongodump
执行以上命令后,客户端会连接到ip为 127.0.0.1 端口号为 27017 的MongoDB服务上,并备份所有数据到 bin/dump/ 目录中。
语法 | 描述 | 实例 |
---|---|---|
mongodump —host HOST_NAME —port PORT_NUMBER | 该命令将备份所有MongoDB数据 | mongodump —host runoob.com —port 27017 |
mongodump —dbpath DB_PATH —out BACKUP_DIRECTORY | mongodump —dbpath /data/db/ —out /data/backup/ | |
mongodump —collection COLLECTION —db DB_NAME | 该命令将备份指定数据库的集合。 | mongodump —collection mycol —db test |
.bson为备份数据文件,.metadata.json为集合元数据文件
MongoDB数据恢复
mongodb使用 mongorestore 命令来恢复备份的数据。
语法
mongorestore命令脚本语法如下:
>mongorestore -h <hostname><:port> -d dbname <path>
—host <:port>, -h <:port>:
MongoDB所在服务器地址,默认为: localhost:27017—db , -d :
需要恢复的数据库实例,例如:test,当然这个名称也可以和备份时候的不一样,比如test2—drop:
恢复的时候,先删除当前数据,然后恢复备份的数据。就是说,恢复后,备份后添加修改的数据都会被删除,慎用哦!:
mongorestore 最后的一个参数,设置备份数据所在位置,例如:c:\data\dump\test。
你不能同时指定 和 —dir 选项,—dir也可以设置备份目录。—dir:
指定备份的目录
你不能同时指定 和 —dir 选项。
接下来我们执行以下命令:
>mongorestore
5.4 原子操作常用命令
在执行更新操作时可以使用以下指令
$set
用来指定一个键并更新键值,若键不存在并创建。
{ $set : { field : value } }
$unset
用来删除一个键。
{ $unset : { field : 1} }
$inc
$inc可以对文档的某个值为数字型(只能为满足要求的数字)的键进行增减的操作。
{ $inc : { field : value } }
以下都是用在update,updateOne,updateMany中
$push
用法:
{ $push : { field : value } }
把value追加到field里面去,field一定要是数组类型才行,如果field不存在,会新增一个数组类型加进去。
db.col2.updateMany({},{$push:{"favorites":"跳舞"}})
$pushAll
同$push,只是一次可以追加多个值到一个数组字段内。
{ $pushAll : { field : value_array } }
$pull
从数组field内删除一个等于value值。
{ $pull : { field : _value } }
$addToSet
增加一个值到数组内,而且只有当这个值不在数组内才增加。
$pop
删除数组的第一个或最后一个元素
{ $pop : { field : 1 } }
$rename
修改字段名称
{ $rename : { old_field_name : new_field_name } }
$bit
位操作,integer类型
{$bit : { field : {and : 5}}}
5.5 高级索引
MongoDB可以针对数组,和嵌入文档创建索引,我们可以看执行计划有没有使用索引。
在数组中创建索引,需要对数组中的每个字段依次建立索引。所以在我们为数组 tags 创建索引时,会为 music、cricket、blogs三个值建立单独的索引。
嵌入文档索引我们可以针对嵌入文档的某个字段创建索引:
db.user.createIndex({“address.city”:1})
我们有以下数据: user表有数据,有嵌入文档,我们针对favorites和address创建索引。
{
"_id" : 2,
"name" : "张三",
"age" : 52,
"favorites" : [
"羽毛球",
"看小说"
]
}
{
"_id" : 3,
"name" : "王五",
"age" : 28,
"favorites" : [
"打篮球",
"看小说"
]
}
{
"_id" : 4,
"name" : "李四",
"age" : 26,
"favorites" : [
"喝茶",
"跳舞"
],
"address" : {
"city" : "上海",
"region" : "张江高科"
}
}
{
"_id" : 6,
"name" : "王豪",
"age" : 27,
"favorites" : "",
"address" : {
"city" : "北京",
"region" : "海定区"
}
}
{ "_id" : 7, "name" : "hayo", "age" : 23 }
db.user.createIndex({"address.city":1})
db.user.createIndex({favorites:1})
然后查看下文档索引:
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "first.user"
},
{
"v" : 2,
"key" : {
"name" : 1
},
"name" : "name_1",
"ns" : "first.user"
},
{
"v" : 2,
"key" : {
"favorites" : 1
},
"name" : "favorites_1",
"ns" : "first.user"
}
]
> db.user.createIndex({"address.city":1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 3,
"numIndexesAfter" : 4,
"ok" : 1
}
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "first.user"
},
{
"v" : 2,
"key" : {
"name" : 1
},
"name" : "name_1",
"ns" : "first.user"
},
{
"v" : 2,
"key" : {
"favorites" : 1
},
"name" : "favorites_1",
"ns" : "first.user"
},
{
"v" : 2,
"key" : {
"address.city" : 1
},
"name" : "address.city_1",
"ns" : "first.user"
}
]
db.user.find({favorites:'看小说'}).pretty()
> db.user.find({favorites:'看小说'}).pretty()
{
"_id" : 2,
"name" : "张三",
"age" : 52,
"favorites" : [
"羽毛球",
"看小说"
]
}
{
"_id" : 3,
"name" : "王五",
"age" : 28,
"favorites" : [
"打篮球",
"看小说"
]
}
>
db.user.find({favorites:'看小说'}).explain()
执行查询计划看到使用了索引。
使用address查询:
db.user.find({"address.city":"北京"}).pretty() address.city一定要加引号
db.user.find({"address.city":"北京"}).explain()
5.6 索引限制
额外开销
每个索引占据一定的存储空间,在进行插入,更新和删除操作时也需要对索引进行操作。所以,如果你很少对集合进行读取操作,建议不使用索引。
内存(RAM)使用
由于索引是存储在内存(RAM)中,你应该确保该索引的大小不超过内存的限制。
如果索引的大小大于内存的限制,MongoDB会删除一些索引,这将导致性能下降。
查询限制
索引不能被以下的查询使用:
- 正则表达式及非操作符,如
$nin
,$not
, 等。 - 算术运算符,如
$mod
, 等。 $where
子句
所以,检测你的语句是否使用索引是一个好的习惯,可以用explain来查看。
索引键限制
从2.6版本开始,如果现有的索引字段的值超过索引键的限制,MongoDB中不会创建索引。
插入文档超过索引键限制
如果文档的索引字段值超过了索引键的限制,MongoDB不会将任何文档转换成索引的集合。与mongorestore和mongoimport工具类似。
最大范围
- 集合中索引个数不能超过64个
- 索引名的长度不能超过128个字符
- 一个复合索引最多可以有31个字段
5.7 索引实现原理
MongoDB 是文档型的数据库,它使用BSON 格式保存数据,比关系型数据库存储更方便。比如之前关系型数据库中处理用户、订单等数据要建立对应的表,还要建立它们之间的关联关系。但是BSON就不 一样了,我们可以把一条数据和这条数据对应的数据都存入一个BSON对象中,这种形式更简单,通俗易 懂。
MySql是关系型数据库,数据的关联性是非常强的,区间访问是常见的一种情况,底层索引组织数据使用B+树,B+树由于数据全部存储在叶子节点,并且通过指针串在一起,这样就很容易的进行区间 遍历甚至全部遍历。
MongoDB使用B树,所有节点都有Data域,只要找到指定索引就可以进行访问, 单次查询从结构上来看要快于MySql。
B树是一种自平衡的搜索树:
(1) 多路 非二叉树 (2) 每个节点 既保存数据 又保存索引 (3) 搜索时 相当于二分查找
B+树是B树的变种:
(1) 多路非二叉 (2) 只有叶子节点保存数据 (3) 搜索时 也相当于二分查找 (4) 增加了 相邻节点指针
从上面我们可以看出最核心的区别主要有俩,一个是数据的保存位置,一个是相邻节点的指向。就是这俩造成了MongoDB和MySql的差别。
(1)B+树相邻接点的指针可以大大增加区间访问性,可使用在范围查询等,而B树每个节点 key 和 data 在一起 适合随机读写 ,而区间查找效率很差。
(2)B+树更适合外部存储,也就是磁盘存储,使用B树结构结构的话,每次磁盘预读中的很多数据是用不上的数据。因此,它没能利用好磁盘预读的提供的数据。由于节点内无 data 域,每个节点能索引的范围更大更精确。
(3)注意这个区别相当重要,是基于(1)(2)的,B树每个节点即保存数据又保存索引,树的深度小,所以磁盘IO的次数很少,B+树只有叶子节点保存,较B树而言深度大磁盘IO多,但是区间访问比较好。
6 Mongo Java
6.1 java
1 引入依赖
<!-- https://mvnrepository.com/artifact/org.mongodb/mongo-java-driver -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>3.12.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
2 example
public class PrimaryLearn {
private MongoDatabase mongoDatabase;
private MongoClient mongoClient;
@Before
public void createConnection() {
// Enable MongoDB logging in general
System.setProperty("org.slf4j.simpleLogger.log.org.mongodb.driver", "info");
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
//连接到mongodb服务
MongoClient mongoClient = new MongoClient("127.0.0.1", 27017);
mongoClient.watch();
this.mongoClient = mongoClient;
MongoDatabase mongoDatabase = mongoClient.getDatabase("first");
this.mongoDatabase = mongoDatabase;
}
@After
public void closeConnection() {
if (this.mongoClient != null) {
mongoClient.close();
}
}
@Test
public void generalQuery() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
//mongoDatabase.createCollection("tc1");
MongoCollection<Document> cmg = mongoDatabase.getCollection("cmg");
//另起一个线程监控对几个cmg的操作
Thread t = new Thread(() -> {
MongoCursor<ChangeStreamDocument<Document>> d = cmg.watch().iterator();
while (d.hasNext()) {
ChangeStreamDocument<Document> cd = d.next();
System.out.println("集合cmg操作记录:" + cd.toString());
}
});
t.start();
Document document = new Document();
document.put("name", "cmg");
document.put("age", 20);
cmg.insertOne(document);
BasicDBObject where = new BasicDBObject();
//hint 为强制使用某一索引 这种方法某些情形下会提升性能
//cmg.find().hint();
//where = new BasicDBObject("age", new BasicDBObject("$gt", 17)).append("name", "陈梦鸽");
//在find后面加filter是根据filter条件去查询 ,而不是先查询出来再根据查询条件去筛选的
FindIterable<Document> documents = cmg.find(where).filter(new BasicDBObject("name", "陈梦鸽")).limit(-1).sort(new BasicDBObject("age", -1));
MongoCursor<Document> iterator = documents.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().toJson());
}
countDownLatch.await();
}
//全文检索
@Test
public void fullTextSearch(){
MongoCollection<Document> cmg = mongoDatabase.getCollection("cmg");
BasicDBObject basicDBObject = new BasicDBObject("$text",new BasicDBObject("$search","cmg"));
FindIterable<Document> documents = cmg.find(basicDBObject);
MongoCursor<Document> i = documents.iterator();
while (i.hasNext()) {
System.out.println(i.next());
}
}
/**
* 复合查询
*/
@Test
public void aggregateQuery() {
MongoCollection<Document> cmg = mongoDatabase.getCollection("cmg");
BsonDocument bson = new BsonDocument();
//group
bson.append("$group", new BsonDocument("_id", new BsonString("$name")).append("sum", new BsonDocument("$sum", new BsonString("$age"))));
AggregateIterable<Document> aggregate = cmg.aggregate(Arrays.asList(bson));
MongoCursor<Document> i = aggregate.iterator();
while (i.hasNext()) {
System.out.println(i.next());
}
System.out.println("######-----------------######");
//match
BasicDBObject basicDBObject = new BasicDBObject("$match", new BasicDBObject("age", new BasicDBObject("$gte", 18)));
aggregate = cmg.aggregate(Arrays.asList(basicDBObject));
i = aggregate.iterator();
while (i.hasNext()) {
System.out.println(i.next());
}
System.out.println("######------------------######");
//$project
basicDBObject = new BasicDBObject("$project", new BasicDBObject("title", "haha").append("add", "拉拉").append("_id", 0));
aggregate = cmg.aggregate(Arrays.asList(basicDBObject));
i = aggregate.iterator();
while (i.hasNext()) {
System.out.println(i.next());
}
System.out.println("######------------------######");
//$sort 相当于 order by name desc,age desc
basicDBObject = new BasicDBObject("$sort", new BasicDBObject("name", 1).append("age", -1));
aggregate = cmg.aggregate(Arrays.asList(basicDBObject));
i = aggregate.iterator();
while (i.hasNext()) {
System.out.println(i.next());
}
}
@Test
public void FiltersTest(){
MongoCollection<Document> user = mongoDatabase.getCollection("user");
Bson bson = Filters.eq("name", "张三");
FindIterable<Document> documents = user.find(bson);
for (Document document : documents) {
System.out.println(document);
}
}
@Test
public void multiConditionFind(){
MongoCollection<Document> user = mongoDatabase.getCollection("user");
Document condition = new Document();
condition.put("name","张三");
condition.put("age",34);
FindIterable<Document> documents = user.find(condition);
for (Document document : documents) {
System.out.println(document);
}
}
6.2 Spring 使用Mongo
1 引入依赖
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>${spring.version}</version>
</dependency>
2 配置文件 xml配置
也可以使用@Configuration配置类进行实现
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mongo="http://www.springframework.org/schema/data/mongo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/data/mongo
http://www.springframework.org/schema/data/mongo/spring-mongo.xsd">
<!-- 构建MongoDb工厂对象 -->
<mongo:db-factory id="mongoDbFactory"
client-uri="mongodb://192.168.211.133:37017/lg_resume">
</mongo:db-factory>
<!-- 构建 MongoTemplate 类型的对象 -->
<bean id="mongoTemplate"
class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg index="0" ref="mongoDbFactory"></constructor-arg>
</bean>
<!-- 开启组件扫描 -->
<context:component-scan base-package="com.lagou"></context:component-scan>
</beans>
3 哪里使用Mongo就可以注入MongoTemplate进行操作
@Autowired
protected MongoTemplate mongoTemplate;
spring boot只需要在配置文件中配置mongo数据库的地址信息,直接注入MongoTemplate就可以。
4 使用MongoTemplate
public class MongoLoginInfoRepository implements SessionRepository<LoginInfoBase, String> {
@Autowired
private ApplicationConfigProperties applicationConfigProperties;
@Autowired
private MongoTemplate mongoTemplate;
/**
* 保存信息
*
* @param loginInfoBase
* @return
*/
@Override
public void save(LoginInfoBase loginInfoBase) {
//生成sessionId
loginInfoBase.setSessionId(UUID.randomUUID().toString());
//设置过期时间
loginInfoBase.setExpirationTime(LocalDateTime.now().plusSeconds(applicationConfigProperties.getSessionTimeOut().getSeconds()));
mongoTemplate.save(loginInfoBase);
}
/**
* 刷新超时时间
*
* @param sessionId
*/
@Override
public void refresh(String sessionId) {
Query query = new Query(Criteria.where("_id").is(sessionId));
Update update = new Update();
//更新过期时间
update.set("expiration_time", LocalDateTime.now().plusSeconds(applicationConfigProperties.getSessionTimeOut().getSeconds()));
mongoTemplate.updateFirst(query, update, getDocument());
}
/**
* 通过id获取信息
*
* @param sessionId
* @return
*/
@Override
public LoginInfoBase get(String sessionId) {
Query query = new Query(Criteria.where("_id").is(sessionId));
return mongoTemplate.findOne(query, LoginInfoBase.class, getDocument());
}
/**
* 删除session
*
* @param sessionId
*/
@Override
public void remove(String sessionId) {
Query query = new Query(Criteria.where("_id").is(sessionId));
//其实调用的是deleteOne或者deleteMany方法
mongoTemplate.remove(query, getDocument());
}
private String getDocument() {
Document document = LoginInfoBase.class.getDeclaredAnnotation(Document.class);
String collectionName = document.value();
if (StringUtil.isEmpty(collectionName)) {
collectionName = document.collection();
if (StringUtil.isEmpty(collectionName)) {
collectionName = MongoCollectionName.LOGIN_INFO;
}
}
return collectionName;
}
/**
* 更新缓存信息
*
* @param loginInfoBase
*/
@Override
public boolean updateInfo(LoginInfoBase loginInfoBase) {
if (loginInfoBase == null || StringUtil.isEmpty(loginInfoBase.getSessionId())) {
return false;
}
Query query = new Query(Criteria.where("_id").is(loginInfoBase.getSessionId()));
Update update = new Update();
String realName = loginInfoBase.getRealName();
String email = loginInfoBase.getEmail();
String pwd = loginInfoBase.getPwd();
String headImg = loginInfoBase.getHeadImg();
String telNo = loginInfoBase.getTelNo();
boolean doUpdate = false;
if (StringUtil.isNotEmpty(realName)) {
update.set("real_name", realName);
doUpdate = true;
}
if (StringUtil.isNotEmpty(headImg)) {
update.set("head_img", headImg);
doUpdate = true;
}
if (StringUtil.isNotEmpty(telNo)) {
update.set("tel_no", telNo);
doUpdate = true;
}
if (StringUtil.isNotEmpty(pwd)) {
update.set("pwd", pwd);
doUpdate = true;
}
if (StringUtil.isNotEmpty(email)) {
update.set("email", email);
doUpdate = true;
}
if (doUpdate) {
UpdateResult updateResult = mongoTemplate.updateFirst(query, update, getDocument());
return updateResult.getMatchedCount() > 0;
} else {
return true;
}
}
}
实体类
@Document(collection = MongoCollectionName.LOGIN_INFO)
public interface LoginInfoBase {
Integer getUserId();
String getSessionId();
void setSessionId(String sessionId);
String getEmail();
String getTelNo();
Integer getDepartId();
String getDepartName();
String getRealName();
String getCode();
String getHeadImg();
Integer getStatus();
String getPosition();
String getPwd();
default Integer getRoleId(){return 0;};
LocalDateTime getExpirationTime();
void setExpirationTime(LocalDateTime expirationTime);
}
public class UserInfoRespDto implements LoginInfoBase {
private Integer userId;
@Field(value = "_id")
private String sessionId;
private String pwd;
private String code;
private String realName;
private String telNo;
private String email;
private String position;
private Integer departId;
private Integer status;
private String headImg;
private String departName;
private LocalDateTime expirationTime;
private Integer canPubTopic;
private Integer canPubNotice;
private Integer roleId;
private String roleName;
也可以只查询部分字段,比如:
使用fields().include(“field”)方法。
//首先查一下点赞人
Query recordQuery = new Query(Criteria.where("_id").is(commentId));
recordQuery.fields().include("like_users").include("from_user_id").include("topic_id").include("topic_title");
//查询评论记录
UserComment userComment = mongoTemplate.findOne(recordQuery, UserComment.class);
多条件:
Query query = new Query(Criteria.where("to_user_id").is(userId));
query.addCriteria(Criteria.where("read_is").is(0));
query.fields().include("topic_id").include("topic_title")
.include("from_user_name").include("type");
query.with(Sort.by(Sort.Direction.DESC, "create_time"));
query.limit(6);
long count = mongoTemplate.count(query, UserNotifyRecord.class);
or操作:
Query query = new Query(new Criteria().orOperator(Criteria.where("ltc_id").is(commentId), Criteria.where("_id").is(commentId)));
6.3 MongoRepository 的方式
1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
2 配置文件 配置mongo服务器地址
spring:
data:
mongodb:
#host: 47.112.218.99
host: 127.0.0.1
database: hrh_social
port: 27019
field-naming-strategy: org.springframework.data.mapping.model.SnakeCaseFieldNamingStrategy
3 编写实体类 并在实体类上加@Document(“集合名”)注解
4 编写 Repository 接口 继承 MongoRepository 方法具体参考:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-m ethods.query-creation 如果内置方法不够用 就自己定义 如:定义find|read|get 等开头的方法进行查询
5 注入Repository接口,操作即可
7 MongoDB架构
7.1 逻辑结构
MongoDB 与 MySQL 中的架构相差不多,底层都使用了可插拔的存储引擎以满足用户的不同需要。
用户可以根据程序的数据特征选择不同的存储引擎,在最新版本的 MongoDB 中使用了 WiredTiger 作为默 认的存储引擎,WiredTiger 提供了不同粒度的并发控制和压缩机制,能够为不同种类的应用提供了最 好的性能和存储率。
在存储引擎上层的就是 MongoDB 的数据模型和查询语言了,由于 MongoDB 对数据的存储与 RDBMS 有较大的差异,所以它创建了一套不同的数据模型和查询语言。
7.2 数据模型
内嵌 文档嵌套
内嵌的方式指的是把相关联的数据保存在同一个文档结构之中。MongoDB的文档结构允许一个字 段或者一个数组内的值作为一个嵌套的文档。引用 类似于关系型数据库的表关联
引用方式通过存储数据引用信息来实现两个不同文档之间的关联,应用程序可以通过解析这些数据引 用来访问相关数据
如何选择呢?
选择内嵌:
- 数据对象之间有包含关系 ,一般是数据对象之间有一对多或者一对一的关系 。
- 需要经常一起读取的数据。
- 有 map-reduce/aggregation 需求的数据放在一起,这些操作都只能操作单个 collection。
选择引用:
- 当内嵌数据会导致很多数据的重复,并且读性能的优势又不足于覆盖数据重复的弊端 。
- 需要表达比较复杂的多对多关系的时候 。
- 大型层次结果数据集嵌套不要太深。
比如一个部门有很多员工,不可能将员工信息嵌入到部门信息所在文档吧。
7.3 存储引擎
存储引擎是MongoDB的核心组件,负责管理数据如何存储在硬盘和内存上。
MongoDB支持的存储引擎有 MMAPv1 ,WiredTiger和InMemory。
InMemory存储引擎用于将数据只存储在内存中,只将少量的元数据 (meta-data)和诊断日志(Diagnostic)存储到硬盘文件中,由于不需要Disk的IO操作,就能获取所需的数据,InMemory存储引擎大幅度降低了数据查询的延迟(Latency)。如果MongoDB只用来做缓存层,可以使用此种存储引擎。
从mongodb3.2开始默认的存储 引擎是WiredTiger,3.2版本之前的默认存储引擎是MMAPv1,mongodb4.x版本不再支持MMAPv1存储引 擎。
配置文件:
storage:
journal:
enabled: true
dbPath: /data/mongo/
##是否一个库一个文件夹
directoryPerDB: true
##数据引擎
engine: wiredTiger
##WT引擎配置
WiredTiger:
engineConfig:
##WT最大使用cache(根据服务器实际情况调节)
cacheSizeGB: 2
##是否将索引也按数据库名单独存储
directoryForIndexes: true
##日志压缩(默认snappy)
journalCompressor: none
##表压缩配置(默认snappy,还可选none、zlib)
collectionConfig:
#块压缩
blockCompressor: zlib
##索引配置
indexConfig:
prefixCompression: true
WiredTiger存储引擎优势
- 文档空间分配方式 WiredTiger使用的是BTree存储 MMAPV1 线性存储 需要Padding
- 并发级别 WiredTiger 文档级别锁 MMAPV1引擎使用表级锁
- 数据压缩 snappy (默认) 和 zlib ,相比MMAPV1(无压缩) 空间节省数倍。
- 内存使用 WiredTiger 可以指定内存的使用大小。
- Cache使用 WT引擎使用了二阶缓存WiredTiger Cache, File System Cache来保证Disk上的数据的最终一 致性。 而MMAPv1 只有journal 日志。
WiredTiger引擎包含的文件和作用
WiredTiger.basecfg: 存储基本配置信息,与 ConfigServer有关系
WiredTiger.lock: 定义锁操作
table.wt: 存储各张表的数据
WiredTiger.wt: 存储table 的元数据
WiredTiger.turtle: 存储WiredTiger.wt的元数据
journal: 存储WAL(Write Ahead Log) 采用预分配机制,默认分配1G
7.5 WiredTiger 存储引擎实现原理
增删改
WiredTiger的写操作会默认写入 Cache ,并持久化到 WAL (Write Ahead Log) journal日志,每60s或Log文件达到2G 做一次 checkpoint (当然我们也可以通过在写入时传入 j: true 的参数强制 journal 文件的同步 , writeConcern { w: , j: , wtimeout: }) 产生快照文件。WiredTiger初始化时,恢复至最新的快照状态,然后再根据WAL 恢复数据,保证数据的完整性。
Cache是基于BTree的,节点是一个page,root page是根节点,internal page是中间索引节点,leaf page真正存储数据,数据以page为单位读写。WiredTiger采用Copy on write的方式管理写操作 (insert、update、delete),写操作会先缓存在cache里,持久化时,写操作不会在原来的leaf page 上进行,而是写入新分配的page,每次checkpoint都会产生一个新的root page。
Cache和WAL日志都会进行的。Cache是是内存中的以Btree结构的内存页。WAL日志其实就是journal日志,类似于Mysql的redolog,WAL日志也不是直接写到磁盘,也会先写到内存的一个journal buffer中,然后后台异步的将buffer中日志记录到journal中。
checkpoint流程:
- 对所有的table进行一次checkpoint,每个table的checkpoint的元数据更新至WiredTiger.wt
- 对WiredTiger.wt进行checkpoint,将该table checkpoint的元数据更新至临时文件 WiredTiger.turtle.set
- 将WiredTiger.turtle.set重命名为WiredTiger.turtle。
- 上述过程如果中间失败,WiredTiger在下次连接初始化时,首先将数据恢复至最新的快照状态,然后根 据WAL恢复数据,以保证存储可靠性。
Journaling:
在数据库宕机时 , 为保证 MongoDB 中数据的持久性,MongoDB 使用了 Write Ahead Logging 向磁盘 上的 journal 文件预先进行写入。除了 journal 日志,MongoDB 还使用检查点(checkpoint)来保证数据的一致性,当数据库发生宕机时,我们就需要 checkpoint 和 journal 文件协作完成数据的恢复工 作。
- 在数据文件中查找上一个检查点的标识符
- 在 journal 文件中查找标识符对应的记录
- 重做对应记录之后的全部操作
8 主从复制
master-slave架构中master节点负责数据的读写,slave没有写入权限只负责读取数据。
在主从结构中,主节点的操作记录成为oplog(operation log)。oplog存储在系统数据库local的
oplog.rs集合中,这个集合的每个文档都代表主节点上执行的一个操作。从服务器会定期从主服务器
中获取oplog记录,然后在本机上执行!对于存储oplog的集合,MongoDB采用的是固定集合,也就是说随
着操作过多,新的操作会覆盖旧的操作!
主从结构没有自动故障转移功能,需要指定master和slave端,不推荐在生产中使用。
9 复制集(repliaca sets)
复制集是由一组拥有相同数据集的mongod实例做组成的集群。
复制集是一个集群,它是2台及2台以上的服务器组成,以及复制集成员包括Primary主节点,secondary从 节点 和投票节点。
复制集提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性,保证数据的安全 性。
9.1 特性
1.高可用 防止设备(服务器、网络)故障。 提供自动failover 功能。 技术来保证高可用
2.灾难恢复 当发生故障时,可以从其他节点恢复 用于备份。
3.功能隔离 我们可以在备节点上执行读操作,减少主节点的压力 比如:用于分析、报表,数据挖掘,系统任务等。
9.2 原理
一个复制集中Primary节点上能够完成读写操作,Secondary节点仅能用于读操作。Primary节点需要记录所有改变数据库状态的操作,这些记录保存在 oplog 中,这个文件存储在 local 数据库,各个Secondary 节点通过此 oplog 来复制数据并应用于本地,保持本地的数据与主节点的一致。oplog 具有幂等性,即无 论执行几次其结果一致,这个比 mysql 的二进制日志更好用。oplog在local oplog.rs中
Oplog组成:
{
"ts" : Timestamp(1446011584, 2),
"h" : NumberLong("1687359108795812092"),
"v" : 2,
"op" : "i",
"ns" : "test.nosql",
"o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb",
"score" : "10"}
}
ts:操作时间,当前timestamp + 计数器,计数器每秒都被重置
h:操作的全局唯一标识
v:oplog版本信息
op:操作类型
i:插入操作
u:更新操作
d:删除操作
c:执行命令(如createDatabase,dropDatabase)
n:空操作,特殊用途
ns:操作针对的集合
o:操作内容
o2:更新查询条件,仅update操作包含该字段
复制集数据同步分为初始化同步和keep复制同步。
- 初始化同步指全量从主节点同步数据,如果Primary 节点数据量比较大同步时间会比较长。
- 而keep复制指初始化同步过后,节点之间的实时同步一般是增量同步。
初始化同步有以下两种情况会触发: (1) Secondary第一次加入。 (2) Secondary落后的数据量超过了oplog的大小,这样也会被全量复制。
9.3 Primary节点选举
MongoDB的Primary节点选举基于心跳触发。一个复制集N个节点中的任意两个节点维持心跳,每个节点维护其他N-1个节点的状态。
心跳检测: 整个集群需要保持一定的通信才能知道哪些节点活着哪些节点挂掉。mongodb节点会向副本集中的其他节点 每2秒就会发送一次pings包,如果其他节点在10秒钟之内没有返回就标示为不能访问。每个节点内部都会 维护一个状态映射表,表明当前每个节点是什么角色、日志时间戳等关键信息。如果主节点发现自己无法与 大部分节点通讯则把自己降级为secondary只读节点。
主节点选举触发的时机:
- 第一次初始化一个复制集 Secondary节点权重比Primary节点高时,发起替换选举
- Secondary节点发现集群中没有Primary时,发起选举
- Primary节点不能访问到大部分(Majority)成员时主动降级
当触发选举时,Secondary节点尝试将自身选举为Primary。主节点选举是一个二阶段过程+多数派协议
第一阶段: 检测自身是否有被选举的资格 如果符合资格会向其它节点发起本节点是否有选举资格的 FreshnessCheck,进行同僚仲裁
第二阶段: 发起者向集群中存活节点发送Elect(选举)请求,仲裁者收到请求的节点会执行一系列合法性检查,如果检查通过,则仲裁者(一个复制集中最多50个节点 其中只有7个具有投票权)给发起者投一票。
pv0通过30秒选举锁防止一次选举中两次投票。
pv1使用了terms(一个单调递增的选举计数器)来防止在一次选举中投两次票的情况。
- 多数派协议: 发起者如果获得超过半数的投票,则选举通过,自身成为Primary节点。获得低于半数选票的原因,除了常 见的网络问题外,相同优先级的节点同时通过第一阶段的同僚仲裁并进入第二阶段也是一个原因。因此,当 选票不足时,会sleep[0,1]秒内的随机时间,之后再次尝试选举。
9.4 复制集搭建
配置文件
server-1:
storage:
dbPath: D:\develop\Mongodb\server-1\data
journal:
enabled: true
directoryPerDB: true
engine: wiredTiger
systemLog:
destination: file
path: D:\develop\Mongodb\server-1\logs\mongod.log
logAppend: true
net:
port: 32017
bindIp: 127.0.0.1
security:
authorization: disabled
replication:
replSetName: mongod_test
processManagement:
windowsService:
#windows服务展示名称
displayName: mongo-server1
注意配置文件不能添加processManagement.windowsService.serviceName配置,不然会报错,需要在执行命令的时候指定。
server-2:
storage:
dbPath: D:\develop\Mongodb\server-2\data
journal:
enabled: true
directoryPerDB: true
engine: wiredTiger
systemLog:
destination: file
path: D:\develop\Mongodb\server-2\logs\mongod.log
logAppend: true
net:
port: 32018
bindIp: 127.0.0.1
security:
authorization: disabled
replication:
replSetName: mongod_test
processManagement:
windowsService:
displayName: mongo-server2
server-3:
storage:
dbPath: D:\develop\Mongodb\server-3\data
journal:
enabled: true
directoryPerDB: true
engine: wiredTiger
systemLog:
destination: file
path: D:\develop\Mongodb\server-3\logs\mongod.log
logAppend: true
net:
port: 32019
bindIp: 127.0.0.1
security:
authorization: disabled
replication:
replSetName: mongod_test
processManagement:
windowsService:
displayName: mongo-server3
然后需要管理员权限的cmd窗口:
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\server-1\mongod.conf --install --serviceName Mongo-Server1
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\server-2\mongod.conf --install --serviceName Mongo-Server2
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\server-3\mongod.conf --install --serviceName Mongo-Server3
然后进入:32017
mongo --port 32017
执行:
priority是为了让32017选举为primary节点。优先级高的选举的时候会选举为primary节点。默认为1
var cfg ={"_id":"mongod_test",
"protocolVersion" : 1,
"members":[
{"_id":1,"host":"127.0.0.1:32017","priority":10},
{"_id":2,"host":"127.0.0.1:32018"}
]
}
rs.initiate(cfg)
查看集群状态:
rs.status()
动态增删节点: 修改集群节点操作应该在primary节点进行操作。secondary节点不能操作。
增加节点
rs.add("127.0.0.1:32019")
删除slave 节点
rs.remove("127.0.0.1:32019")
添加仲裁节点:
rs.addArb("IP:端口");
注意:
搭建集群的时候,副本集集群节点数要尽可能为奇数个,如果为偶数个应该添加一个仲裁节点。
如果集群节点数为偶数个可能会导致:
- 两个节点获取的投票数一样,导致一直无法选出primary节点。
- 脑裂,类似于zookeeper。当极端情况下可能出现网络分区的情况,两个机房各有一半的集群节点数。网络分区后可能会选举出两个primary节点,导致数据不一致。
当我们成本有限时,比如只有两个数据节点,我们就可以找一个机器作为ARBITER: 仲裁节点。仲裁节点的存在主要是为了节省成本。
而且有这样一句话:如果可能,尽可能在副本集中使用奇数个数据成员,而不要使用仲裁者。
db.col.insert( {
_id: 2,
name: "张三",
age: 34
} ,{w:3,j:true});
10 分片
分片(sharding)是MongoDB用来将大型集合水平分割到不同服务器(或者复制集)上所采用的方法。 不需要功能强大的大型计算机就可以存储更多的数据,处理更大的负载。
为啥需要扩容?
1.存储容量需求超出单机磁盘容量。
2.活跃的数据集超出单机内存容量,导致很多请求都要从磁盘读取数据,影响性能。
3.IOPS超出单个MongoDB节点的服务能力,随着数据的增长,单机实例的瓶颈会越来越明显。
4.副本集具有节点数量限制。
10.1 工作原理
分片集群由以下3个服务组成:
- Shards Server: 每个shard由一个或多个mongod进程组成,用于存储数据。
- Router Server: 数据库集群的请求入口,所有请求都通过Router(mongos)进行协调,不需要在应用程序添加一个路由选择器,Router(mongos)就是一个请求分发中心它负责把应用程序的请求转发到对应的 Shard服务器上。
- Config Server: 配置服务器。存储所有数据库元信息(路由、分片)的配置。
片键(shard key)
为了在数据集合中分配文档,MongoDB使用分片主键分割集合。
区块(chunk)
在一个shard server内部,MongoDB还是会把数据分为chunks,每个chunk代表这个shard server内部一部分数据。MongoDB分割分片数据到区块,每一个区块包含基于分片主键的左闭右开的 区间范围。
分片策略
范围分片(ranged) 1
范围分片是基于分片主键的值切分数据,每一个区块将会分配到一个范围。
范围分片适合满足在一定范围内的查找,例如查找X的值在[20,30)之间的数据,mongo 路由根据 Config server中存储的元数据,可以直接定位到指定的shard的Chunk中。
缺点: 如果shard key有明显递增(或者递减)趋势,则新插入的文档多会分布到同一个chunk,无 法扩展写的能力。hash分片(hashed)
Hash分片是计算一个分片主键的hash值,每一个区块将分配一个范围的hash值。
Hash分片与范围分片互补,能将文档随机的分散到各个chunk,充分的扩展写能力,弥补了范围 分片的不足
缺点:不能高效的服务范围查询,所有的范围查询要分发到后端所有的Shard才能找满足条件的文档。组合片键 A + B(散列思想 不能是直接hash)
数据库中没有比较合适的片键供选择,或者是打算使用的片键基数太小(即变化少如星期只有7天 可变化),可以选另一个字段使用组合片键,甚至可以添加冗余字段来组合。一般是粗粒度+细粒 度进行组合。
实际使用时需要合理的选择shard key:从两个方面考虑,数据的查询和写入,最好的效果就是数据查询时能命中更少的分片,数据写入时 能够随机的写入每个分片,关键在于如何权衡性能和负载。
10.2 分片环境搭建
参考博客:【https://www.cnblogs.com/leohahah/p/8652572.html】
分片和配置服务都必须使用集群搭建。我这里试了如果使用单实例config节点是无法启动的,必须以replset模式启动。而单实例的分片不能addShard。
一个路由节点router server,2个分片shard,两个配置服务config server。
第一步:配置启动分片集群
首先启动6个示例,分为2个分片集群。
shard1:42018,42019,42020
配置文件:这里只展示其中一个的,其他修改path和端口,displayName就可以了。
storage:
dbPath: D:\develop\Mongodb\mongo-shard\shard-1\data\42018
journal:
enabled: true
directoryPerDB: true
engine: wiredTiger
systemLog:
destination: file
path: D:\develop\Mongodb\mongo-shard\shard-1\mongod-42018.log
logAppend: true
net:
port: 42018
bindIp: 127.0.0.1
security:
authorization: disabled
replication:
replSetName: shard1
sharding:
clusterRole: shardsvr
processManagement:
windowsService:
displayName: mongo-shardsvr-18
_id一定要和replication.replSetName对应
shard2:42031,42032,42033
配置文件:这里只展示其中一个的,其他修改path和端口,displayName就可以了。
storage:
dbPath: D:\develop\Mongodb\mongo-shard\shard-2\data\42031
journal:
enabled: true
directoryPerDB: true
engine: wiredTiger
systemLog:
destination: file
path: D:\develop\Mongodb\mongo-shard\shard-2\mongod-42031.log
logAppend: true
net:
port: 42031
bindIp: 127.0.0.1
security:
authorization: disabled
replication:
replSetName: shard2
sharding:
clusterRole: shardsvr
processManagement:
windowsService:
displayName: mongo-shardsvr-31
然后安装 mongo服务,注意windows这里要以管理员权限打开命令行窗口
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\mongo-shard\shard-1\mongod-42018.conf --install --serviceName Mongo-Shardsvr-18
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\mongo-shard\shard-1\mongod-42019.conf --install --serviceName Mongo-Shardsvr-19
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\mongo-shard\shard-1\mongod-42020.conf --install --serviceName Mongo-Shardsvr-20
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\mongo-shard\shard-2\mongod-42031.conf --install --serviceName Mongo-Shardsvr-31
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\mongo-shard\shard-2\mongod-42032.conf --install --serviceName Mongo-Shardsvr-32
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\mongo-shard\shard-2\mongod-42033.conf --install --serviceName Mongo-Shardsvr-33
然后通过net start serviceName启动服务
shard1
>mongo --port 42018
>var cfg ={"_id":"shard1",
"protocolVersion" : 1,
"members":[
{"_id":1,"host":"127.0.0.1:42018","priority":10},
{"_id":2,"host":"127.0.0.1:42019","priority":5},
{"_id":3,"host":"127.0.0.1:42020"}
]
}
>rs.initiate(cfg)
shard2
>mongo --port 42031
>var cfg ={"_id":"shard2",
"protocolVersion" : 1,
"members":[
{"_id":1,"host":"127.0.0.1:42031","priority":10},
{"_id":2,"host":"127.0.0.1:42032","priority":5},
{"_id":3,"host":"127.0.0.1:42033"}
]
}
>rs.initiate(cfg)
第二步: 配置路由节点的配置节点集群
配置文件:另一个改一下path和端口displayName就可以了。
storage:
dbPath: D:\develop\Mongodb\mongo-shard\conf-server-1\data
journal:
enabled: true
directoryPerDB: true
engine: wiredTiger
systemLog:
destination: file
path: D:\develop\Mongodb\mongo-shard\conf-server-1\logs\mongod.log
logAppend: true
net:
port: 42021
bindIp: 127.0.0.1
security:
authorization: disabled
sharding:
clusterRole: configsvr
replication:
replSetName: configsvr
processManagement:
windowsService:
displayName: mongo-shard-confsvr-1
然后安装服务:
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\mongo-shard\conf-server-1\mongod.conf --install --serviceName Mongo-Shard-Confsvr-1
C:\WINDOWS\system32>mongod --config=D:\develop\Mongodb\mongo-shard\conf-server-2\mongod.conf --install --serviceName Mongo-Shard-Confsvr-2
启动:net start serviceNam
注意这里必须 use admin切换到admin数据库
>mongo --port 42021
>use admin
>var cfg ={"_id":"configsvr",
"protocolVersion" : 1,
"members":[
{"_id":1,"host":"127.0.0.1:42021","priority":10},
{"_id":2,"host":"127.0.0.1:42022"}
]
}
>rs.initiate(cfg)
第三步:配置路由节点并启动
配置文件:注意这里不需要配置dataPath以及security
systemLog:
destination: file
path: D:\develop\Mongodb\mongo-shard\route-server\logs\mongod.log
logAppend: true
net:
port: 42017
bindIp: 127.0.0.1
sharding:
configDB: configsvr/127.0.0.1:42021,127.0.0.1:42022
processManagement:
windowsService:
displayName: mongo-shard-routesvr
安装服务:这里用的是mongos命令
mongos --config=D:\develop\Mongodb\mongo-shard\route-server\mongod.conf --install --serviceName Mongo-Shard-Routesvr
启动:net start serviceName
然后添加分片集群
>mongo --port 42017
>sh.addShard("shard1/127.0.0.1:42018,127.0.0.1:42019,127.0.0.1:42020")
>sh.addShard("shard2/127.0.0.1:42031,127.0.0.1:42032,127.0.0.1:42033")
即使shardCollectioin()指定的集合不存在,在执行的时候会自动创建集合。
mongos> sh.enableSharding("test")
{
"ok" : 1,
"operationTime" : Timestamp(1602236727, 5),
"$clusterTime" : {
"clusterTime" : Timestamp(1602236727, 5),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
mongos> sh.shardCollection("test.student",{"name":"hashed"})
{
"collectionsharded" : "test.student",
"collectionUUID" : UUID("760930e0-e6be-498a-ac68-c2ea8c01c470"),
"ok" : 1,
"operationTime" : Timestamp(1602236798, 55),
"$clusterTime" : {
"clusterTime" : Timestamp(1602236798, 55),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
第四步:测试
插入数据
for(var i=1;i<=50;i++){
db.student.insert({id:i,name:i+"_zhangsan",age:1000%25+1,school:"WTU"})
}
在47018节点查询:
shard1:PRIMARY> use test
switched to db test
shard1:PRIMARY> db.student.find()
{ "_id" : ObjectId("5f8034021e50d244470141c3"), "id" : 2, "name" : "2_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141c4"), "id" : 3, "name" : "3_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141ca"), "id" : 9, "name" : "9_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141cd"), "id" : 12, "name" : "12_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141d0"), "id" : 15, "name" : "15_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141d2"), "id" : 17, "name" : "17_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141d3"), "id" : 18, "name" : "18_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141d5"), "id" : 20, "name" : "20_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141d6"), "id" : 21, "name" : "21_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141d8"), "id" : 23, "name" : "23_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141d9"), "id" : 24, "name" : "24_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141dc"), "id" : 27, "name" : "27_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141e0"), "id" : 31, "name" : "31_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141e2"), "id" : 33, "name" : "33_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141e3"), "id" : 34, "name" : "34_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141e4"), "id" : 35, "name" : "35_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141e6"), "id" : 37, "name" : "37_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141e7"), "id" : 38, "name" : "38_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141e9"), "id" : 40, "name" : "40_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141ea"), "id" : 41, "name" : "41_zhangsan", "age" : 1, "school" : "WTU" }
Type "it" for more
shard1:PRIMARY> it
{ "_id" : ObjectId("5f8034021e50d244470141ed"), "id" : 44, "name" : "44_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141ee"), "id" : 45, "name" : "45_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141f0"), "id" : 47, "name" : "47_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141f1"), "id" : 48, "name" : "48_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141f3"), "id" : 50, "name" : "50_zhangsan", "age" : 1, "school" : "WTU" }
shard1:PRIMARY>
47031节点查询
shard2:PRIMARY> use test
switched to db test
shard2:PRIMARY> db.student.find()
{ "_id" : ObjectId("5f8034021e50d244470141c2"), "id" : 1, "name" : "1_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141c5"), "id" : 4, "name" : "4_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141c6"), "id" : 5, "name" : "5_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141c7"), "id" : 6, "name" : "6_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141c8"), "id" : 7, "name" : "7_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141c9"), "id" : 8, "name" : "8_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141cb"), "id" : 10, "name" : "10_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141cc"), "id" : 11, "name" : "11_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141ce"), "id" : 13, "name" : "13_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141cf"), "id" : 14, "name" : "14_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141d1"), "id" : 16, "name" : "16_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141d4"), "id" : 19, "name" : "19_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141d7"), "id" : 22, "name" : "22_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141da"), "id" : 25, "name" : "25_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141db"), "id" : 26, "name" : "26_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141dd"), "id" : 28, "name" : "28_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141de"), "id" : 29, "name" : "29_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141df"), "id" : 30, "name" : "30_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141e1"), "id" : 32, "name" : "32_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141e5"), "id" : 36, "name" : "36_zhangsan", "age" : 1, "school" : "WTU" }
Type "it" for more
shard2:PRIMARY> it
{ "_id" : ObjectId("5f8034021e50d244470141e8"), "id" : 39, "name" : "39_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141eb"), "id" : 42, "name" : "42_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141ec"), "id" : 43, "name" : "43_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141ef"), "id" : 46, "name" : "46_zhangsan", "age" : 1, "school" : "WTU" }
{ "_id" : ObjectId("5f8034021e50d244470141f2"), "id" : 49, "name" : "49_zhangsan", "age" : 1, "school" : "WTU" }
shard2:PRIMARY>
11 安全认证
参考博客【https://www.cnblogs.com/pl-boke/p/10063351.html】
11.1 语法
内置的角色,只在admin数据库中可用的角色必须(use admin)切换到admin数据库。
read:允许用户读取指定数据库
readWrite:允许用户读写指定数据库
dbAdmin:允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问
system.profile
userAdmin:允许用户向system.users集合写入,可以找指定数据库里创建、删除和管理用户
clusterAdmin:只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限
readAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读权限
readWriteAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的读写权限
userAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的userAdmin权限
dbAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限
root:只在admin数据库中可用。超级账号,超级权限
dbOwner:库拥有者权限,即readWrite、dbAdmin、userAdmin角色的合体
各类型用户对应的角色:
数据库用户角色:read、readWrite
数据库管理角色:dbAdmin、dbOwner、userAdmin
集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManager
备份恢复角色:backup、restore;
所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、
dbAdminAnyDatabase
超级用户角色:root
这里还有几个角色间接或直接提供了系统超级用户的访问(dbOwner 、userAdmin、
userAdminAnyDatabase)
创建用户:
use dbName
db.createUser(
{
user: "账号",
pwd: "密码",
roles: [
{ role: "角色", db: "安全认证的数据库" },
{ role: "角色", db: "安全认证的数据库" }
]
}
)
db.createUser(
{
user:"root",
pwd:"123321",
roles:[{role:"root",db:"admin"}]
}
)
修改密码: db.changeUserPassword( ‘root’ , ‘rootNew’ )
用户添加角色:db.grantRolesToUser( ‘用户名’ , [{ role: ‘角色名’ , db: ‘数据库名’}])
删除用户:db.dropUser(‘用户名’) ,也可以到admin库的system.users集合直接进行操作。
数据库认证:db.auth(‘user’,’pwd’)
11.2 单机安全认证
在创建用户之前,mongodb服务应该以无需验证方式启动。在创建成功之后,再以验证的方式重启。
至少要有一个管理员角色,可以对数据库进行管理。比如创建用户,分配权限等操作。可以分配一个
userAdminAnyDatabase:只在admin数据库中可用,赋予用户所有数据库的userAdmin权限的角色。
创建管理员:
>use admin
switched to db admin
> db
admin
> db.createUser(
{
user:"root",
pwd:"root",
roles:[{role:"root",db:"admin"}]
})
创建普通用户:
必须切换到对应的数据库才有效。这是一个坑。
>use mydb1
switched to db mydb1
> db
mydb1
> db.createUser({
... user:"zhangsan",
... pwd:"123456",
... roles:[{role:"readWrite",db:"mydb1"}]
... })
> db.createUser({
... user:"lisi",
... pwd:"123456",
... roles:[{role:"read",db:"mydb1"}]
... })
然后我们关闭服务:
> use admin
switched to db admin
> db.shutdownServer()
server should be down...
修改配置文件:
security.authorization设为enabled
security:
authorization: enabled
然后重启服务即可。
11.3 集群安全验证
路由节点:
因为连接分片集群的时候,都是连接的路由节点,路由节点的用户创建直接使用11.2单机安全认证的方式就可以。
但是注意路由配置文件不能有下面的配置,不然无法启动
security:
authorization: enabled
配置节点集群、分片集群:
关闭配置节点和分片节点。
如果在linux一台机器上搭建的可以
安装psmisc
yum install psmisc
安装完之后可以使用killall 命令 快速关闭多个进程
killall mongod
不然一个个kill,很麻烦。实际使用的时候肯定是多台机器,那就只能一个个关闭了。
我们这里使用keyFile方式进行验证,因为x509配置有点麻烦。
openssl
生成复杂的伪随机1024字符串,用作共享密码。然后,它chmod
用于更改文件权限,以仅为文件所有者提供读取权限:unix系统需要chmod 400 file,windows不需要
openssl rand -base64 756 > <密钥文件路径>
chmod 400 <密钥文件路径>
然后在配置节点,分片节点
配置文件加入:
security:
authorization: enabled
keyFile: D:\develop\Mongodb\mongo-shard\auth.file
clusterAuthMode: keyFile
然后重启。
路由节点配置文件加入:
security:
keyFile: D:\develop\Mongodb\mongo-shard\auth.file
clusterAuthMode: keyFile
需要注意我们使用java客户端连接mongo的时候,使用root账号是没权限访问的。需要用访问数据库的账号。
作业
windows10 环境。需要先安装为系统服务。
mongodb 4.2.8。
三个分片集群,每个集群3个数据节点,1个仲裁节点。
一个路由配置集群。三个数据节点。
一个路由节点。
需要注意的是,因为需要创建账号。所以分片集群,配置集群和路由节点的security节点先不要配置security属性。路由节点添加用户之后再配置security属性,然后重启。
第一步 分片集群搭建
分片集群1:shard1 仲裁节点[42020] 数据节点[42017,42018,42019]
分片集群2:shard2 仲裁节点[42034] 数据节点[42031,42032,42033]
分片集群3:shard3 仲裁节点[42044] 数据节点[42041,42042,42043]
节点配置文件:因为用的是4以上版本,所以配置文件为yaml格式。
这里只展示一个,其他都是修改dbPath和logPath以及端口,服务展示名就可以了
storage:
dbPath: D:\develop\Mongodb\mongo-shard\shard-1\data\42018
journal:
enabled: true
directoryPerDB: true
engine: wiredTiger
systemLog:
destination: file
path: D:\develop\Mongodb\mongo-shard\shard-1\mongod-42018.log
logAppend: true
net:
port: 42018
bindIp: 127.0.0.1
security:
#是否开启认证
authorization: enabled
#认证文件
keyFile: D:\develop\Mongodb\mongo-shard\auth.file
#集群认证模式
clusterAuthMode: keyFile
replication:
#集群名
replSetName: shard1
sharding:
#集群角色 shardsvr分片集群 configsvr配置集群
clusterRole: shardsvr
processManagement:
windowsService:
#服务显示名称
displayName: mongo-shardsvr-18
然后安装 mongo服务,注意windows这里要以管理员权限打开命令行窗口.
可以将这些命令放到.bat文件里面执行。
mongod --config=D:\develop\Mongodb\mongo-shard\shard-1\mongod-42017.conf --install --serviceName Mongo-Shardsvr-17
mongod --config=D:\develop\Mongodb\mongo-shard\shard-1\mongod-42018.conf --install --serviceName Mongo-Shardsvr-18
mongod --config=D:\develop\Mongodb\mongo-shard\shard-1\mongod-42019.conf --install --serviceName Mongo-Shardsvr-19
mongod --config=D:\develop\Mongodb\mongo-shard\shard-1\mongod-42020.conf --install --serviceName Mongo-Shardsvr-20
mongod --config=D:\develop\Mongodb\mongo-shard\shard-2\mongod-42031.conf --install --serviceName Mongo-Shardsvr-31
mongod --config=D:\develop\Mongodb\mongo-shard\shard-2\mongod-42032.conf --install --serviceName Mongo-Shardsvr-32
mongod --config=D:\develop\Mongodb\mongo-shard\shard-2\mongod-42033.conf --install --serviceName Mongo-Shardsvr-33
mongod --config=D:\develop\Mongodb\mongo-shard\shard-2\mongod-42034.conf --install --serviceName Mongo-Shardsvr-34
mongod --config=D:\develop\Mongodb\mongo-shard\shard-3\mongod-42041.conf --install --serviceName Mongo-Shardsvr-41
mongod --config=D:\develop\Mongodb\mongo-shard\shard-3\mongod-42042.conf --install --serviceName Mongo-Shardsvr-42
mongod --config=D:\develop\Mongodb\mongo-shard\shard-3\mongod-42043.conf --install --serviceName Mongo-Shardsvr-43
mongod --config=D:\develop\Mongodb\mongo-shard\shard-3\mongod-42044.conf --install --serviceName Mongo-Shardsvr-44
然后net start serviceName启动服务
net start Mongo-Shardsvr-17
net start Mongo-Shardsvr-18
net start Mongo-Shardsvr-19
net start Mongo-Shardsvr-20
net start Mongo-Shardsvr-31
net start Mongo-Shardsvr-32
net start Mongo-Shardsvr-33
net start Mongo-Shardsvr-34
net start Mongo-Shardsvr-41
net start Mongo-Shardsvr-42
net start Mongo-Shardsvr-43
net start Mongo-Shardsvr-44
分片集群配置,注意_id和replication.replSetName要一样。
分片集群一,连接 42017端口
>mongo --port 42017
>var cfg ={"_id":"shard1",
"protocolVersion" : 1,
"members":[
{"_id":1,"host":"127.0.0.1:42017","priority":10},
{"_id":2,"host":"127.0.0.1:42018","priority":8},
{"_id":3,"host":"127.0.0.1:42019","priority":5},
{"_id":4,"host":"127.0.0.1:42020","arbiterOnly":true}
]
}
>rs.initiate(cfg)
>rs.status()
分片集群二,连接42031节点
>mongo --port 42031
>var cfg ={"_id":"shard2",
"protocolVersion" : 1,
"members":[
{"_id":1,"host":"127.0.0.1:42031","priority":10},
{"_id":2,"host":"127.0.0.1:42032","priority":8},
{"_id":3,"host":"127.0.0.1:42033","priority":5},
{"_id":4,"host":"127.0.0.1:42034","arbiterOnly":true}
]
}
>rs.initiate(cfg)
>rs.status()
分片集群三,连接42041节点
>mongo --port 42041
>var cfg ={"_id":"shard3",
"protocolVersion" : 1,
"members":[
{"_id":1,"host":"127.0.0.1:42041","priority":10},
{"_id":2,"host":"127.0.0.1:42042","priority":8},
{"_id":3,"host":"127.0.0.1:42043","priority":5},
{"_id":4,"host":"127.0.0.1:42044","arbiterOnly":true}
]
}
>rs.initiate(cfg)
>rs.status()
至此,分片集群搭建完成
第二步 路由节点的配置节点集群
三个节点 42021,42022,42033
配置文件:另一个改一下path和端口displayName就可以了。
storage:
dbPath: D:\develop\Mongodb\mongo-shard\conf-server-2\data
journal:
enabled: true
directoryPerDB: true
engine: wiredTiger
systemLog:
destination: file
path: D:\develop\Mongodb\mongo-shard\conf-server-2\logs\mongod.log
logAppend: true
net:
port: 42022
bindIp: 127.0.0.1
security:
authorization: enabled
keyFile: D:\develop\Mongodb\mongo-shard\auth.file
clusterAuthMode: keyFile
sharding:
clusterRole: configsvr
replication:
replSetName: configsvr
processManagement:
windowsService:
displayName: mongo-shard-confsvr-2
然后安装服务:
mongod --config=D:\develop\Mongodb\mongo-shard\conf-server-1\mongod.conf --install --serviceName Mongo-Shard-Confsvr-1
mongod --config=D:\develop\Mongodb\mongo-shard\conf-server-2\mongod.conf --install --serviceName Mongo-Shard-Confsvr-2
mongod --config=D:\develop\Mongodb\mongo-shard\conf-server-3\mongod.conf --install --serviceName Mongo-Shard-Confsvr-3
然后启动服务
net start mongo-shard-confsvr-1
net start mongo-shard-confsvr-2
net start mongo-shard-confsvr-3
连接到42021
>mongo --port 42021
>use admin
>var cfg ={"_id":"configsvr",
"protocolVersion" : 1,
"members":[
{"_id":1,"host":"127.0.0.1:42021","priority":10},
{"_id":2,"host":"127.0.0.1:42022"},
{"_id":3,"host":"127.0.0.1:42023"}
]
}
>rs.initiate(cfg)
>rs.status()
第三步 配置路由节点
三个节点,37017
配置文件:
systemLog:
destination: file
path: D:\develop\Mongodb\mongo-shard\route-server\logs\mongod.log
logAppend: true
net:
port: 37017
bindIp: 127.0.0.1
security:
keyFile: D:\develop\Mongodb\mongo-shard\auth.file
clusterAuthMode: keyFile
sharding:
configDB: configsvr/127.0.0.1:42021,127.0.0.1:42022,127.0.0.1:42023
processManagement:
windowsService:
displayName: mongo-shard-routesvr
安装服务:使用的是mongos命令
mongos --config=D:\develop\Mongodb\mongo-shard\route-server\mongod.conf --install --serviceName Mongo-Shard-Routesvr
启动服务:
net start mongo-shard-routesvr
连接37017节点
在admin库中创建root账号
在lg_resume创建lagou_gx,密码abc321账号
use admin
db.createUser(
{
user:"root",
pwd:"root",
roles:[{role:"root",db:"admin"}]
})
use lg_resume
db.createUser(
{
user:"lagou_gx",
pwd:"abc321",
roles:[{role:"readWrite",db:"lg_resume"}]
})
添加分片集群:
>mongo --port 42017
>sh.addShard("shard1/127.0.0.1:42017,127.0.0.1:42018,127.0.0.1:42019,127.0.0.1:42020")
>sh.addShard("shard2/127.0.0.1:42031,127.0.0.1:42032,127.0.0.1:42033,127.0.0.1:42034")
>sh.addShard("shard3/127.0.0.1:42041,127.0.0.1:42042,127.0.0.1:42043,127.0.0.1:42044")
开启集合分片
即使shardCollectioin()指定的集合不存在,在执行的时候会自动创建集合。
mongos> sh.enableSharding("lg_resume")
{
"ok" : 1,
"operationTime" : Timestamp(1602633817, 5),
"$clusterTime" : {
"clusterTime" : Timestamp(1602633817, 5),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
mongos> sh.shardCollection("lg_resume.lg_resume_datas",{"name":"hashed"})
{
"collectionsharded" : "lg_resume.lg_resume_datas",
"collectionUUID" : UUID("f4d38f1d-68c8-408e-9087-06ed502719a1"),
"ok" : 1,
"operationTime" : Timestamp(1602633889, 48),
"$clusterTime" : {
"clusterTime" : Timestamp(1602633889, 48),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
mongos>
分片集群,配置集群和路由节点添加security属性,重启。先启动配置节点。再启动分片节点。
security:
authorization: enabled
keyFile: D:\develop\Mongodb\mongo-shard\auth.file
clusterAuthMode: keyFile
路由节点不要security.authorization属性。
springboot可以连接使用了。
13 扩展
13.1 稀疏索引 spare
稀疏索引(或者称间隙索引)就是只包含有索引字段的文档的条目,即使索引字段包含一个空值。也就是说间隙索引可以跳过那些索引键不存在的文档。因为他并非包含所有的文档,因此称为稀疏索引。与之相对的非稀疏索引或者说普通索引则包含所有的文档以及为那些不包含索引的字段存储null值。
一、间隙索引创建描述
稀疏索引(或者称间隙索引)就是只包含有索引字段的文档的条目,跳过索引键不存在的文档
本文中后面的描述使用间隙索引
创建索引的语法:
db.collection.createIndex(keys, options)
创建间隙索引示例:
db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )
这个示例,哪些不包含xmpp_id的键(列)的文档将不会被索引
间隙索引不会被使用到的情形
如果一个间隙索引会导致查询或者排序操作得到一个不完整结果集的时候,MongoDB将不会使用这个索引,hint提示除外
哪些索引缺省情况就是间隙索引
2dsphere (version 2), 2d, geoHaystack, 文本索引等总是稀疏索引
间隙索引与唯一性
一个既包含稀疏又包含唯一的索引避免集合上存在一些重复值得文档,但是允许多个文档忽略该键。
二、间隙索引示例
1、创建间隙索引
> db.version()
3.2.10
> db.scores.insertMany([
{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" },
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 },
{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }])
//下面为score键创建稀疏索引
> db.scores.createIndex( { score: 1 } , { sparse: true } )
> db.scores.find( { score: { $lt: 90 } } )
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }
//由于文档newbie并不包含score键,因此该文档不会出现在稀疏索引之中,也就不会被查询返回
> //下面查询socre小于90文档的执行计划
> db.scores.find( { score: { $lt: 90 } } ).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.scores", //Author : Leshami
"indexFilterSet" : false, //Blog : http://blog.csdn.net/leshami
"parsedQuery" : {
"score" : {
"$lt" : 90
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN", //使用到了索引扫描
"keyPattern" : {
"score" : 1
},
"indexName" : "score_1", //索引为score_1
"isMultiKey" : false,
"isUnique" : false,
"isSparse" : true, //此处表名为间隙索引
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"score" : [
"[-inf.0, 90.0)"
...........
"ok" : 1
}
2、间隙索引无法使用的示例
> db.scores.find().sort( { score: -1 } )
{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }
{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" }
//从上面的查询结果可知,基于索引列score的排序返回了所有的文档
//这个排序真实的执行计划则是全表扫描,因为索引键并不包含不存在的用户id为newbie的文档
> db.scores.find().sort( { score: -1 } ).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "test.scores",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [ ]
},
"winningPlan" : {
"stage" : "SORT",
"sortPattern" : {
"score" : -1
},
"inputStage" : {
"stage" : "SORT_KEY_GENERATOR",
"inputStage" : {
"stage" : "COLLSCAN", //使用了集合扫描方式
"filter" : {
"$and" : [ ]
},
"direction" : "forward"
}
............
"ok" : 1
}
3、强制间隙索引的示例
//如果我们强制增加一个hint提示,则用户id为newbie的文档未被返回,即走了索引(执行计划此处略)
> db.scores.find().sort( { score: -1 } ).hint( { score: 1 } )
{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }
{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }
4、间隙索引与唯一约束
在唯一索引中,唯一索引会把null当做值,也就是说为null的通常只能有一个。后面的null将无法插入。
//下面创建一个带有唯一约束的稀疏索引
> db.scores.createIndex( { score: 1 } , { sparse: true, unique: true } )
{
"ok" : 0,
"errmsg" : "Index with name: score_1 already exists with different options",
"code" : 85
}
//由于score列上已经存在一个索引了,因此提示我们,需要先删除,再创建
> db.scores.dropIndex("score_1")
{ "nIndexesWas" : 2, "ok" : 1 }
> db.scores.createIndex( { score: 1 } , { sparse: true, unique: true } )
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
//下面尝试插入一些带有score键以及不带有score键的文档,如下,可以成功插入
> db.scores.insert( { "userid": "AAAAAAA", "score": 43 } )
WriteResult({ "nInserted" : 1 })
> db.scores.insert( { "userid": "CCCCCCC" } )
WriteResult({ "nInserted" : 1 })
//下面插入一些score相关的文档,提示重复,如下示例
> db.scores.insert( { "userid": "AAAAAAA", "score": 82 } )
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.scores index: score_1 dup key: { : 82.0 }"
}
})
> db.scores.insert( { "userid": "BBBBBBB", "score": 90 } )
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.scores index: score_1 dup key: { : 90.0 }"
}
})
三、小结
a、间隙索引就是创建索引的索引列在某些文档上列不存在,导致索引存在间隙。
b、间隙索引在创建时应指定选项:{ sparse: true }
c、间隙索引列上可以指定唯一性约束
13.2 WriteConcern Read-Reference
write-concern:
write concern表示对于写操作,MongoDB在什么情况下给予客户端响应。包括下面三个字段:
{ w: , j: , wtimeout: }
w: 表示当写请求在value个MongoDB实例处理之后才向客户端返回。取值范围:
1:默认值,表示数据写入到standalone的MongoDB或者replica set的primary之后返回
0:不用写入就直接向客户端返回,性能高,但可能丢数据。不过可以配合j:True来增加数据的可持久性(durability)
1: 只有在replica set环境下才有用
‘majority’: 当数据写入到replica set的大多数节点之后向客户端返回,对于这种情况,一般是配合read-concern使用:
After the write operation returns with a
w: "majority"
acknowledgement to the client, the client can read the result of that write with a"majority"
readConcern
j:表示当写请求在写入journal之后才向客户端返回,默认为False。两点注意:
如果在对于未开启journaling的MongoDB实例使用j:True,会报错
在MongoDB3.2及之后,对于w>1, 需要所有实例都写到journal之后才返回
wtimeout:表示写入的超时时间,即在指定的时间(number),如果还不能向客户端返回(w大于1的情况),那么返回错误
默认为0,相当于没有设置该选项
在MongoDB3.4中,加入了writeConcernMajorityJournalDefault
.这么一个选项,使得w,j在不同的组合下情况下不同:
read-reference:
在前文已经讲解过,一个replica set由一个primary和多个secondary组成。primary接受写操作,因此数据一定是最新的,secondary通过oplog来同步写操作,因此数据有一定的延迟。对于时效性不是很敏感的查询业务,可以从secondary节点查询,以减轻集群的压力。
可以在com.mongodb.MongoClientOptions进行配置
MongoDB指出在不同的情况下选用不同的read-reference,非常灵活。MongoDB driver支持一下几种read-reference:
primary:默认模式,一切读操作都路由到replica set的primary节点
primaryPreferred:正常情况下都是路由到primary节点,只有当primary节点不可用(failover)的时候,才路由到secondary节点。
secondary:一切读操作都路由到replica set的secondary节点
secondaryPreferred:正常情况下都是路由到secondary节点,只有当secondary节点不可用的时候,才路由到primary节点。
nearest:从延时最小的节点读取数据,不管是primary还是secondary。对于分布式应用且MongoDB是多数据中心部署,nearest能保证最好的data locality。
如果使用secondary或者secondaryPreferred,那么需要意识到:
(1) 因为延时,读取到的数据可能不是最新的,而且不同的secondary返回的数据还可能不一样;
(2) 对于默认开启了balancer的sharded collection,由于还未结束或者异常终止的chunk迁移,secondary返回的可能是有缺失或者多余的数据
(3) 在有多个secondary节点的情况下,选择哪一个secondary节点呢,简单来说是“closest”即平均延时最小的节点,具体参加Server Selection Algorithm
read-concern:
read concern是在MongoDB3.2中才加入的新特性,表示对于replica set(包括sharded cluster中使用复制集的shard)返回什么样的数据。不同的存储引擎对read-concern的支持情况也是不一样的
read concern有以下三个level:
local:默认值,返回当前节点的最新数据,当前节点取决于read reference。
majority:返回的是已经被确认写入到多数节点的最新数据。该选项的使用需要以下条件: WiredTiger存储引擎,且使用election protocol version 1
;启动MongoDB实例的时候指定 --enableMajorityReadConcern
选项。
linearizable:3.4版本中引入,这里略过了,感兴趣的读者参考文档。
在文章中有这么一句话:
Regardless of the read concern level, the most recent data on a node may not reflect the most recent version of the data in the system.
就是说,即便使用了read concern:majority, 返回的也不一定是最新的数据,这个和NWR理论并不是一回事。究其根本原因,在于最终返回的数值只来源于一个MongoDB节点,该节点的选择取决于read reference。
在这篇文章中,对readconcern的引入的意义以及实现有详细介绍,在这里只引用核心部分:
readConcern
的初衷在于解决『脏读』的问题,比如用户从 MongoDB 的 primary 上读取了某一条数据,但这条数据并没有同步到大多数节点,然后 primary 就故障了,重新恢复后 这个primary 节点会将未同步到大多数节点的数据回滚掉,导致用户读到了『脏数据』。当指定 readConcern 级别为 majority 时,能保证用户读到的数据『已经写入到大多数节点』,而这样的数据肯定不会发生回滚,避免了脏读的问题。
一致性 or 可用性?
回顾一下CAP理论中对一致性 可用性的问题:
一致性,是指对于每一次读操作,要么都能够读到最新写入的数据,要么错误。
可用性,是指对于每一次请求,都能够得到一个及时的、非错的响应,但是不保证请求的结果是基于最新写入的数据。
前面也提到,本文对一致性 可用性的讨论是基于replica set的,是否是shared cluster并不影响。另外,讨论是基于单个客户端的情况,如果是多个客户端,似乎是隔离性的问题,不属于CAP理论范畴。基于对write concern、read concern、read reference的理解,我们可以得出以下结论。
- 默认情况(w:1、readconcern:local)如果read preference为primary,那么是可以读到最新的数据,强一致性;但如果此时primary故障,那么这个时候会返回错误,可用性得不到保证。
- 默认情况(w:1、readconcern:local)如果read preference为secondary(secondaryPreferred、primaryPreferred),虽然可能读到过时的数据,但能够立刻得到数据,可用性比较好
- writeconern:majority保证写入的数据不会被回滚; readconcern:majority保证读到的一定是不会被回滚的数据
- 若(w:1、readconcern:majority)即使是从primary读取,也不能保证数据一定不会回滚,因此是弱一致性
- 若(w: majority、readcocern:majority),如果是从primary读取,那么一定能读到最新的数据,且这个数据一定不会被回滚,但此时写可用性就差一些;如果是从secondary读取,不能保证读到最新的数据,弱一致性。