# MongoDB
## 数据库分类
### 关系型数据库
* 具备ACID特性
* Atomic原子性,也就是说事务里的所有操作要么全部做完,要么都不做,事务成功的条件是事务里的所有操作都成功,只要有一个操作失败,整个事务就失败,需要回滚。*比如银行转账,从A账户转100元至B账户,分为两个步骤:1)从A账户取100元;2)存入100元至B账户。这两步要么一起完成,要么一起不完成,如果只完成第一步,第二步失败,钱会莫名其妙少了100元。*
* Consistency一致性,也就是说数据库要一直处于一致的状态,事务的运行不会改变数据库原本的一致性约束。*例如现有完整性约束a+b=10,如果一个事务改变了a,那么必须得改变b,使得事务结束后依然满足a+b=10,否则事务*失败。
* Isolation独立性,所谓的独立性是指并发的事务之间不会互相影响,如果一个事务要访问的数据正在被另外一个事务修改,只要另外一个事务未提交,它所访问的数据就不受未提交事务的影响。*比如现在有个交易是从A账户转100元至B账户,在这个交易还未完成的情况下,如果此时B查询自己的账户,是看不到新增加的100元的。*
* Durability持久性,持久性是指一旦事务提交后,它所做的修改将会永久的保存在数据库上,即使出现宕机也不会丢失。
* 局限性和不适用场景
* 关系型数据库为了维护一致性所付出的巨大代价就是其读写性能比较差。在网页应用中,尤其是SNS(社交)应用中,一致性却不是显得那么重要,用户A看到的内容和用户B看到同一用户C内容更新不一致是可以容忍的,或者说,两个人看到同一好友的数据更新的时间差那么几秒是可以容忍的,因此,关系型数据库的最大特点在这里已经无用武之地,起码不是那么重要了。
* 关系数据库的另一个特点就是其具有固定的表结构,因此,其扩展性比较差,而在SNS中,系统的升级,功能的增加,往往意味着数据结构巨大变动,这一点关系型数据库也难以应付,需要新的结构化数据存储。
### **非关系型数据库 NoSQL(Not Only SQL )**
* NoSQL数据库的四大分类
|分类 |举例 |典型应用场景 |数据模型 |优点 |缺点 |
|--- |--- |--- |--- |--- |--- |
|键值(key-value)存储数据库 |Tokyo Cabinet/Tyrant, Redis, Voldemort, Oracle BDB |内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等等。 |Key 指向 Value 的键值对,通常用hash table来实现 |查找速度快 |数据无结构化,通常只被当作字符串或者二进制数据 |
|--- |--- |--- |--- |--- |--- |
|**列存储数据库** |Cassandra, HBase, Riak |分布式的文件系统 |以列簇式存储,将同一列数据存在一起 |查找速度快,可扩展性强,更容易进行分布式扩展 |功能相对局限 |
|**文档型数据库** |CouchDB, MongoDb |Web应用(与Key-Value类似,Value是结构化的,不同的是数据库能够了解Value的内容) |Key-Value对应的键值对,Value为结构化数据 |数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构 |查询性能不高,而且缺乏统一的查询语法。 |
|**图形(Graph)数据库** |Neo4J, InfoGrid, Infinite Graph |社交网络,推荐系统等。专注于构建关系图谱 |图结构 |利用图结构相关算法。比如最短路径寻址,N度关系查找等 |很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群方案。 |
* Mongodb
* 是文档型的非关系型数据库,使用bson( Binary Serialized Document Format) 结构。其优势在于查询功能比较强大,能存储海量数据。
# Mac OSX 平台安装Mongo
## 使用 brew 下载并安装
1、终端输入:brew install mongodb
2、安装成功后可以看到提示:
```
To have launchd start mongodb now and restart at login:
brew services start mongodb
Or, if you don't want/need a background service you can just run:
mongod --config /usr/local/etc/mongod.conf
==> Summary
🍺 /usr/local/Cellar/mongodb/4.0.0: 18 files, 268.4MB
```
## 运行mongoDB服务
1、首先创建一个数据库存储目录 /data/db,命令行输入:sudo mkdir -p /data/db
2、创建日志文件:sudo vim /data/db/mongo.log;创建pid文件:sudo vim /var/run/mongo.pid
3、启动 mongodb,分两种启动方式:
* 启动命令直接指定命令参数方式:sudo mongod -port 27017 -dbpath /data/db -fork -pidfilepath=/var/run/mongo.pid -logpath /data/db/mongo.log
* 启动命令指定配置文件方式:
* 1、创建配置文件 :sudo vim /etc/mongodb.conf
* 2、执行:sudo mongo -f /etc/mongodb.conf
* 启动参数说明(可以通过mongo -help来查看全部参数):
* -port arg #指定服务端口号,默认端口27017。
* -dbpath # 指定存储路径。
* -logpath arg # 指定MongoDB日志文件,注意是指定文件不是目录。使用fork参数时因为日志无法写到控制台,所以需要同时使用logpath参数。
* -pidfilepath arg # 指定PID File 的完整路径,如果没有设置,则没有PID文件。
* -fork # 以守护进程的方式运行MongoDB,相当于nohup “shell” &用法。使用fork参数时因为日志无法写到控制台,所以需要同时使用logpath参数。
* -directoryperdb # 设置每个数据库将被保存在一个单独的目录
* -maxConns arg # 最大同时连接数 默认2000
* -auth #用户认证,默认false,当设置为true时候,进入数据库需要auth验证,当数据库里没有用户,则不需要验证也可以操作。直到创建了第一个用户,之后操作都需要验证。
启动成功后可以看到以下提示:
```
[initandlisten] waiting for connections on port 27017
或
forked process: 41540
child process started successfully, parent exiting
```
## 停止mongoDB服务
正常关闭方式:
* 前台方式启动时,命令行光标键入CTRL+C
* 通过连接的客户端关闭,首先连接mongo成功后
* 第一步:use admin
* 第二步:db.shutdownServer()
异常关闭再次启动报错时:
* 使用kill进程方式
* ps -ef |grep mongo
* kill -15 pid #建议不要使用 ”kill -9 pid“,因为如果运行在没开启日志(—journal)的情况下,可能会造成数据损失。
> 注意:在mongodb的启动时,在数据目录下,会生成一个mongod.lock文件。如果在正常退出时,会清除这个mongod.lock文件,若是异常退出,在下次启动的时候,会禁止启动,并看到下面的报错。
```
exception in initAndListen: DBPathInUse: Unable to lock the lock file:
/data/db/mongod.lock (Resource temporarily unavailable). Another mongod instance is already running on the /data/db directory, terminating
```
## Mongo连接
### 客户端连接
语法格式:mongo 远程主机ip或DNS:端口号/数据库名 -u user -p password
例:连接本地数据库,在终端窗口输入:mongo
```
~ ᐅ mongo
MongoDB shell version v4.0.0
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 4.0.0
```
例:连接远程sit环境mongo数据库
```
~ ᐅ mongo 10.4.12.78/admin -u sit-user -p xxxxA5
MongoDB shell version v4.0.0
connecting to: mongodb://10.4.12.78:27017/admin
MongoDB server version: 3.2.12
```
### 标准uri连接
语法格式:mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
```
`#!/usr/bin/env python
``# -*- coding:utf-8 -*-
from pymongo import MongoClient
uri = 'mongodb://sit-user:xxxxqA5@10.4.12.78/admin'
con = MongoClient(uri)`
```
# MongoDB语法
## 概念术语
|SQL术语/概念 |MongoDB术语/概念 |解释/说明 |
|--- |--- |--- |
|database |database |数据库 |
|--- |--- |--- |
|table |collection |数据库表/集合 |
|row |document |数据记录行/文档 |
|column |field |数据字段 |
|index |index |索引 |
|primary key |primary key |主键,MongoDB自动将_id字段设置为主键 |
## 数据库操作
### 创建数据库
1、语法格式:use <database_name> #如果数据库存在时会切换到该数据库,不存在时会创建该数据库。
2、操作成功后可以看到提示:
```
> use sms
switched to db sms
```
### 删除数据库
1、语法格式:db.dropDatabase() #删除当前使用的数据库,可以通过命令“db”来查看当前数据库
2、操作成功后可以看到提示:
```
> db #查看当前数据库
sms
> db.dropDatabase()
{ "ok" : 1 }
```
## 集合操作
### 创建集合
1、语法格式:db.createCollection(<name>,{ <options>}) #name为要创建的集合名,options为可选参数:
* capped #布尔类型,如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。当该值为 true 时,必须指定 size 参数。
* size #数值,为固定集合指定一个最大值(以字节计)。如果 capped 为 true,也需要指定该字段**。**
* max #数值,指定固定集合中包含文档的最大数量。
2、执行:db.createCollection("seller",{ capped : true, size :6142800, max : 10000 },创建集合成功后可以看到提示:
```
> db #查看当前数据库
sms
> db.createCollection("seller",{ capped : true, size :6142800, max : 10000 })
{ "ok" : 1 }
```
### 删除集合
1、语法格式:db.collection_name.drop()
2、删除集合成功后,可以看到命令行返回“true”,否则返回“false”。
```
> show collections #查看当前数据库下的集合
ark_open_api_account
seller
> db.ark_open_api_account.drop()
true
> show collections
seller
```
## 文档操作
### 创建文档
1、语法格式:db.collection_name.insert(<document>)
2、执行db.seller.insert({"role" : "partner","shopname" : "redqa009-测试","email" : "[xx@163.com](mailto:xx@163.com)"})
```
> db.seller.insert({"role" : "partner","shopname" : "redqa009-测试","email" : "[xx@163.com](mailto:xx@163.com)"})
WriteResult({ "nInserted" : 1 })
> db.seller.find() #查询文档
{ "_id" : ObjectId("5b554bd275f30ccd39ca6483"), "role" : "partner", "shopname" : "redqa009-测试", "email" : "xx@163.com" }
```
或 先定义document后再执行insert(document)
```
> document=([{"role" : "partner","shopname" : "redqa007品牌店","email" : "1401261542@qq.com"},{"role" : "partner","shopname" : "redqa018","email" : "qatest6@redqa.xyz"}])
[
{
"role" : "partner",
"shopname" : "redqa007品牌店",
"email" : "1401261542@qq.com"
},
{
"role" : "partner",
"shopname" : "redqa018",
"email" : "[qatest6@redqa.xyz](mailto:qatest6@redqa.xyz)"
}
]
> db.seller.insert(document)
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 2,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
> db.seller.find()
{ "_id" : ObjectId("5b554bd275f30ccd39ca6483"), "role" : "partner", "shopname" : "redqa009-测试", "email" : "[xx@163.com](mailto:xx@163.com)" }
{ "_id" : ObjectId("5b554ee875f30ccd39ca6484"), "role" : "partner", "shopname" : "redqa007品牌店", "email" : "xx@qq.com" }
{ "_id" : ObjectId("5b554ee875f30ccd39ca6485"), "role" : "partner", "shopname" : "redqa018","email" : "xxx@redqa.xyz" }
```
### 更新文档
* update()方法
* 1、语法格式:
```
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>, # 可选,含义为如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
multi: <boolean>, # 可选,默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
writeConcern: <document> # writeConcern** **:可选,抛出异常的级别。
}
)
```
* 2、更新shopname为"redqa009-测试1”商家的邮箱
* 执行'db.seller.update({shopname:"redqa009-测试1"},{$set:{email:"xx@163.com"}},{upsert:true})'
```
> db.seller.find()
{ "_id" : ObjectId("5b554bd275f30ccd39ca6483"), "role" : "partner", "shopname" : "redqa009-测试", "email" : "lxx@163.com" }
{ "_id" : ObjectId("5b554ee875f30ccd39ca6484"), "role" : "partner", "shopname" : "redqa007品牌店", "email" : "xx@qq.com" }
{ "_id" : ObjectId("5b554ee875f30ccd39ca6485"), "role" : "partner", "shopname" : "redqa018", "email" : "xx@redqa.xyz" }
> db.seller.update({shopname:"redqa009-测试1"},{$set:{email:"xx@163.com"}},{upsert:true})
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("5b556dca30563a38f0284e00")
})
```
> 注意:当集合为Capped collection时,如果更新或替换操作更改了文档大小,则操作将失败。错误提示如下:
```
WriteResult({
"nMatched" : 0,
"nUpserted" : 0,
"nModified" : 0,
"writeError" : {
"code" : 10003,
"errmsg" : "Cannot change the size of a document in a capped collection: 79 != 77"
}
})
```
* save()方法
* 1、语法格式
```
db.collection.save(
<document>, # 根据文档中的'_id'字段,找到一个已经存在的文档,进行更新。
{
writeConcern: <document>
}
)
```
> 补充:
> 当文档中包含'_id'字段,但匹配不到已存在的文档,也会将文档插入数据库。
> 当文档中不含'_id'字段,save方法将调用insert方法,插入这条文档并分配一个_id。
### 删除文档
1、语法格式:
```
db.collection.remove(
<query>, # 可选,删除的文档的条件。
{
justOne: <boolean>, # 可选,如果设为 true 或 1,则只删除一个文档。
writeConcern: <document>
}
)
```
> 注意:Capped collection不允许使用remove()方法删除,只能使用db.collection.drop()方法删除集合。db.collection.isCapped() 命令可以查看一个集合是否是 Capped Collection。
### 查询文档
```
1、db.collection.find(query, projection) # 可选,query为查询条件,可选,projection指定返回的键。
```
### 比较操作符
* 大于:$gt
* 大于等于:$gte
* 小于:$lt
* 小于等于:$lte
* 等于:$eq
例:查询订单“total_discounted_price”大于等于100并且小于等于200的任意一个订单号。
```
db.order.findOne({total_discounted_price:{$gte:150,$lte:200}},{order_id:1})
#projection:1代表只返回指定字段,为0代表不返回该字段。
```
* 不等于:$ne
例:查询商家("seller":"53df5710b4c4d6383ae8e9a6")任意一个不是“已取消”的订单
```
db.order_package.find({"seller":"53df5710b4c4d6383ae8e9a6","status":{$ne:998}})
```
* 匹配数组中任意值:$in
例:查询所有订单状态状态为“待配货”、“配货中”或“已发货”的订单。
```
db.order_package.find({ "status": {$in:[4,5,6]}})
```
* 不匹配数组中任意值:$nin
### 逻辑操作符
* 与查询:$and
例:查询商家("seller":"53df5710b4c4d6383ae8e9a6")所有订单状态状态为“待配货”、“配货中”或“已发货”的订单。
```
db.order_package.find({$and:[{"seller":"53df5710b4c4d6383ae8e9a6"},{"status": {$in:[4,5,6]}}]})
等同于
db.order_package.find({ "seller":"53df5710b4c4d6383ae8e9a6","status": {$in:[4,5,6]}})
```
* 或查询:$or
例:查询订单“total_discounted_price”小于等于100或大于等于200的订单号。
```
db.order.find({$or:[{total_discounted_price:{$lte:100}},{total_discounted_price:{$gte:200}}]})
```
### 元素操作符
* 查询是否存在某字段:$exists
例:查询维护了贸易模式的所有商家。
```
db.seller.find({ "trade_mode":{$exists:true} })
```
* 查询数组中元素是否满足指定的条件:$elemMatch
例:查询所有支持“red_bonded”物流模式的商家。
```
被查询集合的数据结构:
{
"_id": ObjectId("5a151d9deb90b912e76ee832"),
"shopname": "redqa009-测试",
"logistics_infos": [
{
"name": "red_auto",
"logistics": "auto",
"is_default": true
},
{
"name": "red_bonded",
"logistics": "bonded",
"customs_code": "SHANGHAI",
"is_default": false
}
],
"trade_mode": 0
}
写法:
db.seller.find({ "logistics_infos":{$elemMatch:{name:"red_bonded"}} })
```
### 排序
* 升序
```
db.COLLECTION_NAME.find().sort({KEY:1})
```
* 降序
```
db.COLLECTION_NAME.find().sort({KEY:-1})
```
# MongoDB访问权限控制
## 访问控制参数
### **绑定IP地址**
* mongod 启动参数:-bind_ip <ip_address>
默认值是所有的IP地址都能访问,该参数指定MongoDB对外提供服务的绑定IP地址,用于监听客户端 Application的连接,客户端只能使用绑定的IP地址才能访问mongod,其他IP地址是无法访问的。
### **设置监听端口**
* mongod 启动参数:-port <port>
MongoDB 默认监听的端口是27017,该参数显式指定MongoDB实例监听的TCP 端口,只有当客户端Application连接的端口和MongoDB实例监听的端口一致时,才能连接到MongoDB实例。
### **启用用户验证**
* mongod 启动参数:-auth
当mongod 使用该参数启动时,MongoDB会验证客户端连接的账户和密码,以确定其是否有访问的权限。如果认证不通过,那么客户端不能访问MongoDB的数据库。
### **权限认证**
* mongo 连接参数:-username <username>, -u <username>
* mongo 连接参数:-password <password>, -p <password>
* mongo 连接参数:-authenticationDatabase <dbname> 指定创建User的数据库;在特定的数据库中创建User,该DB就是User的authentication database。
在连接mongo时,使用参数 --authenticationDatabase,会认证 -u 和 -p 参数指定的账户和密码。如果没有指定验证数据库,mongo使用连接字符串中指定的DB作为验证数据块。
```
mongo 10.4.12.78/admin -u sit-user -p xxxx4qA5 --authenticationDatabase "admin"
```
## **基于角色的访问控制**
**内置角色**
内置角色是MongoDB预定义的角色,操作的资源是在DB级别上。MongoDB拥有一个SuperUser的角色:root,拥有最大权限,能够在系统的所有资源上执行任意操作。
* 数据库内置用户角色
* read:授予User只读数据的权限
* readWrite:授予User读写数据的权限
* 数据库内置管理角色
* dbAdmin:在当前dB中执行管理操作
* dbOwner:在当前DB中执行任意操作
* userAdmin:在当前DB中管理User
* 所有数据库角色
* readAnyDatabase
* readWriteAnyDatabase
* userAdminAnyDatabase
* dbAdminAnyDatabase
* 超级用户角色
* root
### 用户创建
```
> db.createUser({user: "jhh",pwd: "pwd",roles: [{role:"readWrite",db:"sms"}]})
Successfully added user: {
"user" : "jhh",
"roles" : [
{
"role" : "readWrite",
"db" : "sms"
}
]
}
```
> 补充:查看用户授权情况可以在admin库下通过db.system.users.find({user:"user_name"})查看。
### **用户的作用范围**
在admin 数据库中创建的角色,作用范围是全局的,能够在admin,其他数据库中使用,并且能够继承其他数据库的角色;而在非admin中创建的角色,作用范围是当前数据库,只能在当前DB中使用,只能继承当前数据库的角色。
# MongoDB备份与恢复
MongoDB官方提供了两套数据导入导出工具:一般来说,进行整库导出导入时使用mongodump和mongostore,这一对组合操作的数据是BSON格式,进行大量dump和restore时效率较高。进行单个集合导出导入时使用mongoexport和mongoimport,这一对组合操作的数据是JSON格式,可读性较高。
## 数据库备份与恢复
* mongodump脚本语法:mongodump -h dbhost -d dbname -o dbdirectory
* -h:
MongDB所在服务器地址,例如:127.0.0.1,当然也可以指定端口号:127.0.0.1:27017
* -d:
需要备份的数据库实例,例如:test
* -o**:**
备份的数据存放位置,例如:\data\dump,当然该目录需要提前建立,在备份完成后,系统自动在dump目录下建立一个test目录,这个目录里面存放该数据库实例的备份数据。
* mongorestore脚本语法:mongorestore -h <hostname><:port> -d dbname <path>
* --host <:port>, -h <:port>:
MongoDB所在服务器地址,默认为: localhost:27017
* --db , -d :
需要恢复的数据库实例,例如:test,当然这个名称也可以和备份时候的不一样,比如test2
* --drop:
恢复的时候,先删除当前数据,然后恢复备份的数据。
* <path>:
mongorestore 最后的一个参数,设置备份数据所在位置,例如:\data\dump\test。
你不能同时指定 <path> 和 --dir 选项,--dir也可以设置备份目录。
* --dir:
指定备份的目录
你不能同时指定 <path> 和 --dir 选项。
## 集合导入与导出
* mongoexport语法:mongoexport -d dbname -c collectionname -o file --type json/csv -f field
* -h:代表远程连接的数据库地址,默认连接本地Mongo数据库
* --port:代表远程连接的数据库的端口,默认连接的远程端口27017
* -d :数据库名
* -c :collection名
* -o :输出的文件名,导出成功后文件会生成在当前执行路径下。
* --type : 输出的格式,默认为json
* -f :输出的字段,如果-type为csv,则需要加上-f "字段名"。
* -q:代表查询条件
* -limit:读取指定数量的数据记录
例:从sit环境数据数据库导出sms库里seller集合任意5文档到本地。
```
~ ᐅ mongoexport -h=10.4.12.78 -d=sms -c=seller -o=seller.json
--limit=5 -u sit-user -p cDk%KVJA4qA5 --authenticationDatabase=admin --query '{logistics:"auto"}'
2018-07-24T12:50:18.992+0800 connected to: 10.4.12.78
2018-07-24T12:50:19.037+0800 exported 5 records
```
> 注意:这里一定要加—authenticationDatabase=admin指定use和password作为admin库的验证,顺序直接跟在-u和-p参数之后,否则会作为 sms库的验证,导致验证失败(猜测sit-user是admin库下全局用户,在sms的库用户中不存在)。具体见“角色作用范围”一节。
* mongoimport语法:mongoimport -d dbname -c collectionname --file filename --headerline --type json/csv -f field
* -d :数据库名
* -c :collection名
* --type :导入的格式默认json
* -f :导入的字段名,type为json格式时不能指定。
* --headerline :如果导入的格式是csv,则可以使用第一行的标题作为导入的字段
* --file :要导入的文件
例:将上面导出的seller.json文件导入到本地数据库sms下。
```
~ ᐅ mongoimport -d sms -c seller --file seller.json --type json
2018-07-24T13:39:05.380+0800 connected to: localhost
2018-07-24T13:39:05.394+0800 imported 5 documents
```
客户端查看seller集合文档条数:
```
导入执行前:
> db.seller.find().count();
2
导入执行后:
> db.seller.find().count();
7
```
# 数据库与缓存之间更新机制
## **缓存同步的常用模式**
缓存同步的模式,可以按照缓存的用途(主要用于读或者写)分为两类:读缓存的同步和写缓存的同步。
读缓存的同步:
### **缓存预加载模式**
提前将数据从数据库加载到缓存,如果数据库有写更新,同步更新缓存。
### **缓存直读模式**
应用先查看缓存中是否有该数据,有则直接使用,如果没有,从数据库加载,然后放入缓存,下次以后再访问就可以直接从缓存中获得。
### **缓存直写模式**
在数据更新时,同时写入缓存和数据库。这种模式是最稳妥的办法,但是性能会受到一定的影响。
其中的过程是这样的:
1.检查用户请求的数据是缓存中是否有存在,如果有存在的话,只需要直接把请求的数据返回,无需查询数据库。
2.如果请求的数据在缓存中找不到,这时候再去查询数据库。返回请求数据的同时,把数据存储到缓存中一份。
3.保持缓存的“新鲜性”,每当数据发生变化的时候(比如,数据有被修改,或被删除的情况下),要同步的更新缓存信息,确保用户不会在缓存取到旧的数据。