基础概念
NoSQL
NoSQL是一种非关系型的DMS,不需要固定的架构,可以避免join链接,并且易于扩展
NoSQL数据库常用于具有庞大数据存储需求的分布式数据存储
NoSQL的功能
-
非关系
- NoSQL数据库从不遵循关系模型
- 切勿为tables提供固定的固定列记录
- 使用自包含的聚合或BLOB
- 不需要对象关系映射和数据规范化
- 没有复杂功能,例如查询语句、查询计划者
- 参照完整性联接,ACID
-
动态架构
- NoSQL数据库是无模式的或具有宽松模式的数据库
- 不需要对数据架构进行任何形式的定义
- 提供同一域中的异构数据结构
-
简单的API
- 提供易于使用的界面,用于存储和查询提供的数据
- API允许进行低级数据操作和选择方法
- 基于文本的协议,通常与带有JSON的HTTP REST一起使用
- 多数不使用基于标准的查询语言 支持Web的数据库作为面向互联网的服务运行
-
分布式
- 可以以分布式方式执行多个NoSQL数据库
- 提供自动缩放和故障转移功能
- 通常可牺牲ACID概念来实现可伸缩性和吞吐量
- 分布式节点之间几乎没有同步复制,多为异步多主复制,对等,HDFS复制
- 仅提供最终的一致性
- 无共享架构。这样可以减少协调并提高分布。
MongoDB环境配置与安装
。。。。暂时略过,主要是本机已经有了懒得弄
MongoDB基础操作
创建数据库
use database_name
如果数据库不存在,则指向数据库,但不创建(等待实际数据入库时创建),否则切换到指定数据库
# 显示所有数据库名
show dbs
# 显示当前数据库的名称
db
# 查看当前db所有的collection
show collections
# 删除当前的数据库
db.dropDatabase()
MongoDB中默认的数据库为test,如果没有创建新的数据库,集合将存放在test数据库中
创建集合
db.createCollection(name, options)
options的选项(可选:指定有关内存大小和索引选项)

例如:
>db.createCollection("log", { capped : true, size : 5242880, max : 5000 } )
删除集合
db.collection_name.drop()
drop()方法成功返回true,反之为false
MongoDB数据类型

插入文档
db.collection_name.insert(document)
在插入文档时,如果不指定_id参数,会自动分配一个唯一的ObjectId
_id是12个字节的十六进制数,唯一一个集合中的每个文档,12个字节被划分如下:
_id: ObjectId(4 bytes timestamp, 3 bytes machine id, 2 bytes process id, 3 bytes incrementer)
insert和save的区别
- 插入的文档无_id
save()方法等同于insert方法
db.col.insert(
{
title: 'oracle',
description: 'oracle is sql database',
}
)
db.col.save(
{
title: 'oracle',
description: 'oracle is sql database',
}
)
- 插入的文档带有_id时
如果插入的数据对象已存在数据集合中
insert会报错
save则会覆盖原有对象
查询文档
db.collection_name.find() # 查询多个文档,返回一个游标
db.mycol.findOne() # 查询当个文档,返回一个对象
pretty()方法
结果显示在一个格式化的方法,可以使用pretty()方法
>db.mycol.find().pretty()
MongoDB与RDBMS where语句比较

AND在mongo中的用法
在 find() 方法,如果通过多个键分离’,',那么 MongoDB 处理 AND 条件
>db.mycol.find({key1:value1, key2:value2}).pretty()
OR在mongo中的用法
OR条件的基础上要查询文件,需要使用$or关键字
>db.mycol.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()
AND和OR一起使用
> db.mycol.find({
"likes": {
$gt: 50
},
$or: [
{
"by": "tutorials itcast"
},
{
"title": "MongoDB Overview"
}
]
}).pretty()
更新文档
MongoDB 使用 update() 和 save() 方法来更新集合中的文档
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)
参数说明
- query
update的查询条件,类似sql update查询内where后面的。 - update
update的对象和一些更新的操作符(如 , , ,inc…)等,也可以理解为sql update查询内set后面的 - upsert
可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。 - multi
可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。 - writeConcern
可选,抛出异常的级别。
MongoDB默认只更新单一的文件,要更新多个你需要设置multi:true
>db.mycol.update({'by':'tutorials itcast'},{$set:{'by':'itcast'}},{multi:true})
MongoDB Save()方法
save() 方法覆盖原有的文档 或者 插入新的文档
db.collection.save(
<document>,
{
writeConcern: <document>
}
)
- document
要存储的文档数据。 - writeConcern
可选,抛出异常的级别。
删除文档
MongoDB remove()方法是用来移除集合中的文档
tip:>在执行remove()函数前先执行find()命令来判断执行的条件是否正确,这是一个比较好的习惯。
db.collection.remove(
<query>,
{
justOne: <boolean>,
writeConcern: <document>
}
)
参数说明
- query :(可选)删除的文档的条件。
- justOne: (可选)如果设为 true 或 1,则只删除一个文档。默认false
- writeConcern: (可选)抛出异常的级别。
索引
索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构
db.collection_name.ensureIndex({key:1})
# key为要创建的索引字段,1为指定升序创建索引,-1即为降序
explain命令
利用 explain 命令,可以很好地观察系统如何使用索引来加快检索,同时可以针对性优化索引。
创建索引前

- collscan
就是所谓的“集合扫描”,看到集合扫描 相当于 sql数据库中的table scan - nReturned
就是所谓的numReturned,就是说最后返回的num个数,从图中可以看到,就是最终返回了三条 - docsExamined
就是documentsExamined,检查了10个documents。。。而从返回上面的nReturned
从上面信息得出,docsExamined10条数据,最终返回3条,说明做了7条数据scan的无用功
创建索引后

