【数据库】MongoDB数据库学习记录

本次学习使用的是麦子学院的《mongodb最佳实践课程》.侵删.

安装数据库

sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 #确保安装的是最新的版本
echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list #将mangodb添加到源
sudo apt-get update
sudo apt-get install -y mongodb-org

在ubuntu下mongodb默认已经启动

sudo service mongod stop #停止
sudo service mongod start #启动

打开mongodb数据库使用命令mongo,如果打开,可以看到版本
如果打开失败,则再确认下是否已经启动,使用上述命令重新启动一次即可

Paste_Image.png

创建并选择一个库,我使用的库名为test,图个简单
use test #自动创建并选择一个库

退出
exit

配置文件所在
/etc/mongod.conf

插入数据

插入数据时mongodb会自动生成一个唯一的_id字段,其实这个_id也可以自行指定。

db.students.indert({_id:"my_id",name:"whaike",age:24,contact:[{city:"深圳"},{phone:["1232354235"]}]})
插入测试数据
db.students.insert({name:"张三",school:{name:"清华大学",city:"北京"},age:19,gpa:3.97})
db.students.insert({name:"李四",school:{name:"北京大学",city:"北京"},age:20,gpa:3.3})
db.students.insert({name:"王二",school:{name:"交通大学",city:"上海"},age:22,gpa:3.68})
db.students.insert({name:"小牛",school:{name:"哈工大",city:"哈尔滨"},age:21,gpa:3.50})
db.students.insert({name:"小马",school:{name:"交通大学",city:"西安"},age:21,gpa:3.70})
db.students.insert({name:"小朱"})

查询

db.students.find() #查询所有记录,也可以使用db.students.find({})
db.students.find({name:"张三"}) #查询name为张三的记录
db.students.find({"school.name":"交通大学"}) #查询学校名称为交通大学的记录
db.students.find({"school.name":"交通大学","school.city":"西安"}) #查询西安的交通大学
db.students.find({$or:[{"school.name":"交通大学"},{"school.city":"北京"}]}) #or运算符,条件或,使用列表
db.students.find({$and:[{"school.name":"交通大学"},{"school.city":"上海"}]}) #and运算符,条件与,使用列表
db.students.find({age:{$ne:20}})  #ne运算符,条件不等于

也可以使用objectid进行查询

> db.students.find({"_id" : ObjectId("58ad4e19a169ed50558d1209")})
{ "_id" : ObjectId("58ad4e19a169ed50558d1209"), "name" : "王二", "school" : { "name" : "交通大学", "city" : "上海" }, "age" : 22, "gpa" : 3.68 }

使用正则进行匹配

db.students.find({name:/^张/}) #匹配开头
db.students.find({name:/.*四/}) #匹配结尾

使用exists查询某条记录是否存在。

db.students.find({school:{$exists:false}}) #查询记录中没有school的记录

更新

通过find命令查看某条记录的_id值,这个值是mongodb自动生成的<b>主键</b>,官网解释为

A special 12-byte BSON type that guarantees uniqueness within the collection. The ObjectId is generated based on timestamp, machine ID, process ID, and a process-local incremental counter. MongoDB uses ObjectId values as the default values for _id fields.

12字节的BSON型数据,前4字节表示时间戳,然后3字节表示机器标识码,然后2字节由进程ID组成 (PID),最后3字节是随机数。这样做不仅可以保证其唯一性,还能保证在高并发的情况下依然保持唯一性。
该值其实是一个96位二进制数。

使用_id值查找记录,结合set关键字进行精确更新操作如下

db.students.update({"_id" : ObjectId("58ad4e19a169ed50558d1207")},{$set:{age:18,gpa:3.95}}) #set关键字,精确操作

inc运算符表示加法

db.students.update({"_id" : ObjectId("58ad4e19a169ed50558d1207")},{$inc:{age:1}}) #将age+1

max关键字可以保证记录中的值为最大值,如果记录中的值比它小则更新它,如果比它大则不更新。min同理,保证最小值。

db.students.update({"_id" : ObjectId("58ad4e19a169ed50558d1207")},{$max:{age:21}})

mul为翻倍的操作

db.students.update({"_id" : ObjectId("58ad4e19a169ed50558d1207")},{$mul:{age:2}}) #age值做x2操作.

unset删除记录,精确删除

db.students.update({"_id" : ObjectId("58ad4e19a169ed50558d1207")},{$unset:{school:true}})
更新2

假设想更新所有记录,将他们的age+1

