MongoDB - The Definitive Guide
<<MongoDB - The Definitive Guide>>的中文版<<mongodb权威指南>>读书笔记
01 简介
MongoDB是面向文档的数据库,不是关系型数据库。基本思路是将原来 row的概念换成更加灵活的 document 模型。
MongoDB从最初设计的时候就考虑到扩展的问题,它所采用的面向文档的数据模型使其可以自动在多台服务器之间分割数据。它还可以平衡集群的数据和负载,自动重排文档。
支持功能:索引,存储JavaScript, 聚合,固定集合,文件存储
02 入门
文档(document) 是MongoDB的核心概念。多个键及关联的值有序地放在一起便是文档。
每种编程语言表示文档的方法不太一样,但大多数编程语言都有相通的一种数据结构,比如map, hash或dictionary.在JavaScript中,文档表示为json对象
文档的key是字符串,区分类型和大小写。
集合(collection) 就是一组文档。组织集合的一种惯例是使用”.”字符分开的按命名空间划分的子集合。
GridFS是一种存储大文件的协议,使用子集合来存储文件的元数据,这样就与内容块分离。
MongoDB中多个文档组成集合,多个集合组成 数据库。数据库名最终变成文件系统里的文件。
mongod是数据库服务器程序,默认监听27017端口;并在28017端口监听,以提供web admin interface
mongo是一个JavaScript shell,可以通过命令行与MongoDB实例交互。shell是一个独立的MongoDB客户端,开启的时候,shell会连到MongoDB服务器的test数据库,并将该数据库
连接赋值给全局变量db。该变量是通过shell访问MongoDB的主要入口点。
在shell中操作数据会用到4个基本操作:创建,读取,更新和删除(CRUD)
insert,
findOne,
update,
remove
MongoDB的文档类似于JSON。Json只有null, boolean, number, string, array, object 6种类型。MongoDB在保留JSON基本的KEY/VALUE特性的基础上,添加了一些数据类型:
- null
- boolean
- 32 bit integer
- 64 bit integer
- string
- ObjectId
- Date
- regular expression
- js code
- binary
- undefined
- array
- nested document
MongoDB中存储的文档必须有一个”_id” key. ObjectId是”_id”的默认类型。它被设计成轻量型,不同的机器都能用全局唯一的同种方法方便地生成它。
MongoDB从一开始就设计用来作为分布式数据库,处理多个节点是一个核心要求。ObjectId使用12字节的存储空间,使用24个16进制的数字表示。
12字节按照如下方式生成:
0 1 2 3 - 4 5 6 - 7 8 - 9 10 11
时间戳 机器 PID 计数器
03 创建,更新及删除文档
当执行插入时,使用的驱动程序会将数据转换成BSON的形式,然后将其送入数据库。
MongoDB在插入时并不执行代码,所以这块没有注入式攻击的可能。
通常文档只会有一部分要更新,利用原子的 更新修改器 , 可以使得这种部分更新极为高效。更新修改器是种特殊的键,用来指定复杂的更新操作,比如调整,增加或者删除键,还可能是操作数组或者内嵌文档。
- “$set”
用来指定一个键的值,如果键不存在,则创建它。这对更新模式或者增加用户定义键来说非常方便。增加,修改或删除键的时候,应该用$修改器。 - “$inc”
用来增加已有键的值,或者在键不存在时创建一个键。对于分析数据,因果关系,投票或者其他有变化数值的地方,使用这个都会非常方便。
“$inc”与”$set”的用法类似,专门用来增加/减少数字。其只能用于整数,长整数或双精度浮点数。“$inc”键的值必须为数字。 - 数组修改器
“$push”向已有的数组末尾加入一个元素,要是没有就会创建一个新的数组。
upsert是一种特殊的更新。要是没有文档符合更新条件,就会以这个条件和更新文档为基础创建一个新的文档。 如果找到匹配的文档,则正常更新。使用upset,即可以避免竞态问题,又可以缩减代码量(update的第三个
参数表示这是个upsert)
默认情况下,更新只能对符合匹配条件的第一个文档执行操作。要是有多个文档符合条件,其余的文档没有变化。要使所有匹配的文档都得到更新,可以设置update的第4个参数为true.
多文档更新对模式迁移非常有用,还可以在对特定用户发布新功能时使用。
04 查询
MongoDB中使用find来进行查询。 查询就是返回一个集合中文档的子集,子集合的范围从0个文档到整个集合。find的第一个参数决定了要返回哪些文档,其形式也是一个文档,说明要执行的查询细节。
有时不需要将文档中所有的key/value返回,可以通过find/findOne的第二个参数指定想要的key.1
db.users.find({}, {"username" : 1, "email" : 1})
查询不仅能像前面说的那样精确匹配,还能匹配更加复杂的条件,比如范围,OR字句和取反。
“$lt”, “$lte”, “$gt”, “$gte” 就是全部的比较操作符,分别对应<, <=, >和>=
例如,查询在18 ~ 30岁(含)的用户:1
db.users.find({"age" : {"$gte" : 18, "$lte" : 30}})
“$ne” 表示”不相等”
“$in” 可以用来查询一个键的多个值
“$or” 更通用,用来完成多个键值的任意给定值。
例如,抽奖活动的中奖号码时725, 542和390,要找出全部中奖数据:1
2db.raffle.find({"ticket_no" : {"$in" : [725, 542, 390]}})
db.raffle.find({"$or" : [{"ticket_no" : 725}, {"winner" : true}]})
MongoDB使用Perl兼容的正则表达式(PCRE)库来匹配正则表达式,PCRE支持的正则表达式语法都能被MongoDB所接收。
查询数组中的元素也是非常容易的,数组绝大多数情况下可以这样理解:每一个元素都是整个key的value.
“$all”可以用来通过多个元素匹配数组。
“$size”可以用其查询指定长度的数组。1
db.food.find({"fruit" : {"$size" : 3}})
“$slice” 返回数组的一个子集. 例如假设现在有一个博客文章的文档,返回前10条评论:1
db.blog.posts.findOne(criteria, {"comments" : {"$slice" : 10}})
“$where” 用它可以执行任意JavaScript作为查询语句。
数据库使用 游标 返回find的执行结果。
limit(), skip(), sort()
例如:你有一个在线商店,有人想搜索mp3,如是想每页返回50个结果,而且按照价格从高到底排序,可以这样写:1
db.stock.find({"desc" : "mp3"}).limit(50).sort({"price" : -1})
点击”下一页”可以看到更多的结果,通过skip可以非常简单实现:1
db.stock.find({"desc" : "mp3"}).limit(50).skip(50).sort({"price" : -1})
使用skip略过少量的文档还是不错的,但是数量非常多的话,skip会变得很慢,所以要尽量避免。
05 索引
索引就是用来加速查询的。数据库索引与书籍的索引类似:有了索引就不需要翻遍整本书,数据库则可以直接在索引中查找,使得查找速度能提高几个数量级。在索引中找到条目后,就可以直接
跳转到目标文档的位置。
MongoDB的索引几乎与传统的关系型数据库索引一模一样,绝大多数优化MySQL/Oracle/SQLite索引的技巧也同样适用于MongoDB.
创建索引使用ensureIndex方法:
1 | db.people.ensureIndex({"username" : 1}) |
创建索引的缺点就是每次插入,更新和删除时都会产生额外的开销。 这是因为数据库不但需要执行这些操作,还要将这些操作在集合的索引中标记。因此,要尽可能少创建索引。每个集合
默认的最大索引个数是64个。 一定不要索引每一个KEY.
建立索引时需要考虑如下问题:
- 会做什么样的查询?其中哪些键需要索引?
- 每个键的索引方向是怎样的?
- 如何应对扩展?有没有种不同的键的排列可以使常用数据更多地保留在内存中。
06 聚合
MongoDB除了基本的查询功能,还提供了很多强大的聚合工具,其中简单的可计算集合中的文档个数,复杂的可利用MapReduce做复杂的数据分析。
count
返回集合中的文档数量distinct
找出给定键的所有不同的值,使用时必须指定集合和键。group
先选定分组所依据的键,而后MongoDB就会将集合依据选定键值的不同分成若干组。然后可以通过聚合每一组内的文档,产生一个结果文档。
MapReduce是一个可以轻松并行化到多个服务器的聚合方法。它会拆分问题,再将各个部分发送到不同的机器上,让每台机器都完成一部分。当所有机器都完成的时候,再将结果汇集起来形成最终完整的结果。
07 进阶值南
- 数据库命令
除了”CRUD”,其他的功能都是作为命令实现的。
db.runCommand({“drop” : “test”})
MongoDB中的命令其实是作为一种特殊类型的查询来实现的,这些查询针对$cmd集合来执行。runCommand仅仅是接受命令文档,执行等价查询,因此drop命令其实是:
db.$cmd.findOne({“drop” : “test”})
要获得所有命令的最新列表,有两种方式:
- 在shell中运行db.listCommands(),或者从驱动程序中运行等价的命令list-Commands
- 浏览管理员接口http://localhost:28017/_commands
- 固定集合
固定集合很像环形队列,如果空间不足,最早的文档就会被删除,为新的文档腾出空间。固定集合在新文档插入的时候自动淘汰最早的文档。
- 对固定集合进行插入速度极快
- 按照插入顺序输出的查询速度极快
- 固定集合能够在新数据插入时,自动淘汰最早的数据。
插入快速,按照插入顺序查询也快速,自动淘汰,这几样组合使得固定集合特别适合像 日志 这种应用场景。
- GridFS
GridFS是一种在MongoDB中存储大二进制文件的机制。使用GridFS存文件有如下几个原因:
- 利用GridFS可以简化需求。要是已经使用了MongoDB,GridFS就可以不需要使用独立文件存储架构
- GridFS会直接利用已建立的复制或分片机制,所以对于文件存储来说故障恢复和扩展都很容易
- GridFS可以避免用于存储用户上传内容的文件系统出现的某些问题。例如,GridFS在同一个目录下放置大量的文件是没有任何问题的。
- GridFS不产生磁盘碎片,因为MongoDB分配数据文件空间时以2GB为一块
最简单的使用GridFS的方法就是利用mongofiles实用程序。
GridFS是一个建立在普通MongoDB文档基础上的轻量级文件存储桂芳。GridFS的一个基本思想就是可以将大文件分成很多块,每块作为一个单独的文档存储。
除了存储文件本身的块,还有一个单独的文档用来存储分块的信息和文件的元数据.
服务器端脚本
在服务器端可以通过db.eval函数来执行JavaScript脚本。也可以把JavaScript脚本保存在数据库中,然后在别的数据库命令中调用。数据库引用
DBRef就像URL,唯一确定一个到文档的引用。
08 管理
执行mongod,启动MongoDB服务器。 MongoDB支持从文件获取配置信息,当需要的配置非常多,或者需要自动启动MongoDB时使用。
当mongod收到SIGINT(kill -2 pid)或者SIGTERM(kill pid)时,会安全退出。也就是会等到当前运行的操作或者文件预分配完成(需要一些时间),关闭所有打开的连接,将缓存的数据刷新到磁盘,最后停止。
千万不要向运行中的MongoDB发送SIGKILL(kill -9),这样会导致数据库直接关闭,上面讲到的步骤都将会被忽略,这会使数据文件损毁。
默认情况下,启动mongod时还会启动一个基本的HTTP服务器,该服务器监听的端口号比主服务的端口号大1000。这个服务器提供了HTTP接口,可以查看MongdoDB的一些基本信息。默认情况下,访问http://localhost:28017就能
看见管理接口。
要获取运行中的MongoDB服务器统计信息,最基本的工具就是serverStatus命令 : db.runCommand({“serverStatus” : 1}),也可以使用mongostat 命令行工具查看serverStatus的结果.
原始的统计信息同样可以由HTTP接口以JSON的形式得到,位置在http://localhost:28017/_status
- 备份和修复
MongoDB将所有数据都存放在数据目录下。在运行时复制数据目录不太安全,所以必须先关闭服务器,再复制数据目录。虽然关停服务器再复制数据目录做备份很有效,也很安全,但还是不太理想。
不停机备份的方式:
mongodump 是一种能在运行时备份的方法。mongodump对运行的MongoDB做查询,然后将所有查到的文档写入磁盘。
mongodump使用普通的查询机制,所以产生的备份不一定是服务器数据的实时快照。服务器在备份过程中处理写入时尤为明显。
mongodump还带来个问题,备份时的查询会对其他客户端的性能产生不利影响
除了mongodump,MongoDB还提供了从备份中恢复数据的工具mongorestore。mongorestore获取mongodump的输出结果,并将备份的数据插入到运行的MongoDB实例中。
虽然用mongodump和mongorestore能不停机备份,但是我们却失去了获取实时数据试图的能力。MongoDB的fsync命令能在MongoDB运行时复制数据目录还不会损毁数据。fsync命令会强制服务器将所有缓冲区写入磁盘。还可以选择上锁阻止对数据库的进一步写入,直到释放锁为止:
use admin
db.runCommand({“fsync” : 1, “lock” : 1})
备份好了,需要解锁:
db.$cmd.sys.unlock.findOne()
db.currentOp()
有了fsync命令,就能非常灵活地备份,不用停掉服务器,也不用牺牲备份的实时特性。要付出的代价就是一些写入操作被暂时阻塞了。
唯一不耽误读写还能保证实时快照的备份方式就是通过 从服务器备份
当以复制的方式运行MongoDB时,前面提到的备份技术就不仅能用在主服务器上,也可以用在从服务器上。在从服务器上备份是MongoDB推荐的备份方式。
09 复制
MongoDB管理员最重要的工作莫过于确保复制设置正确,且运转良好。不仅可以用复制来应对故障切换,数据集成,还可以用来做读扩展,热备份或者作为离线批处理的数据源。
- 主从复制
主从复制是MongoDB最常用的复制方式。这种方式非常灵活,可用于备份,故障恢复,读扩展等。
mongod –master 就启动了主服务器,运行mongod –slave –source master_address则启动了从服务器,master_address就是主节点的地址.
$ mkdir -p ~/dbs/master
$ ./mongod –dbpath ~/dbs/master –port 10000 –master
$ mkdir -p ~/dbs/slave
$ ./mongod –dbpath ~/dbs/slave –port 10001 –slave –source localhost:10000
- 副本集(Replica Set)
简单的说,副本集是有自动故障恢复功能的主从集群。主从集群和副本集最为明显的区别是副本集没有固定的“主节点”,整个集群会选举出一个“主节点”,当其不能工作时则会变更到其他节点。副本集最美妙的地方是所有东西都是自动化的。
从节点的主要作用是作为故障恢复机制,以防主节点数据丢失或者停止服务。
主节点的操作记录称为oplog(operation log的简写)。oplog存储在一个特殊的数据库中,叫做local. oplog中的每个文档都代表主节点上执行的一个操作。oplog只记录改变数据库状态的操作,比如,查询就不在存储在oplog中。这是因为oplog只是作为从节点与主节点保持数据同步的机制。
10 分片
分片是MongoDB的扩展方式。通过分片能够增加更多的机器来应对不断增长的负载和数据,还不影响应用。
分片(sharding)是指将数据拆分,将其分散存在不同的机器上的过程。MongoDB支持自动分片,可以摆脱手动分片的管理困扰。集群自动切分数据,做负载均衡。
MongoDB分片的基本思想就是将集合切分为小块。这些块分散到若干片里面,每个片只负责总数据的一部分。应用程序不必知道哪片对应哪些数据,甚至不需要知道数据已经被拆分,所以在分片之前要运行一个路由进程mongos. 这个路由器知道所有数据的存放位置,所以应用可以连接它来正常发送请求。
对应用来说,它仅知道连接了一个普通的mongod。路由器知道数据和片的对应关系,能够转发请求到正确的片上。如果请求有了回应,路由器将其收集起来回送给应用。
从应用角度看,分片不分片没什么差别。所以需要扩展的时候,不必修改应用程序的代码。
分片时机:一般来说,先要从不分片开始,然后在需要时将其转换成分片的。
- 机器的磁盘不够用
- 单个mongod已经不能满足写数据的性能需要
- 想将大量数据放在内存中提高性能
设置分片时,需要从集合里面选一个键,用该键的值作为数据拆分的依据。这个键称为片键(shard key)
- 建立分片
建立分片有两步:启动实际的服务器,然后决定怎么切分数据.
分片一般会有3个组成部分:
片
片就是保存子集合数据的容器。片可以是单个的mongod服务器(开发和测试用),也可以是副本集(生产用).mongos
mongos就是MongoDB各版本中都配的路由器进程。它路由所有请求,然后将结果聚合。它本身并不存储数据或者配置信息配置服务器
配置服务器存储集群的配置信息:数据和片的对应关系。mongos步永久存放数据,所以需要个地方存放分片配置,它会从配置服务器获取同步数据。
首先启动配置服务器和mongos,配置服务器需要最先启动,因为mongos会用到其上的配置信息。配置服务器的启动就像普通mongod一样。
$ mkdir -p ~/dbs/config
$ ./mongod –dbpath ~/dbs/config –port 20000
$ ./mongos –port 30000 –configdb localhost:20000
添加片,片就是普通的mongod实例:
$mkdir -p ~/dbs/shard1
$ ./mongod –dbpath ~/dbs/shard1 –port 10000
连接刚才启动的mongos,为集群添加一个片
$ ./mongo localhost:30000/amdin
db.runCommand({addshard : “localhost:10000”, allowLocal : true})
当在localhost上运行片时,得设定”allowLocal”键. MongoDB尽量避免由于错误配置,将集群配置到本地,所以需要让它知道这仅仅时开发模式。如果是生产环境中,则要将其部署到不同机器上。
想添加片的时候,就运行addshard,MongoDB会负责将片集成到集群。
切分数据,先开启数据库的分片功能
db.runCommand({“enablesharding” : “foo”})
然后对集合分片:
db.runCommand({“shardcollection” : “foo.bar”, “key” : {“_id” : 1}})
这样集合将按照”-id”分片,后续添加的数据,就会依据”_id”的值自动分散到各个片上。
应用进入产品环境后,就需要更加健壮的方案,成功的构建分片需要如下条件:
- 多个配置服务器
- 多个mongos服务器
- 每个片都是副本集
- 正确设置w
11 应用举例
Java中的文档必须是org.bson.DBObject的实例。
mongod无处不在,可以作为单个服务器,主从节点,副本集的成员,还可以当作片。