- IXSCAN
这个时候再也不是所谓的COLLSCAN了,而是IndexScan,这就说明我们已经命中索引了 - nReturned、totalDocsExamined、totalKeysExamined
从图中可以看到三个参数都是3,这就说明我们的mongodb查看了3个key,3个document,返回3个文档,这个就是所谓的高性能所在
查看索引
db.collection_name.getIndexes()
索引的代价:索引会增加写操作的代价
聚合aggregate
MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。有点类似sql语句中的 count(*), sum(), avg()
db.collection_name.arrgegate(AGGREGATE_OPERATION)
例如要是实现这个聚合
select by_user, count(*) from mycol group by by_user
# 换成mongo
> db.mycol.aggregate([ { $group: { _id: "$by_user", num_tutorial: { $sum: 1 } } }])
常见的聚合表达式

管道的概念
管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的输入
MongoDB的聚合管道将mongo文档在一个管道处理完毕后将结果传递给下一个管道处理,管道操作是可以重复的
表达式:处理输入文档并输出。表达式是无状态的,只能用于计算当前聚合管道的文档,不能处理其它的文档
聚合管道示意图

$project
修改输入文档的结构,可以用来重命名,增加或删除字段(域),也可以用于创建计算结果以及嵌套文档
- 指定输出字段
db.books.aggregate( [ { $project: { title: 1, author: 1 } }] )
这样的话结果中就只还有_id,tilte和by_user三个字段了,默认情况下_id字段是被包含的,如果要想不包含_id话可以这样:
db.books.aggregate({ $project: { _id: 0, title: 1, author: 1 }})
- 输出嵌入式文档中field
db.books.aggregate({ $project: { _id: 0, title: 1, "author.last": 1 }})输出:
{ "title" : "abc123", "author" : { "last" : "zzz" } }
- 改变输出结构
db.books.aggregate({ $project: { _id: 0, title: 1, "lastName": "$author.last" }})输出:
{ "title" : "abc123", "lastName" : "zzz" }
- 计算Fields
利用 $project 新增字段 isbn, lastName, and copiesSold:
substr语法:
{ $substr: [ <string>, <start>, <length> ] }
# $substr,$substrBytes,$substrCP是aggregate的管道操作符,主要可用在project中
# $substr在版本3.4后最好使用$substrBytes
其中< string >是需截取的字符串,若为表内字段可用$加字段名。
< start >截取开始的位置,为整数,整数从0开始。若数字为负数或大于< string >的长度,则返回空字符串""。
< length >截取字符串长度,为整数。若数字为负数则返回< start >后的全部的字符串
db.books.aggregate( [ { $project: { title: 1, isbn: { prefix: { $substr: [ "$isbn", 1, 3 ] }, }, lastName: "$author.last", copiesSold: "$copies" } } ])输出:
{ "_id" : 1, "title" : "abc123", "isbn" : { "prefix" : "001" }, "lastName" : "zzz", "copiesSold" : 5 }
- 指定输出字段
title: 1
- 输出嵌入式文档中field
"author.last": 1
- 改变输出结构
"lastName": "$author.last"
- 计算fields
prefix: { $substr: [ "$isbn", 1, 3 ] }
$match
用于过滤数据,只输出符合条件的文档
利用$match进行匹配操作
db.articles.aggregate([ { $match : { author : "dave" } } ])
范围条件匹配
统计 articles 集合中 score在70~90中间,或者views大于等于1000
- 聚合match写法
db.articles.aggregate([ { $match: { $or: [ { score: { $gt: 70, $lt: 90 } }, { views: { $gte: 1000 } } ] } }])
- 计算count
统计 articles 集合中 score在70~90中间,或者views大于等于1000 个数
db.articles.count( { $or: [ { score: { $gt: 70, $lt: 90 } }, { views: { $gte: 1000 } } ]})
换成aggregate写法

db.articles.aggregate([ { $match: { $or: [ { score: { $gt: 70, $lt: 90 } }, { views: { $gte: 1000 } } ] } }, { $group: { _id: null, count: { $sum: 1 } } }] )

$group
将集合中的文档分组,可用于统计结果
下面的聚合操作使用 $group 将文档按月、日、年组分组, 计算平均数量以及每个组的文档数:
db.sales.aggregate(
[
{
$group : {
_id : { month: { $month: "$date" }, day: { $dayOfMonth: "$date" }, year: { $year: "$date" } },
averageQuantity: { $avg: "$quantity" },
count: { $sum: 1 }
}
}
]
)
group by null
下面的聚合操作指定_id 等于null的空组,计算总价格和平均数量以及集合中的所有文件数
db.sales.aggregate(
[
{
$group : {
_id : null,
totalPrice: { $sum: { $multiply: [ "$price", "$quantity" ] } },
averageQuantity: { $avg: "$quantity" },
count: { $sum: 1 }
}
}
]
)
检索不同的值
下面的聚合操作使用 $group 将item字段去重,以检索不同的项目值:
db.sales.aggregate( [ { $group : { _id : "$item" } } ] )
# 类似sql语句: select _id from sales group by _id
例子:

- group title by author(下面的聚合操作 按authors分组, 收集books中的titles)
db.books.aggregate(
[
{ $group : { _id : "$author", books: { $push: "$title" } } }
]
)
- group documents by author(下面的聚合操作 按author分组,收集 $$ROOT 系统变量(代表文档自身))

$unwind
将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值
- 指定要拆分字段的路径
{ $unwind: <field path> }
- 指定一个文档格式
3,2版本后有
{
$unwind:
{
path: <field path>,
includeArrayIndex: <string>,
preserveNullAndEmptyArrays: <boolean>
}
}
例子1
db.inventory.insert({ "_id" : 1, "item" : "ABC1", sizes: [ "S", "M", "L"] })
使用$unwind 让数组中的每个元素输出一个文档:
db.inventory.aggregate( [ { $unwind : "$sizes" } ] )