> db.students.update({},{$inc:{age:1}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

由此可见mongodb默认只更新一条记录
如果要更新多条记录

> db.students.update({},{$inc:{age:1}},{multi:true})
WriteResult({ "nMatched" : 6, "nUpserted" : 0, "nModified" : 6 })

multi表示找到一条记录更新之后继续寻找下一条进行更新

setOnInsert中的内容表示只在插入操作的时候有效,upsert表示存在数据就更新没有数据则插入。

> db.students.update({name:"Mike"},{$inc:{age:1},$setOnInsert:{school:{name:"new school"}}},{upsert:true}) 
WriteResult({
    "nMatched" : 0,
    "nUpserted" : 1,
    "nModified" : 0,
    "_id" : ObjectId("58ae7693cf9b6da6d48b6c8f")
})

此时可以看到匹配到0条所以插入1条。
当我们再次执行时,其school的记录已经有了,故不会再发生插入操作,也就是setOnInsert字段后的内容无效,即使你修改了其中的数据,它只会进行age+1的操作了,如下

> db.students.update({name:"Mike"},{$inc:{age:1},$setOnInsert:{school:{name:"new school--2"}}},{upsert:true})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

BSON

BSON文档:mongodb的根本,与json非常类似,json-style document的形式,bson是二进制Bin的,而json是文本text的。
mongodb有两个特点

1、面向Document.他的增删改查都是document的形式,每一条记录都是document.
2、Key-Value 的数据组织形式。

mongodb的优势在于他可以很容易将数据映射成key-value的形式,多数情况可以将程序中的object直接与数据库达成此映射,很方便数据交换。
注意:

  1. _id字段为主键,他的值覆盖主键,如果你提供了此值则不会使用mongodb自动提供的值。
  2. 所有Field不能以$开头,这个符号被mongodb保留使用了.
  3. name不能使用*.*这样的格式,这个点好在key中很容易识别错误.
  4. key可以重名,但最好不要,很容易查询到错误的value.
  5. value的类型比较严格,因为存在numberlong格式,存入的数据如果是1则很可能存储为"1",造成查询时无法匹配,这个与使用的框架或语言有关。
  6. mongodb对于单个的document的限制为16M.

内嵌对象与reference

在MongoDB中,表示关系有两种办法:
一种是嵌套(embedded),既是将一个文档包裹一个子文档;
另一种是引用链接(reference link),使用MongoDB的DBRef对象建立文档和文档之间的关系。
关于表示关系的详细说明可以参看这篇博文
MongoDB数据库关系表示和设计:(1)嵌套文档和引用链接

索引

查询的三个方式

index seek #直接索引 - 快
index scan #扫索引 - 中
table scan #扫表 - 慢

mongodb使用BTREE实现索引。
开始使用name查询记录
创建索引

db.students.ensureIndex({name:1}) #为name创建索引

db.students.getIndexes()#查看是否存在索引
此时根据name查看数据就会走Btree索引的方式

> db.students.find({name:"Mike"}).explain()
{
    "cursor" : "BtreeCursor name_1",
        ..........

explain()可以查看执行某条命令的具体信息。
创建联合索引

db.students.ensureIndex({age:1,gpa:1})

此时数据库中的顺序会先按age排序,然后根据gpa排序,有点类似DataFrame的Index吧.
此时查询建立了索引的字段会走索引,例如

> db.students.find({age:{$gt:21},gpa:{$lt:3.6}}).explain()
{
    "cursor" : "BtreeCursor age_1_gpa_1",
    "isMultiKey" : false,
    "n" : 1,
    "nscannedObjects" : 1,
    "nscanned" : 4,
        ........

单查某一个比如gpa则不会走此索引

> db.students.find({gpa:{$lt:3.6}}).explain()
{
    "cursor" : "BasicCursor",
    "isMultiKey" : false,
    "n" : 2,
    "nscannedObjects" : 7,
    "nscanned" : 7,
        ......

树的表达和查询

假设某平台存在这样的数据结构

设计此document可以使用{_id:...,symble:"电脑"}
为保证某字段其唯一性,可以为其创建一个索引和一个唯一索引

 db.cats.ensureIndex({symbol:1},{unique:1})

那么如何表示其关系呢,
可以增加children字段
{_id:...,symble:"电脑",children:["平板","台式机","笔记本"]}
如果觉得一个数组中存储的数据太多,可以使用parent字段
{_id:...,symble:"电脑",children:["电子"]}
这样查询的时候使用递归或者其他方式进行循环查询可以得出特定需求的结果。

当然还有更好的方式:

1、Ancestors Array。在每个子类中使用一个字段专门用来记录其所有的父辈爷辈等通路路径上的所有节点,再为该字段创建索引,查询的时候直接使用这个索引即可.

举例:{_id:...,symble:"电脑",ancestors:["电子",电脑]}
建立索引:ensureIndex({ancestors:1})
查询:find({ancestors:"电子"})
这样就可以查到所有acenstors中带电子的记录,而且还是走的索引,效率比较高。

2、Path 。这是官方的做法。跟上一个有点类似,但是它使用的是path字段,为string类型,内容为严格的路径节点顺序,并使用指定字符进行分隔,例如"|",查询的时候可以使用正则,find({$path:/^某节点名|节点名/})

举例:{_id:...,symble:"电脑",ancestors:"电子|电脑"}
查询:find({$path:/^电子/}) #查询电子下所有记录
查询:find({$path:/^电子|电脑/}) #查询电子-电脑下所有记录

3、Model Tree Structures
详见官方文档Model Tree Structures

查询拓展

find({条件},{映射(也叫投影技术)})
find({},{字段名:0,name:1,age:1}) #0表示不需要,1表示需要

分页技术
db.students.find({},{"_id":0,name:1,age:1}).limit(2).skip(2).sort({age:1}) #组合使用limit,skip,sort
limit表示每一页多少条数据,skip表示跳过多少条数据,如果limit和skip一起使用,则跳过指定数据后会显示之后limit条数据.
排序中要使用字典方式,value的1表示顺序-1表示倒序(排序的sort函数放在任何位置效果都是一样的!这点与其他数据库不一样),倒序的排序也会优先使用索引,而且是在顺序的索引基础上进行reverse。

数组内嵌对象的查询

简单查询

插入数据

db.students.insert({name:"艾客",school:{name:"地球大学",city:"深圳"},courses:[{name:"MongoDB",grade:88},{name:"Java",grade:99}],age:19,gpa:3.9})

查询

db.students.find({courses:{name:"MongoDB",grade:88}}) #精确查询
db.students.find({"courses.name":"MongoDB"}) #模糊查询所有匹配
db.students.find({"courses.0.name":"MongoDB"}) #数组中第n个元素(对象)的name为mongodb的
db.students.find({courses:{$size:2}}) #查询ourses中恰好两个元素的数据
复合查询

插入数据

db.students.insert({name:"啊哈",school:{name:"宇宙大学",city:"深圳"},courses:[{name:"MongoDB",grade:88,quiz:[9,7,9,10]},{name:"Java",grade:98,quiz:[5,6,7,9]}],age:24,gpa:3.8})

查询

db.students.find({courses:{$elemMatch:{name:"MongoDB",grade:{$gte:88}}}}) #复合查询
db.students.find({courses:{$elemMatch:{name:"MongoDB",quiz:{$elemMatch:{$gt:3,$lt:5}}}}}) #复合查询elemMatch
db.students.find({courses:{$elemMatch:{name:"MongoDB",quiz:{$all:[8,9]}}}}) #all匹配数组中的元素

更新插入

添加记录

db.students.update({"_id" : ObjectId("58a903fbce4e9c33a3563dc3")},{$addToSet:{courses:{name:"Swift",grade:89}}}) 

addToSet可以保证数据的唯一性,如果存在记录,则不会添加此字段,如果想要添加同样的记录两条,则要使用push,从已有的元素中删除使用pull(严格匹配,也就是值也要正确),pull该条数据中所有符合条件的全部删除,如果只删除相同数据的其中一条则需要更精确的匹配<b>(这里需要再验证下)</b>。
2.4之前如果要添加或者删除多条的时候使用pushAll,pullAll
版本2.4之后,push也可以插入多条

$push:{courses:{$each:[{name:"Swift",grade:89},{name:"C++",grade:87}],$sort:{grade:-1}}} 

上面的代码在使用each之后,此时的push可以使用数组进行多个元素的添加,而且支持排序.

$push:{courses:{$each:[],$sort:{grade:1}}} #使用空的方括号,可以单独进行排序操作
$position:2 表示插入的位置,从0开始计算
$slice:2 切割,只显示/保留数据数组中的前多少数据

以上三个sort/position/slice都是与each搭配使用的!

更精确的修改操作

db.students.update({"_id":ObjectId("......"),"courses.name":"Swift"},{$set:{"courses.$.grade":99}}) 

更精确的修改操作.courses.$.grade中使用$符号表示不知道查到的元素在该条数据中的位置,如果知道,可以使用数字,从0开始。

db.students.update({"_id" : ObjectId("58a903fbce4e9c33a3563dc3"),"courses.name":"MongoDB"},{$push:{"courses.$.quiz":9.6}}) #为某条数据中某数组中的数组增加一个值

游标cursor

var cursor = db.students.find({}) 
cursor.limit(2)#取出多少条记录
cursor.count()#总共多少条记录
cursor.objsLeftInBatch()查询取出一部分记录之后还剩下多少条数据
cursor.hasNext() #返回布尔值,是否还存在记录,也就是是否还可以执行next()方法取得数据
cursor.next() #取出下一条数据
cursor.toArray() #将数据转化为数组,可以通过[n]访问某一条记录
db.students.find({}).map(function(doc){return doc.name}) #类似js
db.students.find({}).map(function(doc){return {"name":doc.name,"gpa":doc.gpa/4*100}}) #可以写复杂点的函数
db.students.find().forEach(function(doc){print(doc.name)}) #forEach函数与map函数类似也可以将参数逐个传入进行处理
db.students.find({},{"_id":0,age:1,gpa:1},hint({age:1,gpa:1})) #投影+排序  hint使用指定索引.强迫使用复合索引

地理位置查询

先插入数据

db.pois.insert({name:"AAA Store",loc:{type:"Point",coordinates:[70,30]}})
db.pois.insert({name:"BBB Bank",loc:{type:"Point",coordinates:[69.99,30.01]}})
db.pois.insert({name:"CCC Park",loc:{type:"Polygon",coordinates:[[[70,30],[71,31],[71,30],[70,30]]]}})
db.pois.insert({name:"DDD Forest",loc:{type:"Polygon",coordinates:[[[65,68],[67,68],[67,69],[65,68]]]}})

为地理位置创建索引
db.pois.ensureIndex({loc:"2dsphere"})
结果自动按照由近到远的顺序
$maxDistance:1000000单位是米
如果不创建索引则查询会报错code:17007

附近地点查询
db.pois.find({loc:{
    $near:{
        $geometry:{
            type:"Point",
            coordinates:[70,30]
        },
        $maxDistance:1000000
    }
}})

区域的点组成的环需要封闭,也就是第一个点的坐标跟最后一个点的坐标需要相同,如下

区域内地点查询
db.pois.find({loc:{
    $geoWithin:{
        $geometry:{
            type:"Polygon",
            coordinates:[[[70,30],[71,31],[71,30],[70,30]]]
        }
    }
}})
使用geoNear command

runCommand对数据库执行一些命令

查询
db.runCommand(
    {
        geoNear:"pois",
        near:{type:"Point",coordinates:[70,30]},
        spherical:true,
        minDistance:3000,
        maxDistance:7000
    }
)

得到结果

{
    "results" : [ ],
    "stats" : {
        "nscanned" : NumberLong(13),
        "objectsLoaded" : NumberLong(4),
        "avgDistance" : NaN,
        "maxDistance" : 0,
        "time" : 3
    },
    "ok" : 1
}

在查询的结果集中

results以数组的形式保存查询到的结果,数组中的每个元素都是一个objects,拥有两个key:
dis:距离,离当前位置的距离
obj:查询到的具体数据对象

状态stats显示统计数据
avgDistance:平均距离
maxDistance:最大距离

修改范围使其可以查询到数据,验证results是否准确,以下:

db.runCommand(
    {
        geoNear:"pois",
        near:{type:"Point",coordinates:[70,30]},
        spherical:true,
        minDistance:0,
        maxDistance:700000
    }
)

关于2dsphere索引,详见官网2dsphere Indexes

全文索引

首先插入数据

db.text.insert({content:"text performs a text search on the content of the fields indexed with a text index"})
db.text.insert({content:"When dealing with a small number of documents,.it is possible for the full-text-s"})
db.text.insert({content:"Soros enjoys playing mongo"})
db.text.insert({content:"Why don't you use mongo-db?"})

text创建全文索引
db.text.ensureIndex({content:"text"})
查询
db.text.find({$text:{$search:"mongo"}})

也可以为search指定language

db.text.find({$text:{$search:"mongo",$language:"none"}}) #不区分语言
db.text.find({$text:{$search:"mongo",$language:"en"}}) #英文

数据库管理初步

一般数据库在运行的时候只有一个服务,那么如果它除了问题停止运行了怎么办呢?
此时我们可以运行两个服务,当第一个服务关闭的时候自动切换到第二个服务,这样就可以保证数据库的正常访问了。

首先查看mongodb的进程ps -ax | grep mongod
为了提高服务的高可用性,增加一个服务

sudo mongod --dbpath /srv/data --port 12333 #在前台启动一个

此时我的ubuntu报错

 ERROR: dbpath (/srv/data) does not exist.
 Create this directory or give existing directory in --dbpath.
 See http://dochub.mongodb.org/core/startingandstoppingmongo

于是新建这个目录sudo mkdir /srv/data,再次执行时
显示<code>waiting for connections on port 12333</code>时则表示成功了
可以使用ctrl+c结束,且不必担心会有其他残留问题,这点可以从它自动打印出来的信息得到.

前台启动的方式总是不方便的,那么如何后台启动呢

sudo mongod  --fork --logpath /var/log/mongodb2.log --dbpath /srv/data --port 12333 #在后台启动一个服务

可以看到启动成功的信息

about to fork child process, waiting until server is ready for connections.
forked process: 2596
child process started successfully, parent exiting

连接其他进程的mongo数据库
mongo --port 12333
在数据库内
db.serverCmdLineOpts()可以查看连上的数据库的详细信息

一般使用后台的方式如何停止呢

方式一
使用进程ID进行关闭
sudo kill (process id) #一般关闭数据库

方式二
可以在数据库内关掉它
<small>admin用于管理当前数据库</small>

use admin
db.shutdownServer() #关闭当前数据库

方式三
使用dbpath在shell中指定关闭某个数据库

sudo mongod --shutdown --dbpath /srv/data #使用dbpath指定关闭哪个数据库

数据库服务的监控和profiling

db.runCommand({serverStatus:1}) #查看服务的详细信息
db.runCommand({serverStatus:1}).pid #查看pid
db.runCommand({serverStatus:1}).mem #查看内存
> db.runCommand({serverStatus:1}).mem
{
    "bits" : 64,
    "resident" : 64, #在RAM中所占用的空间
    "virtual" : 662, #虚拟内存
    "supported" : true,
    "mapped" : 240,  #所有存储空间
    "mappedWithJournal" : 480
}
mongodb使用内存映射文件,映射到虚拟内存,然后像读写内存一样来读写文件,类似操作系统的交换内存方式.
> db.runCommand({serverStatus:1}).extra_info
{
    "note" : "fields vary by platform",
    "heap_usage_bytes" : 62586272,
    "page_faults" : 66 #这个值越高读写效率越低
}
使用sharding减少数据量的方式或增加内存的方式来降低page_faults值.

查看数据进出量等

> db.runCommand({serverStatus:1}).network
{ "bytesIn" : 6160, "bytesOut" : 28570, "numRequests" : 69 }
如果numRequests过高可以考虑优化一下document或者采用集群方案
> db.runCommand({serverStatus:1}).connections
{ "current" : 1, "available" : 51199, "totalCreated" : NumberLong(3) }
多少个客户端正在链接数据库,总共允许多少个,总共创建了多少个用户

mongodb可以通过profile来监控数据,进行优化。

db.runCommand({dbStats:1})#数据库的统计

打开profile
> db.runCommand({profile:-1}) #记录所有操作的时间
{ "was" : 0, "slowms" : 100, "ok" : 1 }

> db.runCommand({profile:1}) #只记录比slowms慢的查询
{ "was" : 0, "slowms" : 100, "ok" : 1 }

db.runCommand({profile:0}) #关闭profile

db.runCommand({profile:2}) #打开慢查询

db.system.profile.find({millis:{$gt:1}},{millis:1,op:1,query:1,ns:1}) #查看大于1ms的操作,后面{}中表示投影出来哪些数据

分布式属性Replication

一组Mongodb复制集,就是一组mongod进程,这些进程维护同一个数据集合。复制集提供了数据冗余和高等级的可靠性。保证数据在生产部署时的冗余和可靠性,通过在不同的机器上保存副本来保证数据的不会因为单点损坏而丢失。能够随时应对数据丢失、机器损坏带来的风险。
Replica Set (cluster)
A replica set in MongoDB is a group of mongod processes that maintain the same data set.
其中,一个primary,多个secondary,写只能在primary上
盗来一图

Paste_Image.png

一组集群只有一个主导,他接收所有客户端的操作,其他的Secondary获得主服务器primary的数据并保持同步,主服务挂掉之后他们会通过选举的方式定哪个secondary升级为primary以保证数据库的正常运行.

配置集群
whaike是为replication指定的一个名字,可以随意起名
sudo mongod  --fork --logpath /var/log/mongodb2.log --dbpath /srv/data --port 12333 --replSet "whaike"
sudo mongod  --fork --logpath /var/log/mongodb3.log --dbpath /srv/data1 --port 12334 --replSet "whaike" 
sudo mongod  --fork --logpath /var/log/mongodb4.log --dbpath /srv/data2 --port 12335 --replSet "whaike" 

启动了三个
连接其中一个mongo --port 12333
键入rs.initiate()
查看状态rs.status()

> rs.status()
{
    "set" : "whaike",
    "date" : ISODate("2017-02-24T08:36:51Z"),
    "myState" : 1,
    "members" : [
        {
            "_id" : 0,
            "name" : "ubuntu:12333",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 263,
            "optime" : Timestamp(1487925371, 1),
            "optimeDate" : ISODate("2017-02-24T08:36:11Z"),
            "electionTime" : Timestamp(1487925371, 2),
            "electionDate" : ISODate("2017-02-24T08:36:11Z"),
            "self" : true
        }
    ],
    "ok" : 1
}
health值为1表示正常运行
statuStr表示该数据库的当前角色

将另外两个添加进来
添加时使用的是name值,具体情况具体定

whaike:PRIMARY> rs.add("ubuntu:12334")
{ "ok" : 1 }
whaike:PRIMARY> rs.add("ubuntu:12335")
{ "ok" : 1 }

再次查看状态

whaike:PRIMARY> rs.status()
{
    "set" : "whaike",
    "date" : ISODate("2017-02-24T08:43:32Z"),
    "myState" : 1,
    "members" : [
        {
            "_id" : 0,
            "name" : "ubuntu:12333",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 664,
            "optime" : Timestamp(1487925805, 1),
            "optimeDate" : ISODate("2017-02-24T08:43:25Z"),
            "electionTime" : Timestamp(1487925371, 2),
            "electionDate" : ISODate("2017-02-24T08:36:11Z"),
            "self" : true
        },
        {
            "_id" : 1,
            "name" : "ubuntu:12334",
            "health" : 1,
            "state" : 5,
            "stateStr" : "STARTUP2",
            "uptime" : 16,
            "optime" : Timestamp(0, 0),
            "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
            "lastHeartbeat" : ISODate("2017-02-24T08:43:30Z"),
            "lastHeartbeatRecv" : ISODate("2017-02-24T08:43:31Z"),
            "pingMs" : 0,
            "lastHeartbeatMessage" : "initial sync need a member to be primary or secondary to do our initial sync"
        },
        {
            "_id" : 2,
            "name" : "ubuntu:12335",
            "health" : 1,
            "state" : 5,
            "stateStr" : "STARTUP2",
            "uptime" : 7,
            "optime" : Timestamp(0, 0),
            "optimeDate" : ISODate("1970-01-01T00:00:00Z"),
            "lastHeartbeat" : ISODate("2017-02-24T08:43:31Z"),
            "lastHeartbeatRecv" : ISODate("2017-02-24T08:43:30Z"),
            "pingMs" : 0,
            "lastHeartbeatMessage" : "initial sync need a member to be primary or secondary to do our initial sync"
        }
    ],
    "ok" : 1
}

可以看到添加成功.
我们插入一条数据试试

whaike:PRIMARY> db.students.insert({name:"testAAA"})
WriteResult({ "nInserted" : 1 })
whaike:PRIMARY> db.students.find()
{ "_id" : ObjectId("58aff25e7e8f7ae117685350"), "name" : "testAAA" }

插入并查询成功!

现在我们将primary关闭以模拟该数据库事故

use admin
db.shutdownServer()
exit #退出

重新进入一个数据库

test1@ubuntu:~$ mongo --port 12334
MongoDB shell version: 2.6.12
connecting to: 127.0.0.1:12334/test
whaike:PRIMARY>

此时可以看到该复制集切换成了primary
键入rs.status()查看状态
可以看出12333端口所关联的数据库的health已经变为0,也就是不可用了,但是之前的STARTUP2变成了primary主导.

此时如果还想扩展其他端口关联的副本进来,可以继续使用rs.add()

其实创建Replication的时候可以一条命令添加多个复制集进来

rs.initiate({"_id" : "whaike","members" : [
{"_id" : 0,"host" : "127.0.0.1:12333"},
{"_id" : 1,"host" : "127.0.0.1:12334"},
{"_id" : 2,"host" : "127.0.0.1:12335"},
]})

sharding分片技术

分片技术,跟关系数据库的表分区类似,我们知道当数据量达到T级别的时候,我们的磁盘,内存就吃不消了,或者单个的mongoDB服务器已经不能满足大量的插入操作,针对这样的场景,mongoDB提供的分片技术来应对。
当然分片除了解决空间不足的问题之外,还极大的提升的查询速度。
盗图

Paste_Image.png

mongodb将数据分摊到片区上,用户代表整个数据库,外面一切访问由通过用户发起,而路由则负责对这些操作按一定的规则分摊到不同的片区执行,这些规则保存在配置服务器中。

下面来个最简单的<b>例子</b>

使用mkdir按此路径建立目录

/srv/maizi/shardsconf/
/srv/maizi/data1/
/srv/maizi/data2/

启动一个配置服务器

sudo mongod --configsvr --port 12339 --dbpath /srv/maizi/shardsconf/ --fork --logpath /var/log/shardconf.log

启动一个router(路由),用于转发查询服务等

sudo mongos --configdb 127.0.0.1:12339 --fork --port 12338 --logpath /var/log/mongos.log

连接mongos

mongo --port 12338
提示符会变为mongos>
exit #退出

启动正常
再启动几个mongodb instance来做sharding服务

sudo mongod --fork --logpath /var/log/shard1.log --dbpath /srv/maizi/data1 --port 12336
sudo mongod --fork --logpath /var/log/shard2.log --dbpath /srv/maizi/data2 --port 12335

此时进程中也许看不到router,但是连接正常,暂且不管他

test1@ubuntu:~$ ps -ax|grep mongod
   991 ?        Ssl    0:20 /usr/bin/mongod --config /etc/mongod.conf
  2692 ?        Sl     0:01 mongod --configsvr --port 12339 --dbpath /srv/maizi/shardsconf/ --fork --logpath /var/log/shardconf.log
  2737 ?        Sl     0:00 mongod --fork --logpath /var/log/shard1.log --dbpath /srv/maizi/data1 --port 12336
  2751 ?        Sl     0:00 mongod --fork --logpath /var/log/shard2.log --dbpath /srv/maizi/data2 --port 12335
  2804 pts/0    S+     0:00 grep --color=auto mongod
test1@ubuntu:~$ mongo --port 12338
MongoDB shell version: 2.6.12
connecting to: 127.0.0.1:12338/test
mongos> exit
bye

连接router

mongo --port 12338 #链接路由
sh.addShard("127.0.0.1:12336") #添加片区
sh.addShard("127.0.0.1:12335") #添加片区

use maizi
sh.enableSharding("maizi") #将麦子数据库enableSharding
sh.shardCollection("maizi.students",{age:1}) #表层面也要enableSharding,还要指定根据什么规则来sharding,这里用年龄

往数据库中插入10万条数据,可能会比较耗时

for(var i=0;i<100000;i++){db.students.insert({age:i%50,name:i});}

查看总条数

db.students.find().count()

查看如何分布

mongos> sh.status()
--- Sharding Status --- 
  sharding version: {
    "_id" : 1,
    "version" : 4,
    "minCompatibleVersion" : 4,
    "currentVersion" : 5,
    "clusterId" : ObjectId("58b387e88aa5cea1a8d87cb9")
}
  shards:
    {  "_id" : "shard0000",  "host" : "127.0.0.1:12336" }
    {  "_id" : "shard0001",  "host" : "127.0.0.1:12335" }
  databases:
    {  "_id" : "admin",  "partitioned" : false,  "primary" : "config" }
    {  "_id" : "maizi",  "partitioned" : true,  "primary" : "shard0001" }
        maizi.students
            shard key: { "age" : 1 }
            chunks:
                shard0000   1
                shard0001   2
            { "age" : { "$minKey" : 1 } } -->> { "age" : 0 } on : shard0000 Timestamp(2, 0) 
            { "age" : 0 } -->> { "age" : 49 } on : shard0001 Timestamp(2, 2) 
            { "age" : 49 } -->> { "age" : { "$maxKey" : 1 } } on : shard0001 Timestamp(2, 3) 

mongos>

"primary" : "shard0001" 这条数据代表就算不sharding,数据也会存储在这个sharding0001中
还可看到
maizi.students是这么分的
一共有shard0000和sharding0001两张表
age从最小值到0分布在shard0000
从0到49分布在shard0001中
从49到最大值也分布在shard0001中
也就是年龄为正数的都是shard0001中

分别查看一下两个sharding中的数据量

test1@ubuntu:~$ mongo --port 12335
MongoDB shell version: 2.6.12
connecting to: 127.0.0.1:12335/test
> use maizi
switched to db maizi
> db.students.find().count()
100000
> exit
bye
test1@ubuntu:~$ mongo --port 12336
MongoDB shell version: 2.6.12
connecting to: 127.0.0.1:12336/test
> use maizi
switched to db maizi
> db.students.find().count()
0
>

因为数据本身并不好且根据年龄分布的规则也未设置好
所以目前所有的数据都分布在shard0001,而shard0000中没有数据
但是sharding应该是有效的
为了验证sharding的有效性
切换到mongos来插入一条年龄为负数的数据

test1@ubuntu:~$ mongo --port 12338
MongoDB shell version: 2.6.12
connecting to: 127.0.0.1:12338/test
mongos> use maizi
switched to db maizi
mongos> db.students.insert({name:"xiaotingzi",age:-26})
WriteResult({ "nInserted" : 1 })

然后切换到shard0000也就是12336端口进行查看<small>(关于这点可以在状态分布的结果中,从shards字段看出)</small>

test1@ubuntu:~$ mongo --port 12336
MongoDB shell version: 2.6.12
connecting to: 127.0.0.1:12336/test
> use maizi
switched to db maizi
> db.students.find()
{ "_id" : ObjectId("58b395c5a04059cd2fbb6ae0"), "name" : "xiaotingzi", "age" : -26 }
>

所以,sharding有效,通过规则区分数据已经成功

关于shard也可以查看文档知识库MongoDB 3.0 常见集群的搭建(主从复制,副本集,分片....)

数据备份与恢复

1、文件系统快照
2、mongodb提供的工具
    1.mongodump #更灵活的操作,缺点:慢
    2.mongorestore

详情见官网MongoDB Backup Methods

安全security checklist

1、Authentication
2、Authorization

mongodb默认的内部通信是明文的,所以需要安置在可信任的环境中.
如果要使用用户名和密码进行访问则需要自己在配置文件中配置
详见官网Security Checklist

服务器端脚本

mongodb2.4版本之后服务端使用的是js的V8引擎来解析服务器端的JS脚本
用回之前的数据库mongo test

通过where可以使用js函数
db.students.find({$where:function(){return obj.age>22;}})
db.students.find({$where:function(){return typeof obj.courses != "undefined" && obj.courses.length >=1;}})
db.students.find({$where:"obj.age>22"}) #也可以直接使用字符串

注意:$where后面的内容返回的一定是一个布尔型
详情见官网Server-side JavaScript

可以自定义JavaScript函数保存在数据库中,使用时用db.eval(...)调用

db.system.js.save(
    {_id:"echoFunction",
    value:function(x){return x;}
    }
)
db.eval("echoFunction('test')")

详见官网Store a JavaScript Function on the Server

事务

<b>ACID</b>
A:原子型 :
C:一致性 :更新一般服务器问题,不会出现脏数据
C:隔离性 :更新过程其他进程无法读取
D:分卷技术 :将要进行的操作写入日志里,日志记录完毕之后会提示操作成功,此时其实并没有成功,如果这时数据库异常,则日志里还有记录,可以保证数据完整。之后再有该日志来修改数据库数据.

所往一个document更新数据 ,符合一致性,要么更新的几个参数一起OK,要么都更新不上,所以单个文档支持事务。

但是多个文档则不支持
但是可以通过某种算法或者技术来实现它
详见官网Perform Two Phase Commits
虽然可以实现,但是依旧要避免出现这种情况。

数据聚合

在这里使用一个可视化软件robomongo-1.0.0-rc1-linux-x86_64-496f5c2.tar.gz
解压之后进入bin目录直接执行robomongo即可打开,配置只有一个连接本地或者远程,端口默认。

这个工具还不错
键入命令

db.students.count({age:{$gt:22}}) #统计年龄大于22的一共有多少个,最简单的聚合
Paste_Image.png

键入

db.students.distinct("age") #统计数据库中所有的年龄,会自动去重

显示方式可以分好几种,我使用的是红色标注的字符模式


Paste_Image.png

统计每个年龄层下有多少人

db.students.group({
        key:{age:1},
        reduce:function(cur,result){result.count+=1},
        initial:{count:0}
    })
db.students.group({
        key:{age:1}, //分组依据
        cond:{age:{$exists:true},gpa:{$exists:true}}, //这里表示只操作有数据的部分
        reduce:function(cur,result){result.count+=1;result.total_gpa += cur.gpa;result.ava_gpa = result.total_gpa / result.count;},  //显示字段与运算规则
        initial:{count:0,total_gpa:0} //初始值
    })
Paste_Image.png
数据聚合流水线
db.students.aggregate(
    [
        {$match:{age:{$exists:true}}},
        {$group:{_id:"$age",count:{$sum:1},total_gpa:{$sum:"$gpa"}}}
    ]
)

$age表示从上一个的输出结果中拿到age,其他字段类似

流水线的分组聚合方式中,各个工位在数组中以逗号分隔,工位是可以增加的

db.students.aggregate(
    [
        {$match:{age:{$exists:true}}},
        {$group:{_id:"$age",count:{$sum:1},total_gpa:{$sum:"$gpa"}}},
        {$project:{_id:1,count:1,ava_gpa:{$divide:["$total_gpa","$count"]}}}
    ]
)
db.students.aggregate(
    [
        {$match:{age:{$exists:true}}}, //拿到符合要求的数据
        {$group:{_id:"$age",count:{$sum:1},total_gpa:{$sum:"$gpa"}}}, //根据年龄分组,统计
        {$project:{_id:1,count:1,ava_gpa:{$divide:["$total_gpa","$count"]}}}, //投影与计算平均值
        {$sort:{ava_gpa:-1}} #排序
    ]
)

上一步的计算平均值可以使用内置的函数avg,效果一样

db.students.aggregate(
    [
        {$match:{age:{$exists:true}}},
        {$group:{_id:"$age",count:{$sum:1},ava_gpa:{$avg:"$gpa"}}},
        {$sort:{ava_gpa:-1}}
    ]
)

不同年龄的选课统计

db.students.aggregate(
    [
        {$match:{age:{$exists:true},courses:{$exists:true}}},
        {$project:{age:1,courses_count:{$size:"$courses"}}}, //在这里先对数据进行投影可以减少数据量,减小内存使用,避免浪费资源
        {$group:{_id:"$age",acc:{$avg:"$courses_count"}}},
        {$sort:{acc:-1}}
    ]
)

不能年龄层选课种类统计

db.students.aggregate(
    [
        {$match:{age:{$exists:true},courses:{$exists:true}}},
        {$project:{age:1,"courses.name":1}}, //直接投影课程的名字
        {$unwind:"$courses"}, //展开数组
        {$group:{_id:"$age",c_names:{$addToSet:"$courses.name"}}}, //将同龄的人选的所有课程进行合并
        {$project:{_id:1,c_names:1,cc:{$size:"$c_names"}}}, //统计
        {$sort:{cc:-1}} //最后根据cc排序
    ]
)

详细信息可以查看官方文档Aggregation Pipeline Operators

MapReduce

流水线方式的操作受文档大小限制,且在内存中执行,不适合大数据处理,但是mapReduce是将数据存储在一个collection中进行处理,可以操作很大量的数据,适合大数据处理

db.students.mapReduce(
    function(){ //map函数,起到过滤器的作用,emit发射数据
        emit(this.age,this.gpa);
    },
    function(key,values){ //reduce函数 这里values如果有多个值则以数组形式保存
        return Array.sum(values)/values.length;
    },
    { //options
        query:{age:{$exists:true},gpa:{$exists:true}}, //过滤条件
        out:{inline:1} //输出
    }
)
db.students.mapReduce(
    function(){ //map函数,起到过滤器的作用,emit发射数据
        emit(this.age,this.gpa); //发射出来的两个参数中,age作为key,gpa作为value,如果key下的value不止一个值,那么才会进入reduce进行进一步处理,且传入的value是一个数组,如果只有一个则不会.发射出的数据受到单个bson的object大小限制(16M)且最多只能发射一半(8M)
    },
    function(key,values){ //reduce函数 这里values如果有多个值则以数组形式保存
        return Array.sum(values)/values.length;
    },
    { //options
        query:{age:{$exists:true},gpa:{$exists:true}}, //过滤条件
        out:{inline:1} //输出,out:"avg_gpa" //输出到一个collections中,可以持久化保存,可以用于真正的大数据处理,inline操作在内存中运行受内存大小限制,也受到单个bson的object大小限制(16M)且最多只能发射一半(8M)
    }
)

db.students.mapReduce(
    function(){
        if(this.age>20){
            emit("old",this.gpa);
        }else{
            emit("yong",this.gpa)
        }
    },
    function(key,values){
        return {gpa_array:values};
    },
    {
        query:{age:{$exists:true},gpa:{$exists:true}},
        out:{inline:1}
    }
)
db.students.mapReduce(
    function(){
        var course_names = this.courses.map(
            function(course){
                return course.name
            });
        var intersection_names = course_names.filter(
            function(c_name){
                return params.indexOf(c_name) != -1
            });
        var diff_names = course_names.filter(
            function(c_name){
                return params.indexOf(c_name) == -1
            });
        diff_names.forEach(
            function(c_name){
                emit(c_name,intersection_names.length / course_names.length)
            });
    },
    function(key,values){
        var total = values.reduce(function(p,c){
            return p + c;
        });
        return total;
    },
    {
        query:{courses:{$exists:true}},
        out:{inline:1},
        scope:{params:["MongoDB"]}
    }
)

官方文档mapReduce

推荐阅读更多精彩内容