例子2
db.inventory.insert([{ "_id" : 1, "item" : "ABC", "sizes": [ "S", "M", "L"] },{ "_id" : 2, "item" : "EFG", "sizes" : [ ] },{ "_id" : 3, "item" : "IJK", "sizes": "M" },{ "_id" : 4, "item" : "LMN" },{ "_id" : 5, "item" : "XYZ", "sizes" : null }])
# 以下操作是等效的,并为sizes字段中的每个元素返回一个文档
db.inventory.aggregate( [ { $unwind: "$sizes" } ] )
db.inventory.aggregate( [ { $unwind: { path: "$sizes" } } ] )
tip:如果sizes字段不能解析成数组,但又不属于(不存在,null,空数组时,处理方式为丢弃), $unwind将是为一个单数组操作
指定索引号 includeAarrayIndex
以下的 $unwind操作 使用了 includeArrayIndex选项输出数组元素的数组索引
db.inventory.aggregate( [ { $unwind: { path: "$sizes", includeArrayIndex: "arrayIndex" } }

注意:如果 sizes字段 不能解析成数组,但又不属于情况(不存在,null,或者是空数组)的话,索引值字段为null

{
$unwind:
{
path: <field path>, #拆分路径
includeArrayIndex: <string>, #指定数组索引号
preserveNullAndEmptyArrays: <boolean> #防止数据丢失
}
}
$lookup
执行左连接到一个集合(unsharded),必须在同一个数据库中
$lookup添加了一个新的数组字段,该字段的元素是joined集合中的匹配文档
lookup语法:
{
$lookup:
{
from: <collection to join>, #右集合
localField: <field from the input documents>, #左集合 join字段
foreignField: <field from the documents of the "from" collection>, #右集合 join字段
as: <output array field> #新生成字段(类型array)
}
}

例子:

聚合操作对 orders左集合 左连接 inventory右集合,通过 orders下的item 与 inventory集合的sku
需要注意:
- 两个集合必须在同一个db
- orders是左集合,左连接
- item是orders左集合字段
- sku是inventory右集合字段
- item为null,左连接,右集合sku为null

$out
将聚合结果直接输出到集合文档中
db.books.aggregate( [
{ $group : { _id : "$author", books: { $push: "$title" } } },
{ $out : "authors" }
] )
# 这里就是将聚合结果输出到名为authors的集合(表)中
一些mongo进阶记录
mongoDB复制(副本集)
mongoDB复制是将数据同步到多个服务器的过程
复制提供了数据的冗余备份,并在多个服务器上存储了数据副本,提高了数据可用性,并可以保证数据的安全性
复制还允许从硬件故障和服务中断中恢复数据
为什么有这个复制?
为了数据安全
高数据可用性
灾难恢复
无停机维护(如备份,索引重建,压实)
读缩放(额外的副本读取)
副本集对应用程序是透明的
工作原理

副本集特点
- N个节点的集群
- 任何节点都可为主节点
- 所有写入操作都在主节点
- 自动故障转移
- 自动恢复
设置一个副本集

replSet基本语法
sudo mongod --port "PORT" --dbpath "YOUR_DB_DATA_PATH" --replSet "REPLICA_SET_INSTANCE_NAME"
例子:
- 用适当的选项启动副本集的每个成员
启动副本集名称 rs0
sudo rm -rf /MongoDB/node1 /MongoDB/node2 /MongoDB/node3sudo mkdir -p /MongoDB/node1 /MongoDB/node2 /MongoDB/node3sudo mongod —bind_ip 192.168.17.129 —port 27020 —dbpath "/MongoDB/node1" —replSet rs0sudo mongod —bind_ip 192.168.17.129 —port 27021 —dbpath "/MongoDB/node2" —replSet rs0sudo mongod —bind_ip 192.168.17.129 —port 27022 —dbpath "/MongoDB/node3" —replSet rs0
还可以通过配置文件中指定副本集名称。启动mongod使用配置文件,与配置选项指定的文件:
- mongo shell连接副本集
mongo -port 27020 --host 192.168.17.129
- 初始化initiate副本集
利用rs.initiate()在副本集的一个成员上:
rs.initiate()
mongo会初始化一个默认的复制集配置
4. 验证初始副本集配置
使用rs.conf()显示副本集配置对象
rs.conf()
rs0:OTHER> rs.conf()
{
"_id" : "rs0",
"version" : 1,
"protocolVersion" : NumberLong(1),
"members" : [
{
"_id" : 0,
"host" : "192.168.17.129:27020",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {
},
"slaveDelay" : NumberLong(0),
"votes" : 1
}
],
"settings" : {
"chainingAllowed" : true,
"heartbeatIntervalMillis" : 2000,
"heartbeatTimeoutSecs" : 10,
"electionTimeoutMillis" : 10000,
"getLastErrorModes" : {
},
"getLastErrorDefaults" : {
"w" : 1,
"wtimeout" : 0
},
"replicaSetId" : ObjectId("579b3500299da8059cc5fb99")
}
}
rs0:PRIMARY>
复制(副本集)当前的状态rs.status()
此输出反映了副本集的当前状态,使用来自副本集的其他成员发送的心跳数据包的数据。
rs0:PRIMARY> rs.status()
{
"set" : "rs0",
"date" : ISODate("2016-07-29T11:09:58.433Z"),
"myState" : 1,
"term" : NumberLong(1),
"heartbeatIntervalMillis" : NumberLong(2000),
"members" : [
{
"_id" : 0,
"name" : "192.168.17.129:27020",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 1200,
"optime" : {
"ts" : Timestamp(1469789441, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2016-07-29T10:50:41Z"),
"electionTime" : Timestamp(1469789440, 2),
"electionDate" : ISODate("2016-07-29T10:50:40Z"),
"configVersion" : 1,
"self" : true
}
],
"ok" : 1
}
rs0:PRIMARY>
- 将剩下的成员添加到副本集
必须连接到副本集primary主节点上,才能使用rs.add()添加剩余成员
`rs.add()`在某些情况下,触发一个选举。如果你连接到主节点primary成为从节点secondary,你需要连接Mongo shell到主节点primary继续增加新的副本集成员
利用`rs.status()`识别副本集主节点primary

rs0:PRIMARY> rs.add('192.168.17.129:27022')
{ "ok" : 1 }
rs0:PRIMARY> rs.status()
{
"set" : "rs0",
"date" : ISODate("2016-07-29T11:17:28.721Z"),
"myState" : 1,
"term" : NumberLong(1),
"heartbeatIntervalMillis" : NumberLong(2000),
"members" : [
{
"_id" : 0,
"name" : "192.168.17.129:27020",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 1650,
"optime" : {
"ts" : Timestamp(1469791047, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2016-07-29T11:17:27Z"),
"electionTime" : Timestamp(1469789440, 2),
"electionDate" : ISODate("2016-07-29T10:50:40Z"),
"configVersion" : 3,
"self" : true
},
{
"_id" : 1,
"name" : "192.168.17.129:27021",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 115,
"optime" : {
"ts" : Timestamp(1469790932, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2016-07-29T11:15:32Z"),
"lastHeartbeat" : ISODate("2016-07-29T11:17:27.171Z"),
"lastHeartbeatRecv" : ISODate("2016-07-29T11:17:27.159Z"),
"pingMs" : NumberLong(0),
"configVersion" : 3
},
{
"_id" : 2,
"name" : "192.168.17.129:27022",
"health" : 1,
"state" : 0,
"stateStr" : "STARTUP",
"uptime" : 1,
"optime" : {
"ts" : Timestamp(0, 0),
"t" : NumberLong(-1)
},
"optimeDate" : ISODate("1970-01-01T00:00:00Z"),
"lastHeartbeat" : ISODate("2016-07-29T11:17:27.168Z"),
"lastHeartbeatRecv" : ISODate("2016-07-29T11:17:28.182Z"),
"pingMs" : NumberLong(1),
"configVersion" : -2
}
],
"ok" : 1
}
rs0:PRIMARY>
完成时,这就有了一个完整功能的副本集,新的副本集将选出一个primary主节点
6. 检查副本集的状态
rs.status()
- 删除副本
rs.remove("192.168.17.129:27021")
副本集成员
副本集的每个成员都有一个反映它在集合中的配置的状态

测试性能
批量入库
db.eval(function, arguments)
# funcition: 要执行的一个JavaScript函数 : typy为function
# arguments: 参数列表传递给JavaScript函数。省略函数如果不带参数。 type为list
use example
db.eval(
function(num) {
for (var i=0;i<num;i++)
{
db.mycol.save({'_id':i})
}
},10000
)
查看状态
rs0:PRIMARY> rs.status()
查看从节点数据情况

tip:主从启动后,连接slave可以成功连上,但是在slave中执行show dbs的时候会报错
QUERY Error: listDatabases failed:{ "note" : "from execCommand", "ok" : 0, "errmsg" : "not master" }
如何解决
在报错的slave机器上执行 rs.slaveOk() 方法即可
db.getMongo().setSlaveOk()
那么slaveOK方法是什么意思
This allows the current connection to allow read operations to run on secondary members. See the readPref() method for more fine-grained control over read preference in the mongo shell
# 翻译
这允许当前连接允许在辅助成员上运行读操作
测试
- 关闭主节点(模拟主节点宕机的情景)
python@ubuntu:~$ ps -ef | grep mongod
python 6731 2119 0 9月13 ? 00:01:02 /opt/sublime_text/sublime_text /var/log/mongodb/mongod.log
root 37130 6046 0 22:31 pts/22 00:00:00 sudo mongod --bind_ip 192.168.17.129 --port 27020 --dbpath /MongoDB/node1 --replSet rs0
root 37131 37130 0 22:31 pts/22 00:00:21 mongod --bind_ip 192.168.17.129 --port 27020 --dbpath /MongoDB/node1 --replSet rs0
root 37180 37153 0 22:31 pts/12 00:00:00 sudo mongod --bind_ip 192.168.17.129 --port 27021 --dbpath /MongoDB/node2 --replSet rs0
root 37181 37180 0 22:31 pts/12 00:00:15 mongod --bind_ip 192.168.17.129 --port 27021 --dbpath /MongoDB/node2 --replSet rs0
root 37229 37202 0 22:31 pts/21 00:00:00 sudo mongod --bind_ip 192.168.17.129 --port 27022 --dbpath /MongoDB/node3 --replSet rs0
root 37230 37229 0 22:31 pts/21 00:00:15 mongod --bind_ip 192.168.17.129 --port 27022 --dbpath /MongoDB/node3 --replSet rs0
python 41115 28977 0 23:08 pts/6 00:00:00 grep --color=auto mongod
python@ubuntu:~$ sudo kill -9 37131
- 查看集群情况
mongo --port 27021 --host 192.168.17.129
rs0:SECONDARY> rs.status()
{
"set" : "rs0",
"date" : ISODate("2016-09-14T15:17:34.915Z"),
"myState" : 2,
"term" : NumberLong(2),
"syncingTo" : "192.168.17.129:27022",
"heartbeatIntervalMillis" : NumberLong(2000),
"members" : [
{
"_id" : 0,
"name" : "192.168.17.129:27020",
"health" : 0,
"state" : 8,
"stateStr" : "(not reachable/healthy)", # 这时的原主节点状态为不可用
"uptime" : 0,
"optime" : {
"ts" : Timestamp(0, 0),
"t" : NumberLong(-1)
},
"optimeDate" : ISODate("1970-01-01T00:00:00Z"),
"lastHeartbeat" : ISODate("2016-09-14T15:17:33.613Z"),
"lastHeartbeatRecv" : ISODate("2016-09-14T15:16:53.392Z"),
"pingMs" : NumberLong(0),
"lastHeartbeatMessage" : "Connection refused",
"configVersion" : -1
},
{
"_id" : 1,
"name" : "192.168.17.129:27021",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 2778,
"optime" : {
"ts" : Timestamp(1473866226, 1),
"t" : NumberLong(2)
},
"optimeDate" : ISODate("2016-09-14T15:17:06Z"),
"syncingTo" : "192.168.17.129:27022",
"configVersion" : 3,
"self" : true
},
{
"_id" : 2,
"name" : "192.168.17.129:27022",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY", # 新选举出来的主节点
"uptime" : 2613,
"optime" : {
"ts" : Timestamp(1473866226, 1),
"t" : NumberLong(2)
},
"optimeDate" : ISODate("2016-09-14T15:17:06Z"),
"lastHeartbeat" : ISODate("2016-09-14T15:17:34.603Z"),
"lastHeartbeatRecv" : ISODate("2016-09-14T15:17:33.643Z"),
"pingMs" : NumberLong(0),
"electionTime" : Timestamp(1473866225, 1),
"electionDate" : ISODate("2016-09-14T15:17:05Z"),
"configVersion" : 3
}
],
"ok" : 1
}
Mongo分片
当mongoDB存储海量数据的时候,一台机器可能不足以存储数据,也可能不足以提供可接收的读写吞吐量,这时,我们就可以通过多台机器上分割数据,使得数据库系统能存储和处理更多数据
为什么用分片?
- 本地磁盘不够大
- 请求量巨大时会出现内存不足
- 垂直扩展价格昂贵(内存、磁盘、cpu)
在mongo中使用分片集群机构分布


主要组件:
- shard
用于存储实际的数据块,实际生产环境中一个shard server角色可由几台机器组成或者一个replica set承担,防止主机单点故障 - config server
路由表服务,每一台都具有全部chunk的路由信息 - query routers
前端路由,客户端由此接入,且让整个集群看上去像单一数据库,前端应用可以透明使用
分片实例
分片结构
Shard Server 1:27031
Shard Server 2:27032
Config Server: 27100
Route Process: 27777
- 启动shard server
sudo rm -rf /MongoDB/shard/s1 /MongoDB/shard/s2 /MongoDB/shard/log
sudo mkdir -p /MongoDB/shard/s1 /MongoDB/shard/s2 /MongoDB/shard/log
sudo mongod --port 27031 --dbpath=/MongoDB/shard/s1
sudo mongod --port 27032 --dbpath=/MongoDB/shard/s2
- 启动config server
sudo rm -rf /MongoDB/shard/config
sudo mkdir -p /MongoDB/shard/config
sudo mongod --port 27100 --dbpath=/MongoDB/shard/config
注意:这里完全可以像启动普通mongodb服务一样启动,不需要添加shardsvr和configsvr参数,因为这两个参数的作用就是改变启动端口,所以我们自行指定了端口就可以
- 启动route process
mongos --port 27777 --configdb 192.168.17.129:27100
- 配置sharding
使用mongodb shell登录到mongo,添加shard节点
mongo admin --port 27777
MongoDB shell version: 2.0.7
connecting to: 127.0.0.1:27777/admin
mongos> db.runCommand({ addshard:"192.168.17.129:27031" })
{ "shardAdded" : "shard0000", "ok" : 1 }
......
mongos> db.runCommand({ addshard:"192.168.17.129:27032" })
{ "shardAdded" : "shard0009", "ok" : 1 }
- 对某个数据库test启用分片
mongos> db.runCommand({ enablesharding:"test" }) #设置分片存储的数据库
{ "ok" : 1 }
- 对collection进行分片
mongos> db.runCommand({ shardcollection: "test.mycol", key: { _id:1}})
{ "collectionsharded" : "test.mycol", "ok" : 1 }
- 测试
mongo test --port 27777

8. 查看分片情况
查看分片情况时,必须在config(配置服务器上执行),而且必须在admin(如mongo 127.0.0.1:27100/admin)集合下执行
mongo admin --port 27100 #config(配置服务器上执行)
sh.status()
在这里插入代码片-- Sharding Status ---
sharding version: {
"_id" : 1,
"minCompatibleVersion" : 5,
"currentVersion" : 6,
"clusterId" : ObjectId("57cfcdfef06b33543fdeb52e")
}
shards:
{ "_id" : "shard0000", "host" : "localhost:27031" }
{ "_id" : "shard0001", "host" : "localhost:27032" }
active mongoses:
"3.2.7" : 1
balancer:
Currently enabled: yes
Currently running: no
Failed balancer rounds in last 5 attempts: 0
Migration Results for the last 24 hours:
1 : Success
databases:
{ "_id" : "test", "primary" : "shard0000", "partitioned" : true }
test.mycol
shard key: { "_id" : 1 }
unique: false
balancing: true
chunks:
shard0000 2
shard0001 1
{ "_id" : { "$minKey" : 1 } } -->> { "_id" : 1 } on : shard0001 Timestamp(2, 0)
{ "_id" : 1 } -->> { "_id" : 57 } on : shard0000 Timestamp(2, 1)
{ "_id" : 57 } -->> { "_id" : { "$maxKey" : 1 } } on : shard0000 Timestamp(1, 3)
分片原理
mongo admin --port 27100 #config(配置服务器上执行)
use config
> db.chunks.find().pretty()
{
"_id" : "test.mycol-_id_MinKey",
"lastmod" : Timestamp(2, 0),
"lastmodEpoch" : ObjectId("57da5df2bb44f821e869eaeb"),
"ns" : "test.mycol",
"min" : {
"_id" : { "$minKey" : 1 }
},
"max" : {
"_id" : 1
},
"shard" : "shard0001"
}
{
"_id" : "test.mycol-_id_1.0",
"lastmod" : Timestamp(2, 1),
"lastmodEpoch" : ObjectId("57da5df2bb44f821e869eaeb"),
"ns" : "test.mycol",
"min" : {
"_id" : 1
},
"max" : {
"_id" : 57
},
"shard" : "shard0000"
}
{
"_id" : "test.mycol-_id_57.0",
"lastmod" : Timestamp(1, 3),
"lastmodEpoch" : ObjectId("57da5df2bb44f821e869eaeb"),
"ns" : "test.mycol",
"min" : {
"_id" : 57
},
"max" : {
"_id" : { "$maxKey" : 1 }
},
"shard" : "shard0000"
}
> db.settings.find()
{ "_id" : "chunksize", "value" : NumberLong(64) }
从上面的min和max可以看出,数据被分成了三快,分别是
(-∞,1)在shard0001上,[1,57]和[57,+∞)在shard0000上
这说明了mongodb中的区间是左闭右开
查看结果(db.mycol.stats())
mongo --port 27777
use test
switched to db test
mongos> show collections
mycol
mycol111
mycol2
db.mycol.stats()
Hashed sharding
选择哈希片键最大的好处是保证数据在各个节点分布基本均匀,下面使用_id作为哈希片键做个简单的测试:
mongo admin --port 27777
mongos> db.runCommand({ shardcollection: "test.myhash", key: { _id:"hashed"}})
{ "collectionsharded" : "test.myhash", "ok" : 1 }
use test
var num =10000
for (var i=0;i<num;i++){
db.myhash.save({'_id':i})
}
哈希分片将提供的片键散列成一个非常大的长整型作为最终的片键
MongoDB备份与恢复
在mongodb中使用mongodump命令来备份mongodb数据,该命令可以导出所有数据到指定目录中国
备份
mongodump语法
>mongodump -h dbhost -d dbname -o dbdirectory
- -h:MongoDB所在的服务器地址,如127.0.0.1,也可以指定端口号,127.0.0.1:27017
- -d :需要备份的数据库实例,例如test
- -o:备份的数据存储位置,例如:/home/mongodump/,当然该目录需要提前建立,这个目录里面存放该数据库实例的备份数据。
例如:
python@ubuntu:~$ sudo rm -rf /home/mongodump/
python@ubuntu:~$ sudo mkdir -p /home/mongodump/
python@ubuntu:~$ sudo mongodump -h 192.168.17.129:27017 -d itcast -o /home/mongodump/
执行以上命令后,客户端会连接到ip为 192.168.17.129 端口号为 27017 的MongoDB服务上,并备份所有数据到 /home/mongodump/目录中
导出一个集合
python@ubuntu:/home/mongodump$ sudo mongodump -h 192.168.17.129:27017 -d example -c itcast -o /home/mongodump/
2016-09-15T20:08:12.856+0800 writing example.itcast to
2016-09-15T20:08:12.862+0800 done dumping example.itcast (1 document)
python@ubuntu:/home/mongodump$ tree
.
├── example
│ ├── itcast.bson
│ └── itcast.metadata.json
└── itcast
├── articles.bson
├── articles.metadata.json
├── books.bson
├── books.metadata.json
├── col.bson
├── col.metadata.json
├── factories.bson
├── factories.metadata.json
├── inventory.bson
├── inventory.metadata.json
├── itcast.bson
├── itcast.metadata.json
├── mycol.bson
├── mycol.metadata.json
├── myLimit.bson
├── myLimit.metadata.json
├── orders.bson
├── orders.metadata.json
├── sales.bson
└── sales.metadata.json
2 directories, 22 files
更多命令参数可以通过下面命令去看
mongodump --help
恢复
mongorestore命令
>mongorestore -h dbhost -d dbname --dir dbdirectory
- -h:mongodb所在服务器地址
- -d:需要恢复的数据库实例,例如:test,当然这个名称也可以和备份时候的不一样,比如test2
- –dir:备份数据所在位置,例如:/home/mongodump/itcast/
- –drop:恢复的时候,先删除当前数据,然后恢复备份的数据,就是说,恢复后,备份后添加修改的数据都会被删除,慎用
例如:
mongorestore -h 192.168.17.129:27017 -d itcast_restore --dir /home/mongodump/itcast/
恢复数据库example下的集合itcast
python@ubuntu:/home/mongodump$ mongorestore -h 192.168.17.129:27017 -d example_restore --dir /home/mongodump/example/
2016-09-15T20:19:58.176+0800 building a list of collections to restore from /home/mongodump/example dir
2016-09-15T20:19:58.177+0800 reading metadata for example_restore.itcast from /home/mongodump/example/itcast.metadata.json
2016-09-15T20:19:58.206+0800 restoring example_restore.itcast from /home/mongodump/example/itcast.bson
2016-09-15T20:19:58.208+0800 restoring indexes for collection example_restore.itcast from metadata
2016-09-15T20:19:58.209+0800 finished restoring example_restore.itcast (1 document)
2016-09-15T20:19:58.209+0800 done
python@ubuntu:/home/mongodump$ mongorestore -h 192.168.17.129:27017 -d example_restore -c itcast_restore --dir /home/mongodump/example/itcast.bson
2016-09-15T20:24:00.372+0800 checking for collection data in /home/mongodump/example/itcast.bson
2016-09-15T20:24:00.373+0800 reading metadata for example_restore.itcast_restore from /home/mongodump/example/itcast.metadata.json
2016-09-15T20:24:00.398+0800 restoring example_restore.itcast_restore from /home/mongodump/example/itcast.bson
2016-09-15T20:24:00.471+0800 restoring indexes for collection example_restore.itcast_restore from metadata
2016-09-15T20:24:00.471+0800 finished restoring example_restore.itcast_restore (1 document)
2016-09-15T20:24:00.471+0800 done
python@ubuntu:/home/mongodump$
mongorestore命令更多参数可以用
mongorestore --help
MongoDB Map-Reduce(MR)
Map-Reduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果规约(REDUCE)成最终结果

新的mapReduce

MapReduce命令
基础语法:
>db.collection.mapReduce(
function() {emit(key,value);}, //map 函数
function(key,values) {return reduceFunction}, //reduce 函数
{
out: collection,
query: document,
sort: document,
limit: number
}
)
使用 MapReduce 要实现两个函数 Map 函数和 Reduce 函数,Map 函数调用 emit(key, value), 遍历 collection 中所有的记录, 将key与 value 传递给 Reduce 函数进行处理。
Map 函数必须调用 emit(key, value) 返回键值对。
参数说明:
- map: 映射函数(生成键值对序列,作为reduce函数参数)
- reduce:(规约)统计函数,reduce函数的任务就是将key-values变成key-value,也就是values数组变成一个单一的值value
- out:统计结果存放集合(不指定则使用临时集合,再客户端断开后自动删除)
- query:一个筛选条件,只有满足条件的文档才会调用map函数(query,limit,sort可以随意组合)
- sort:和limit结合的sort排序参数(也就是在发往map函数前给文档排序),可以优化分组机制
- limit:执行map函数之前,限定文档数量的上限(要是没有limit,单独使用sort的用处不大)
例子
db.orders.insert([
{
_id: 1,
cust_id: "marong",
ord_date: new Date("Oct 04, 2012"),
status: 'A',
items: [ { sku: "mmm", qty: 5, price: 2.5 },
{ sku: "nnn", qty: 5, price: 2.5 } ]
},
{
_id: 2,
cust_id: "marong",
ord_date: new Date("Oct 05, 2012"),
status: 'B',
items: [ { sku: "mmm", qty: 5, price: 3 },
{ sku: "nnn", qty: 5, price: 3 } ]
}
])
计算每个客户的总消费
执行过程

- 执行map操作过程
定义map(映射函数)来处理每个文档
映射每个文档的cust_id,并处理items
先遍历items,分别对每个items成员qty和price相乘再求和
var mapFunction2 = function() {
var key = this.cust_id;
var value = 0;
for (var idx = 0; idx < this.items.length; idx++) {
value += this.items[idx].qty * this.items[idx].price;
}
emit(key, value);
};
- 定义reduce函数对两个参数keyCustId和valuesPrices
valuesPrices是数组,由keyCustId分组,收集value而来
reduce函数对valuesPrices数组 求和
var reduceFunction2 = function(keyCustId, valuesPrices) {
return Array.sum(valuesPrices);
};
- 执行map-reduce函数
db.orders.mapReduce(
mapFunction2,
reduceFunction2,
{ out: "map_reduce_example" }
)
MongoDB事务

本文中的所有示例使用mongo shell与数据库进行交互,并假设有两个集合:首先,一个名为accounts的集合存储每个账户的文档数据,另一个名为transactions的集合存储事务本身。
首先创建两个名为A和B的账户,使用下面的命令:
db.accounts.save({name: "A", balance: 1000, pendingTransactions: []})
db.accounts.save({name: "B", balance: 1000, pendingTransactions: []})
事务过程
- 设置事务初始状态initial
通过插入下面的文档创建transaction集合,transaction文档持有源(source)和目标(destination),它们引用自accounts集合文档的字段名,以及value字段表示改变balance字段数量的数据。最后,state字段反映事务的当前状态。

- 切换事务到pending状态
在修改accounts集合记录之前,将事务状态从initial设置为pending。使用findOne()方法将transaction文档赋值给shell会话中的局部变量t:

- 将事务应用到两个账户
使用update()方法应用事务到两个账户。在update()查询中,条件pendingTransactions:{$ne:t._id}阻止事务更新账户,如果账户的pendingTransaction字段包含事务t的_id:

- 设置事务状态为committed
使用下面的update()操作设置事务的状态为committed:

- 移除pending事务
使用下面的update()操作从accounts集合中移除pending事务

- 设置事务状态为done
通过设置transaction文档的state为done完成事务

从失败场景恢复
最重要的部分不是上面的典型例子,而是从各种失败场景中恢复未完成的事务的可能性。这部分将概述可能的失败,并提供方法从这些事件中恢复事务。这里有两种类型的失败:
- 所有发生在第一步(即设置事务的初始状态initial)之后,但在第三步(即应用事务到两个子账户)之前的失败,为了还原事务,应用应该获取一个pending状态的transaction列表并且从第二部(即切换事务到pending状态)中恢复
- 所有发生在第三步之后(即应用事务到两个账户)但在第五步(即设置事务状态done)之前的失败,为了还原事务,应用需要获取一个commited状态的事务列表,并且从第四步(即移除pending事务)恢复
因此应用程序总是能够恢复事务,最终达到一个一致的状态。应用程序开始捕获到每个未完成的事务时运行下面的恢复操作。你可能还希望定期运行恢复操作,以确保数据处于一致状态。达成一致状态所需要的时间取决于应用程序需要多长时间恢复每个事务。
回滚
在某些情况下可能需要“回滚”或“撤消”事务,当应用程序需要“取消”该事务时,或者是因为它永远需要恢复当其中一个帐户不存在的情况下,或停止现有的事务。这里有两种可能的回滚操作:
- 应用事务(即第三步)之后,你已经完全提交事务,你不应该回滚事务,相反,创建一个新的事务,切换源和目标(destination)的值
- 创建事务(即第一步)之后,在应用事务(即第三步之前),使用下面的处理过程:

补充理解
事务的四大特性(ACID)
- 原子性(Atomicity)
事务必须是原子工作单位,对于其数据修改,要么全执行,要么全不执行,类似于redis中的lua脚本实现的多条命令操作的原子性 - 一致性(Consistency)
事务完成时,必须使所有的数据都保持一致状态 - 隔离性(Isolation)
由并发事务所做的修改必须与任何其他并发事务所作的修改隔离(就是一个事务执行过程中不应该受其他事务影响) - 持久性(Durability)
事务完成之后,对于系统的影响是永久的
Read Concern\Write Concern\Read Preference
在事务操作中会分别使用到readConcern、writeConcern、readPreference这几个选项,用于控制session的行为
WriteConcern
事务使用事务级别的writeConcern来提交写操作,决定一个事务的写入成功与否要看writeConcern选项设置了几个节点,默认情况下为1

writeConcern: {
w:"majority" // 大多数原则
j:true,
wtimeout: 5000,
}
# 使用实例
db.user.insert({name: "Jack"}, {writeConcern: {w: "majority"}})
建议
对于重要数据可以应用w:"majority"设置,普通数据w:1设置则可以保证性能最佳,w设置的节点越多,等待的延迟也就越大,如果w等于总结点数,一旦其中某个节点出现故障就会导致整个写入失败,也是有风险的
Read Preference
在一个事务操作中使用事务级别的readPreference来决定读取时从哪个节点读取,可方便的实现读写分离,就近读取策略

场景选择
- primary/primaryPreferred:适合于数据实时性要求较高的场景,例如,订单创建完毕直接跳转到订单详情,如果选择从节点读取,可能会造成主节点数据写入之后,从节点还未复制的情况,因为复制过程是一个异步的操作。
- secondary/secondaryPreferred:适应用于数据实时性要求不高的场景,例如,报表数据、历史订单。还可以减轻对主节点的压力。
Read Concern
Mongo3.2引入了readConcern来决定读取的策略,但是与readPreference不同,readPreference决定从哪个节点读取,readConcern决定该节点的哪些数据是可读的,主要保证事务中的隔离性,避免脏读

使用

mongodb的readConcern默认情况下是脏读
例如,用户在主节点读取一条数据之后,该节点未将数据同步至其它从节点,就因为异常挂掉了,待主节点恢复之后,将未同步至其它节点的数据进行回滚,就出现了脏读。
readConcern 级别的 majority 可以保证读到的数据已经落入到大多数节点。所以说保证了事务的隔离性,所谓隔离性也是指事务内的操作,事务外是看不见的。
读写分离实践
一个典型的应用场景是用户写入订单数据(数据写入 Primary),立即调用查询接口,由于采用读写分离模式,链接字符串设置 readPreference=secondaryPreferred 订单写入主节点之后并不能保证数据立即同步到从节点,若此时直接由从节点读取数据, 偶尔会出现订单数据无法找到,用户就会感觉很奇怪,明明下了订单,却又查找不到,造成一些异常订单。

丢失订单例子
db.order.insert({"id": "123456789"})
db.order.find({"id": "123456789"}).readPref("secondaryPreferred")
解决方案1:
设置readPreference=primary,将复制集的节点读取由从节点转换成主节点
这种方式一个缺点是数据量大了之后会增加主节点的压力,也就是没有了主从分离的模式
解决方案2:
使用writeConcern、readConcern组合来解决,既保证读写分离模式,也保证了数据的一致性
// 写入时保证大多数节点写入成功
db.order.insert({"id": "123456789"}, {writeConern: {w: "majority"}})
// 读取时保证这条数据已在大多数节点存在
db.order.find({"id": "123456789"}).readPref("secondaryPreferred").readConcern("majority")
mongodb数据导出和导入
导出工具mongoexport
Mongodb中的mongoexport工具可以把一个collection导出成JSON格式或CSV格式的文件。可以通过参数指定导出的数据项,也可以根据指定的条件导出数据
语法:
mongoexport -d dbname -c collectionname -o file --type json/csv -f field
- -d:数据库名
- -c:collection名
- -o:输出的文件名
- –type:输出的格式,默认为json
- -f;输出的字段,如果–type为csv,则需要加上-f”字段名“
示例:导出集合articles,字段 _id,author,dave,score,views
python@ubuntu:/home/mongodump$ sudo mongoexport -d itcast -c articles -o /home/mongodump/articles.json --type json -f "_id,author,dave,score,views"
2016-09-15T20:33:50.870+0800 connected to: localhost
2016-09-15T20:33:50.871+0800 exported 7 records
python@ubuntu:/home/mongodump$
数据导入mongoimport
语法:
mongoimport -d dbname -c collectionname --file filename --headerline --type json/csv -f field
- -d 数据库名
- -c collection名
- —type 导入的格式,默认json
- -f 导入的字段名
- —headerline 如果导入的格式是csv,则可以使用第一行的标题作为导入的字段
- —file 要导入的文件
示例:导入集合articles_import,字段 _id,author,dave,score,views
python@ubuntu:/home/mongodump$ sudo mongoimport -d itcast -c articles_import --file /home/mongodump/articles.json --type json
2016-09-15T20:41:05.682+0800 connected to: localhost
2016-09-15T20:41:05.706+0800 imported 7 documents
Mongo的存储引擎WT(WiredTiger)
MongoDB Wiredtiger存储引擎实现原理
mongodb的默认设置,wiredTiger的写操作会先写入cache,并持久化到WAL(write ahead log),每60s或log文件达到2GB时会做一次checkPoint,将当前数据持久化,产生一个新的快照,wiredtiger连接初始化时,首先将数据恢复至最新的快照状态,然后根据wal恢复数据,以保持存储可靠性
Wiredtiger的Cache采用Btree的方式组织,每个Btree节点为一个page,root page是btree的根节点,internal page是btree的中间索引节点,leaf page是真正存储数据的叶子节点;btree的数据以page为单位按需从磁盘加载或写入磁盘
Wiredtiger采用Copy on write的方式管理修改操作(insert、update、delete),修改操作会先缓存在cache里,持久化时,修改操作不会在原来的leaf page上进行,而是写入新分配的page,每次checkpoint都会产生一个新的root page
Checkpoint时,wiredtiger需要将btree修改过的PAGE都进行持久化存储,每个btree对应磁盘上一个物理文件,btree的每个PAGE以文件里的extent形式(由文件offset + size标识)存储,一个Checkpoit包含如下元数据:

WiredTiger 的数据结构
存储引擎简单来说其实就是将磁盘上的数据读到内存并返回给应用,或者将应用修改的数据由内存写到磁盘
B-Tree数据结构
B-tree是为磁盘或其他辅助存储设备而涉及的一种数据结构,目的是为了在查找数据的过程中减少磁盘I/O的次数

整体B-Tree中,从上往下,依次是根节点(Root节点),内部节点和叶子节点,每个节点就是一个Page,数据以Page为单位在内存和磁盘间进行调整,每个Page的大小决定了相应节点的分支数量,每条索引记录会包含一个数据指针,指向一条数据记录所在文件的偏移量

WT内存上的基础数据结构

著作权归https://pdai.tech所有。
链接:https://www.pdai.tech/md/db/nosql-mongo/mongo-y-ds.html
- 内存里面B-Tree包含三种类型的page,即rootpage、internal page和leaf page,前两者包含指向其子页的page index指针,不包含集合中的真正数据,leaf page包含集合中的真正数据即keys/values和指向父页的home指针;
- 内存上的leaf page会维护一个WT_ROW结构的数组变量,将保存从磁盘leaf page读取的keys/values值,每一条记录还有一个cell_offset变量,表示这条记录在page上的偏移量;
- 内存上的leaf page会维护一个WT_UPDATE结构的数组变量,每条被修改的记录都会有一个数组元素与之对应,如果某条记录被多次修改,则会将所有修改值以链表形式保存。
- 内存上的leaf page会维护一个WT_INSERT_HEAD结构的数组变量,具体插入的data会保存在WT_INSERT_HEAD结构中的WT_UPDATE属性上,且通过key属性的offset和size可以计算出此条记录待插入的位置;同时,为了提高寻找待插入位置的效率,每个WT_INSERT_HEAD变量以跳转链表的形式构成。
page的其他数据结构

1万+

被折叠的 条评论
为什么被折叠?



