【数据存储系列】手牵手学习MongoDB
互联网环境,数据是第一生产力,数据的存储至关重要,非关系型数据库是为了解决关系型数据库的一些弊端而出现的。其中Mongo是非关系数据库当中功能最丰富,最像关系数据库的。
MongoDB基本使用
Nosql简介
NoSQL(NoSQL = Not Only SQL ),意即”不仅仅是SQL”。
在现代的计算系统上每天网络上都会产生庞大的数据量, 这些数据有很大一部分是由关系数据库管理系统(RDBMS)来处理。 1970年 E.F.Codd’s提出的关系模型的论文 “A relational model of data for large shared data banks”,这使得数据建模和应用程序编程更加简单。
通过应用实践证明,关系模型是非常适合于客户服务器编程,远远超出预期的利益,今天它是结构化数据存储在网络和商务应用的主导技术。
NoSQL 是一项全新的数据库革命性运动,早期就有人提出,发展至2009年趋势越发高涨。NoSQL的拥护者们提倡运用非关系型的数据存储,相对于铺天盖地的关系型数据库运用,这一概念无疑是一种全新的思维的注入。
什么是NoSQL
NoSQL,指的是非关系型的数据库。NoSQL有时也称作Not Only SQL的缩写,是对不同于传统的关系型数据库的数据库管理系统的统称。
NoSQL用于超大规模数据的存储。(例如谷歌或Facebook每天为他们的用户收集万亿比特的数据)。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。
为什么使用NoSQL
今天我们可以通过第三方平台(如:Google,Facebook等)可以很容易的访问和抓取数据。用户的个人信息,社交网络,地理位置,用户生成的数据和用户操作日志已经成倍的增加。我们如果要对这些用户数据进行挖掘,那SQL数据库已经不适合这些应用了, NoSQL 数据库的发展却能很好的处理这些大的数据
RDBMS Vs NoSQL
RDBMS
- 高度组织化结构化数据
- 结构化查询语言(SQL)
- 数据和关系都存储在单独的表中。
- 数据操纵语言,数据定义语言
- 严格的一致性
- 基础事务
NoSQL
- 代表着不仅仅是SQL
- 没有声明性查询语言
- 没有预定义的模式
- 键 - 值对存储,列存储,文档存储,图形数据库
- 最终一致性,而非ACID(原子性、一致性、隔离性、持久性)属性
- 非结构化和不可预知的数据
NoSQL的优缺点
优点
- 高可扩展性
- 分布式计算
- 低成本
- 架构的灵活性,半结构化数据
- 没有复杂的关系
缺点
- 没有标准化
- 有限的查询功能(到目前为止)
- 最终一致是不直观的程序
分布式理论
CAP定理
在计算机科学中, CAP定理(CAP theorem), 又被称作 布鲁尔定理(Brewer’s theorem), 它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
- 一致性(Consistency) (所有节点在同一时间具有相同的数据)
- 可用性(Availability) (保证每个请求不管成功或者失败都有响应)
- 分区容错性(Partition tolerance) (系统中任意信息的丢失或失败不会影响系统的继续运作)
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,最多只能同时较好的满足两个。
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
BASE理论
BASE:Basically Available, Soft-state, Eventually Consistent。 由 Eric Brewer 定义。
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,最多只能同时较好的满足两个。
BASE是NoSQL数据库对可用性及一致性的弱要求原则:
- Basically Availble –基本可用
- Soft-state –软状态/柔性事务。 “Soft state” 可以理解为”无连接”的, 而 “Hard state” 是”面向连接”的
- Eventual Consistency – 最终一致性, 也是 ACID 的最终目的。
MongoDB基础
MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统。
- 在高负载的情况下,添加更多的节点,可以保证服务器性能。
- MongoDB 旨在为WEB应用提供可扩展的高性能数据存储解决方案
什么是MongoDB
存储结构
MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。
主要特点
- 非关系型数据库,基于 Document data model(文档数据模型)
- MongoDB以 BSON (BinaryJSON) 格式存储数据,类似于 JSON 数据形式
- 关系型数据库使用 table (tables of rows)形式存储数据,而MongoDB使用 collections (collections of documents)
- 支持 临时查询(ad hoc queries): 系统不用提前定义可以接收的查询类型
- 索引通过 B-tree 数据结构, 3.2版本的WiredTiger 支持 log-structured merge-trees(LSM)
- 支持索引和次级索引(secondary indexes): 次级索引是指文档或row有一个 主键(primary key)作为索引,同时允许文档或row内部还拥有一个索引,提升查询的效率,这也是MongoDB比较大的一个特点
MongoDB安装
下载MongoDB
查找MongoDB安装包
到MongoDB地址下载 https://www.mongodb.com/try/download/community linux安装包,并选择对应的版本,点击copy link得到地址就可以通过linux环境wget 进行下载了
下载MongoDB
通过wget命令下载刚才在页面copy的链接进行下载
安装wget
1 | yum -y install wget |
解压安装包
1 | tar -zxvf mongodb-linux-x86_64-rhel70-4.4.5.tgz |
重命名文件夹
1 | mv mongodb-linux-x86_64-rhel70-4.4.5 mongodb4.4.5 |
配置环境变量
这里根据自己对应的mongodb路径配置,将我们的MongoDB的bin目录配置到系统环境中
1 | vi /etc/profile |
在 export PATH USER LOGNAME MAIL HOSTNAME HISTSIZE HISTCONTROL 一行的上面添加如下内容
1 | #设置 Mongodb环境变量 |
保存后通过下面的命令使环境变量生效
1 | source /etc/profile |
安装MongoDB
准备工作
Linux下我们使用tgz格式的安装包进行安装,没有像windows那样可以使用msi进行简易安装,所以,它这个包是不全的,我们需要进入mongodb目录再手动创建两个目录,data和log,data目录是用于存放数据的,log目录是用于存放日志文件的
1 | mkdir data logs |
创建配置文件
因为该安装包不包含配置文件,我们需要去bin目录下面写一个mongodb的配置文件
1 | vi mongodb.conf |
这里面的数据文件以及日志路径就是我们刚才创建的目录的路径
1 | #端口号 默认为27017 |
使用系统服务启动
创建启动脚本
在/etc/init.d/路径下创建mongod的启动脚本
1 | vi /etc/init.d/mongod |
注意将MONGODB_HOME路径改为mongodb的安装路径
1 | !/bin/sh |
设置权限
设置该脚本拥有执行权限
1 | chmod 755 /etc/init.d/mongod |
启动MongoDB
使用如下命令启动MongoDB
1 | # 启动mongodb |
出现如下就提示表示mongodb已经启动成功
访问测试
输入mongo命令使用本地客户端进行访问
1 | mongo |
出现如下界面表示登录mongodb成功
查询所有的数据库
1 | show dbs; |
关闭防护墙
如果需要mongoDB进行外部访问需要开放防火墙端口,因为我们使用了虚拟机所以直接关闭防火墙
检查防火墙状态
使用下面的命令可以检查防火墙状态
1 | systemctl status firewalld.service |
然后在下方可以查看得到“active(running)”,此时说明防火墙已经打开了
停止防火墙
在命令行中输入systemctl stop firewalld.service命令,进行关闭防火墙
1 | systemctl stop firewalld.service |
然后再使用命令systemctl status firewalld.service,在下方出现disavtive(dead),这样就说明防火墙已经关闭。
永久关闭防火墙
再在命令行中输入命令“systemctl disable firewalld.service”命令,即可永久关闭防火墙
1 | systemctl disable firewalld.service |
这样下次启动,防火墙就不会开启了
优雅关机
在生产环境,不要用 kill -9 关掉 mongodb 的进程,很可能造成 mongodb 的数据丢失;可以使用以下方式进行优雅关机
1 | use admin |
基本概念
和传统数据库对比
不管我们学习什么数据库都应该学习其中的基础概念,在mongodb中基本的概念是文档、集合、数据库,下面我们分别介绍,下表将帮助您更容易理解Mongo中的一些概念
通过下图实例,我们也可以更直观的了解Mongo中的一些概念:
数据逻辑层次关系:文档=>集合=>数据库
下面我们对里面的每一个概念进行详细解释
数据库
一个mongoDB的实例可以运行多个database,database之间是完全独立的,每个database有自己的权限,每个database存储于磁盘的不同文件。
命令规范
databases的name可以是任意的UTF-8字符串。但是有以下限制
- 空字符串
””
是非法的 - 不允许出现
’’,.,$,/,\,\0
字符 - 建议名称都是小写
- 不能超过64个字节
特殊数据库
有一些数据库名是保留的,可以直接访问这些有特殊作用的数据库。
- admin:它是root级别的数据库,如果一个用户创建了admin数据库,该用户将自动集成所有数据库的权限,它可以执行一些服务器级别的命令,如列出所有数据库、关闭服务等。
- local:该数据库将永远不能被复制,只能在单台服务器本地使用。
- config:存储分布式部署时shard的配置信息
数据库操作
查看数据库列表
show dbs 命令可以显示所有数据的列表
1 | show dbs; |
显示当前数据库
执行 “db” 命令可以显示当前数据库对象或集合。
1 | db |
创建数据库
MongoDB 使用 use 命令创建数据库,如果数据库不存在,MongoDB 会在第一次使用该数据库时创建数据库。如果数据库已经存在则连接数据库,然后可以在该数据库进行各种操作。
1 | show dbs; |
注意: 在 MongoDB 中,只有在数据库中插入集合后才会创建! 就是说,创建数据库后要再插入一个集合,数据库才会真正创建。
1 | use tmpdb; |
现在 tmpdb 数据库就显示出来了
删除数据库
可以使用 db.dropDatabase() 删除数据库
1 | show dbs; |
集合
相当于关系数据库的表,不过没有数据结构的定义。它由多个document组成。
命令规范
因为是无结构定义的,所以你可以把任何document存入一个collection里。每个collection用一个名字标识,需要注意以下几点:
- 名字不允许是空字符串
""
- 名字不能包含
\0
字符,因为它表示名字的结束 - 不能创建以
system.
开头的
集合操作
创建集合
可以通过 db.createCollection(name,option) 创建集合,参数说明:
- name: 要创建的集合名称
- options: 可选参数, 指定有关内存大小及索引的选项
1 | # 创建或选择tmpdb数据库 |
查看集合
如果要查看已有集合,可以使用 show collections 或 show tables 命令:
1 | show collections; |
删除集合
MongoDB 中使用 drop() 方法来删除集合 db.collection.drop()
如果成功删除选定集合,则 drop() 方法返回 true,否则返回 false。
从结果中可以看出 所有的集合已被删除。
文档
mongoDB的基本单位,相当于关系数据库中的行,它是一组有序的key/value键值对,使用json格式,如:{"foo" : 3, "greeting": "Hello, world!"}
。
key的命令规范
key是个UTF-8字符串,以下几点是需要注意的地方:
- 不能包含
\0
字符(null字符),它用于标识key的结束 .
和$
字符在mangodb中有特殊含义,如$
被用于修饰符($inc
表示更新修饰符),应该考虑保留,以免被驱动解析- 以
_
开始的key也应该保留,比如_id
是mangodb中的关键字
注意事项
- 在mangodb中key是不能重复的
- value 是弱类型,甚至可以嵌入一个document
key/value
键值对在mangodb中是有序的- mangodb是类型和大小写敏感的,如
{"foo" : 3}和{"foo" : "3"}
是两个不同的document,{"foo" : 3}和{"Foo" : 3}
文档基础使用
MongoDB最主要是对文档的操作,下面我们来学习一下文档的操作
插入文档
MongoDB插入数据有多种形式,下面我们来一一学习
insert(不推荐)
插入一条或多条数据需要带有允许插入多条的参数,这个方法目前官方已经不推荐了
注意:若插入的数据主键已经存在,则会抛org.springframework.dao.DuplicateKeyException 异常,提示主键重复,不保存当前数据。
1 | db.blog.insert({ |
如果没有添加_id
参数会自动生成_id
值的,也可以自定义指定_id
1 | db.blog.insert({ |
如果_id
重复会抛出异常
insertOne(推荐)
官方推荐的写法,向文档中写入一个文档
1 | db.blog.insertOne({ |
这样就将数据插入到mongoDB中了
insertMany(推荐)
该语句是进行批量插入的,可以直接进行批量插入
1 | db.blog.insertMany([ |
这样就将多个文档插入到MongoDB中了
查询文档
查询所有文档
find 方法用于查询已存在的文档,MongoDB 查询数据的语法格式如下
1 | db.blog.find(); |
这样就将所有的数据查询出来了
格式化文档
这样看起来不太美观,可以通过pretty进行格式化
1 | db.blog.find().pretty(); |
经过格式化后,这样看起来就好多了
只返回一个文档
find();是返回所有的文档,如果想要只返回第一个文档可以使用findOne()
1 | db.blog.findOne(); |
注意:findOne自动带有格式化效果,不需要在加上pretty方法了
等值查询
我们查询blog表中title=’MySql 教程2’的数据
1 | db.blog.find({ |
这样我们就将数据给查询出来了
投影
projection选择可以控制某一列是否显示,语法格式如下
1 | find({},{"title":1}) |
其中如果title是1则该列显示,否则不显示
1 | #只显示title列的数据 |
注意在一个查询中,投影列的状态必须是一致的,如果不一致将会报错
1 | # 显示 title 不显示description列的数据 |
更新文档
update更新
update() 方法用于更新已存在的文档,更新的时候需要加上关键字 $set
1 | db.blog.update({"_id":"1"},{$set:{"likes":666}}) |
和普通的SQL的对应关系如下
执行完成后查询,我们发现数据已经更新了
save更新
save() 方法通过传入的文档来替换已有文档,_id 主键存在就更新,不存在就插入
1 | db.blog.save({ |
如果_id
不存在则进行插入操作
如果_id
存在则更新数据
1 | db.blog.save({ |
删除文档
条件删除文档
remove() 方法可以删除文档
1 | db.blog.remove({"_id":"1"}) |
这样是删除_id
是1的数据
我们看到数据已经被删除了,我们可以不加条件删除多个文档
1 | db.blog.remove({}); |
这样就删除了所有的文档
只删除第一个文档
remove({})方法可以删除所有文档,如果我们只要删除符合条件的第一个文档
准备数据
我们插入多条数据,这里用到了mongodb shell,后面我们会讲到
1 | for(var i=0;i<10;i++){ |
这样我们就连续插入了10条数据
remove方式删除
可以使用justOne参数,默认是true,只删除符合条件的第一个文档。如果是false 则删除所有符合条件的数据
1 | db.blog.remove({},true); |
这样就把第一个_id
为0的数据给删除了
delete删除文档
官方推荐使用 deleteOne() 和 deleteMany() 方法删除文档
删除单个文档
deleteOne只会删除符合条件的第一个文档,和remove({},true)效果一致
1 | db.blog.deleteOne({}); |
我们看到只删除了一个文档
批量删除文档
deleteMany可以进行批量删除文档,和remove({})效果一致高级查询
1 | db.blog.deleteMany({}); |
这样就把文档全部删除了
准备数据
安装工具包
下载工具包
因为下载的MongoDB是不包含导入导出工具包的,到下载页面选择版本下载即可mongoDB工具包下载地址
选择符合的版本下载即可
1 | wget https://fastdl.mongodb.org/tools/db/mongodb-database-tools-rhel70-x86_64-100.3.1.tgz |
安装工具包
1 | # 解压安装包 |
将MongoDB工具包中的文件复制到 mongodb安装目录的 bin目录
导入测试数据
下载测试数据
这里使用亚马逊官方提供的,下载地址 亚马逊测试数据
使用 wget 命令下载包含示例数据的 JSON 文件
1 | wget http://media.mongodb.org/zips.json |
导入数据
使用 mongoimport 命令将数据导入新数据库 (zips-db)
1 | mongoimport --host 127.0.0.1:27017 --db zips-db --file zips.json |
验证导入文档
导入完成后,使用 mongo 连接到 MongoDB 并验证数据是否已成功加载
1 | mongo --host 127.0.0.1:27017 |
登录后检查发现所有的数据库都是存在的
并检查文档数据
1 | # 切换到zips-db数据库 |
我们发现数据库已经成功导入
测试数据结构
导入的数据是亚马逊官方提供的,没有各个地区的人数统计,数据结构如下
具体对应关系如下图
关系表达式
刚才我们只学习了最基本的查询,下面我们看一下MongoDB的关系表达式
如果你熟悉常规的 SQL 数据,通过下表可以更好的理解 MongoDB 的条件语句查询:
等于
标准写法
等于的操作符是$eq,我们可以查询城市名称是CUSHMAN的数据
1 | db.zips.find({ |
这样我们发现查询出来两条数据
简写方式
查询城市是CUSHMAN的数据,如果一个条件可以直接简写为如下形式
1 | db.zips.find({ |
我们找到两个城市名称是CUSHMAN的数据
小于&小于等于
小于的操作符号是$lt,我们查询城市人数小于 10万的城市有哪些
1 | db.zips.find({ |
我们发现美国有很多州的人口小于十万人
小于等于的操作符号是$lte,我们查询城市没有人的城市,也就是小于等于0的城市
1 | db.zips.find({ |
大于&大于等于
大于的操作符号是$gt,我们查询城市人数大于 十亿的城市有哪些?因为城市的人数单位是万所以是大于十万万人
1 | db.zips.find({ |
第一个符合我们需求的城市就是纽约
大于等于的操作符号是$gte,我们来练习下
1 | db.zips.find({ |
不等于
不等于的操作符号是$ne,我们可以搜索城市人数不是0的城市
1 | db.zips.find({ |
这样我们就找到了城市不是0的数据,是不是很简单呢
包含查询
IN查询的符号是$in,使用方式如下
1 | db.zips.find({ |
我们只查询城市名称缩写是MA以及NY的文档
不包含查询
NIN相当于MySQL的NOT IN查询,操作符号是$nin,我们查询城市名称缩写不是MA以及NY的文档
1 | db.zips.find({ |
这样我们就把数据给查询出来了
判断字段
mongodb是一个文档型数据库,对于表结构没有严格定义,有时候可能缺少字段,如果要查询缺失的字段可以使用$exists判断字段是否存在
1 | db.zips.find({ |
我们发现没有缺失state字段的数据
多条件查询
有时候存在一个字段需要多个条件,比如pop>=10 and pop<50这个如何表示呢
1 | db.zips.find({ |
这样就查询出来了人数在10-50万之间的城市
逻辑表达式
mongodb的逻辑表达式有以下几种
AND 条件
标准写法
AND的标准写法的操作符是$and,下面是查询,我们查询 州缩写是NY并且人数大于一亿的文档
1 | db.zips.find({ |
这样就查询出来了
简写形式
如果只有一个AND操作MongoDB 的 find() 方法可以传入多个键(key),每个键(key)以逗号隔开,即常规 SQL 的 AND 条件。
1 | db.zips.find({ |
OR 条件
MongoDB OR 条件语句使用了关键字 $or,我们查询人数小于0 或者城市缩写是NY的城市。
1 | db.zips.find({ |
这样我们就把所有符合条件的数据筛选出来了
NOT 条件
$not是NOT的操作符,和其他的用法不太一样,使用方法如下
1 | db.zips.find({ |
这样是查询人数 小于十万人的城市
多个条件表达式
我们用一个多条件查询语句,具体语句如下
1 | db.zips.find({ |
这个语句的sql形式如下
1 | select * from zips where (state='NY' and pop>10 and pop <= 50) or (state in('MD','VA') and pop>10 and pop <= 50) |
查询结果如下
排序
在MongoDB中使用sort()方法对数据进行排序,sort()方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而-1是用于降序排列。
语法格式
sort()方法基本语法如下所示
1 | db.COLLECTION_NAME.find().sort({KEY1:1,KEY2:-1,....}) |
升序查询
按照城市人数的升序查询
1 | db.zips.find().sort({ |
我们发现数据是从小到大的升序
降序查询
按照城市人数的降序查询
1 | db.zips.find().sort({ |
我们发现数据是从大到小的降序
组合查询
我们查询人数大于1000万,并且先按照城市缩写升序排,如果城市缩写相同再按照人数降序排
1 | db.zips.find({ |
分页查询
传统关系数据库中都提供了基于row number的分页功能,切换MongoDB后,想要实现分页,则需要修改一下思路。
传统分页思路
1 | #page 1 |
对应的sql是
1 | select * from tables limit(pagesize*(pageIndex-1)+1,pagesize) |
MongoDB的分页
MongoDB提供了skip()和limit()方法。
- skip: 跳过指定数量的数据. 可以用来跳过当前页之前的数据,即跳过pageSize*(n-1)。
- limit: 指定从MongoDB中读取的记录条数,可以当做页面大小pageSize。
前30条的数据是
1 | db.zips.find({},{"_id":1}).limit(30); |
所以可以这样实现分析
1 | # 第一页数据 |
遇到的问题
看起来,分页已经实现了,但是官方文档并不推荐,说会扫描全部文档,然后再返回结果。
1 | The cursor.skip() method requires the server to scan from the beginning of the input results set before beginning to return results. As the offset increases, cursor.skip() will become slower. |
所以,需要一种更快的方式,其实和mysql数量大之后不推荐用limit m,n一样,解决方案是先查出当前页的第一条,然后顺序数pageSize条,MongoDB官方也是这样推荐的。
正确的分页办法
我们假设基于_id的条件进行查询比较,事实上,这个比较的基准字段可以是任何你想要的有序的字段,比如时间戳
实现步骤如下
- 对数据针对于基准字段排序
- 查找第一页的最后一条数据的基准字段的数据
- 查找超过基准字段数据然后向前找pagesize条数据
1 | #第一页数据 |
这样就可以一页一页的向下搜索,但是对于跳页的情况不太友好了。
ObjectId有序性
ObjectId生成规则
1 | 比如"_id" : ObjectId("5b1886f8965c44c78540a4fc") |
取id的前4个字节。由于id是16进制的string,4个字节就是32位,1个字节是两个字符,4个字节对应id前8个字符。即5b1886f8, 转换成10进制为1528334072. 加上1970,就是当前时间。
事实上,更简单的办法是查看org.mongodb:bson:3.4.3里的ObjectId对象。
1 | public ObjectId(Date date) { |
ObjectId存在的问题
MongoDB的ObjectId应该是随着时间而增加的,即后插入的id会比之前的大。但考量id的生成规则,最小时间排序区分是秒,同一秒内的排序无法保证。当然,如果是同一台机器的同一个进程生成的对象,是有序的。
如果是分布式机器,不同机器时钟同步和偏移的问题。所以,如果你有个字段可以保证是有序的,那么用这个字段来排序是最好的。_id则是最后的备选方案,可以考虑增加 雪花算法ID作为排序ID
跳页问题
上面的分页看起来很理想,虽然确实是,但有个刚需不曾指明—我怎么跳页。
我们的分页数据要和排序键关联,所以必须有一个排序基准来截断记录。而跳页,我只知道第几页,条件不足,无法分页了。
现实业务需求确实提出了跳页的需求,虽然几乎不会有人用,人们更关心的是开头和结尾,而结尾可以通过逆排序的方案转成开头。所以,真正分页的需求应当是不存在的。如果你是为了查找某个记录,那么查询条件搜索是最快的方案。如果你不知道查询条件,通过肉眼去一一查看,那么下一页足矣。
在互联网发展的今天,大部分数据的体量都是庞大的,跳页的需求将消耗更多的内存和cpu,对应的就是查询慢,当然,如果数量不大,如果不介意慢一点,那么skip也不是啥问题,关键要看业务场景。
统计查询
MongoDB除了基本的查询功能之外,还提供了强大的聚合功能,这里将介绍一下count, distinct
count
查询记录的总数,下面条件是查询人数 小于十万人的城市的数量
1 | db.zips.find({ |
这样就查询出来符合条件的数据的条数是 118个,还可以写成另外一个形式
1 | db.zips.count({ |
distinct
无条件排重
用来找出给定键的所有不同的值
1 | db.zips.distinct("state"); |
这样就按照state字段进行去重后的数据
有条件排重
对于城市人数是七千万以上的城市的缩写去重
1 | db.zips.distinct("state",{ |
索引
索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。
索引简介
什么是索引
索引最常用的比喻就是书籍的目录,查询索引就像查询一本书的目录。本质上目录是将书中一小部分内容信息(比如题目)和内容的位置信息(页码)共同构成,而由于信息量小(只有题目),所以我们可以很快找到我们想要的信息片段,再根据页码找到相应的内容。同样索引也是只保留某个域的一部分信息(建立了索引的field的信息),以及对应的文档的位置信息。
假设我们有如下文档(每行的数据在MongoDB中是存在于一个Document当中)
索引的作用
假如我们想找id为2的document(即张三的记录),如果没有索引,我们就需要扫描整个数据表,然后找出所有为2的document。当数据表中有大量documents的时候,这个时间就会非常长(从磁盘上查找数据还涉及大量的IO操作)。建立索引后会有什么变化呢?MongoDB会将id数据拿出来建立索引数据,如下
索引的工作原理
这样我们就可以通过扫描这个小表找到document对应的位置。
查找过程示意图如下:
索引为什么这么快
为什么这样速度会快呢?这主要有几方面的因素
- 索引数据通过B树来存储,从而使得搜索的时间复杂度为O(logdN)级别的(d是B树的度,即拥有子结点的个数, 通常d的值比较大,比如大于100),比原先O(N)的复杂度大幅下降。这个差距是惊人的,以一个实际例子来看,假设d=100,N=1亿,那么O(logdN) = 4, 而O(N)是1亿。是的,这就是算法的威力。
- 索引本身是在高速缓存当中,相比磁盘IO操作会有大幅的性能提升。(需要注意的是,有的时候数据量非常大的时候,索引数据也会非常大,当大到超出内存容量的时候,会导致部分索引数据存储
在磁盘上,这会导致磁盘IO的开销大幅增加,从而影响性能,所以务必要保证有足够的内存能容下所有的索引数据)
当然,事物总有其两面性,在提升查询速度的同时,由于要建立索引,所以写入操作时就需要额外的添加索引的操作,这必然会影响写入的性能,所以当有大量写操作而读操作比较少的时候,且对读操作性能不需要考虑的时候,就不适合建立索引。当然,目前大多数互联网应用都是读操作远大于写操作,因此建立索引很多时候是非常划算和必要的操作。
查看索引
索引是提高查询效率最有效的手段。索引是一种特殊的数据结构,索引以易于遍历的形式存储了数据的部分内容(如:一个特定的字段或一组字段值),索引会按一定规则对存储值进行排序,而且索引的存储位置在内存中,所以从索引中检索数据会非常快。如果没有索引,MongoDB必须扫描集合中的每一个文档,这种扫描的效率非常低,尤其是在数据量较大时。
默认主键索引
在创建集合期间,MongoDB 在_id]字段上 创建唯一索引,该索引可防止客户端插入两个具有相同值的文档。
查看索引
查看集合索引
要返回集合中所有索引的列表可以使用db.collection.getIndexes()查看现有索引
1 | db.zips.getIndexes(); |
查看zips集合的所有索引,我们看到有一个默认的_id_索引,并且是一个升序索引
查看数据库
若要列出数据库中所有集合的所有索引,则需在 MongoDB 的 Shell 客户端中进行以下操作:
1 | db.getCollectionNames().forEach(function(collection){ |
这样可以列出本数据库的所有集合的索引
索引常用操作
创建索引
MongoDB使用 createIndex() 方法来创建索引。
注意在 3.0.0 版本前创建索引方法为 db.collection.ensureIndex(),之后的版本使用了 db.collection.createIndex() 方法,ensureIndex() 还能用,但只是 createIndex() 的别名。
语法
createIndex()方法基本语法格式如下所示:
1 | db.collection.createIndex(keys, options) |
语法中 Key 值为你要创建的索引字段,1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可。
1 | db.zips.createIndex({"pop":1}) |
这样就根据pop字段创建了一个升序索引
索引参数
createIndex() 接收可选参数,可选参数列表如下
示例
创建一个名称是pop_union_index的索引,按照pop字段降序,并且在5秒后删除
1 | doc1 = {"date": new Date(),"id":1} |
zips中
1 | db.zips.createIndex( |
这样我们就创建了一个索引
删除索引
MongoDB 提供的两种从集合中删除索引的方法如下:
根据name删除
可以根据索引的名字进行索引删除
1 | db.zips.dropIndex("date_expire") |
这样我们就把一个索引删除了
根据字段删除
还可以根据字段进行删除
1 | db.zips.dropIndex ({ "pop" : 1 }) |
删除集合中pop字段升序的索引,这样就把这个索引删除了
删除所有索引
db.collection.dropIndexes()可以把集合所有索引删除
1 | db.zips.dropIndexes() |
这样就把非默认的主键索引以外的索引删除了
MongoDB索引类型
单键索引
MongoDB为文档集合中任何字段上的索引提供了完整的支持 。默认情况下,所有集合在_id
字段上都有一个索引,应用程序和用户可以添加其他索引来支持重要的查询和操作。
这个是最简单最常用的索引类型,比如我们上边的例子,为id建立一个单独的索引就是此种类型。
创建索引
我们创建一个pop人数升序的索引
1 | db.zips.createIndex({ |
其中{‘pop’: 1}中的1表示升序,如果想设置倒序索引的话使用 {‘pop’: -1}即可
查看执行计划
可以在查询中使用执行计划查看索引是否生效
1 | db.zips.find({ |
我们发现索引已经生效了
复合索引
复合索引(Compound Indexes)指一个索引包含多个字段,用法和单键索引基本一致。使用复合索引时要注意字段的顺序,如下添加一个userid和score的复合索引,userid正序,score倒序,document首先按照userid正序排序,然后userid相同的document按score进行倒序排序。
mongoDB中一个复合索引最多可以包含32个字段。符合索引的原理如下图所示:
上图查询索引的时候会先查询userid,再查询score,然后就可以找到对应的文档。
创建索引
我们创建一个以city升序,state降序的复合索引
1 | db.zips.createIndex({ |
这样我们就把索引创建了
查看执行计划
1 | db.zips.find({ |
我们看到我们的查询走了索引
对于复合索引需要注意以下几点:
最佳左前缀法则
在MySQL中走最佳左前缀法则生效,在mongodb中查询同样生效
1 | db.zips.find({ |
我们只查询最左侧索引列的时候,索引是生效的
但是如果我们查询不加入最左侧索引列
1 | db.zips.find({ |
我们发现索引未生效,走了全表扫描
地理索引
地理索引包含两种地理类型,如果需要计算的地理数据表示为类似于地球的球形表面上的坐标,则可以使用 2dsphere 索引。
通常可以按照坐标轴、经度、纬度的方式把位置数据存储为 GeoJSON 对象。GeoJSON 的坐标参考系使用的是 wgs84(World Geodetic System 1984是为GPS全球定位系统使用而建立的坐标系统) 数据。如果需要计算距离(在一个欧几里得平面上),通常可以按照正常坐标对的形式存储位置数据,可使用 2d 索引。
创建平面地理索引
如果查找的地方是小范围的可以使用平面索引
1 | db.zips.createIndex({ |
创建球面地理索引
如果是大范围的,需要考虑地球弧度的情况下如果使用平面坐标可能不准确,就需要使用球面索引
1 | db.zips.createIndex({ |
常用索引属性
唯一索引
唯一索引(unique indexes)用于为collection添加唯一约束,即强制要求collection中的索引字段没有重复值。添加唯一索引的语法:
1 | db.zips.createIndex({"_id":1,"city":1},{unique:true,name:"id_union_index"}) |
这样我们就创建了一个根据ID以及city的唯一索引
局部索引
局部索引(Partial Indexes)顾名思义,只对collection的一部分添加索引。创建索引的时候,根据过滤条件判断是否对document添加索引,对于没有添加索引的文档查找时采用的全表扫描,对添加了索引的文档查找时使用索引。
创建索引
这样就创建了局部索引
1 | db.zips.createIndex( |
查看执行计划
根据索引特性 ,我们知道,只有查找的人数大于10000,才会走索引
1 | db.zips.find({ |
我们看到,查询10000以内的数据不走索引
如果查找的条件大于10000就会走索引
1 | db.zips.find({ |
执行计划
MongoDB中的explain()函数可以帮助我们查看查询相关的信息,这有助于我们快速查找到搜索瓶颈进而解决它,本文我们就来看看explain()的一些用法及其查询结果的含义。整体来说,explain()的用法和sort()、limit()用法差不多,不同的是explain()必须放在最后面。
基本用法
先来看一个基本用法:
1 | db.zips.find({ |
直接跟在find()函数后面,表示查看find()函数的执行计划,结果如下:
1 | { |
返回结果包含两大块信息,一个是 queryPlanner,即查询计划,还有一个是 serverInfo,即MongoDB服务的一些信息。
参数解释
那么这里涉及到的参数比较多,我们来一一看一下:
添加不同参数
explain() 也接收不同的参数,通过设置不同参数我们可以查看更详细的查询计划。
queryPlanner
是默认参数,添加queryPlanner参数的查询结果就是我们上文看到的查询结果,so,这里不再赘述。
executionStats
会返回最佳执行计划的一些统计信息,如下:
1 | db.zips.find({ |
我们发现增加了一个executionStats的字段列的信息
1 | { |
这里除了我们上文介绍到的一些参数之外,还多了executionStats参数,含义如下
allPlansExecution:用来获取所有执行计划,结果参数基本与上文相同,这里就不再细说了。
慢查询
在MySQL中,慢查询日志是经常作为我们优化查询的依据,那在MongoDB中是否有类似的功能呢?答案是肯定的,那就是开启Profiling功能。该工具在运行的实例上收集有关MongoDB的写操作,游标,数据库命令等,可以在数据库级别开启该工具,也可以在实例级别开启。该工具会把收集到的所有都写入到system.profile集合中,该集合是一个capped collection。(一旦集合填满其分配的空间,它就会覆盖集合中最旧的文档,从而为新文档腾出空间)
慢查询分析流程
慢查询日志一般作为优化步骤里的第一步。通过慢查询日志,定位每一条语句的查询时间。比如超过了200ms,那么查询超过200ms的语句需要优化。然后它通过 .explain() 解析影响行数是不是过大,所以导致查询语句超过200ms。
所以优化步骤一般就是:
- 用慢查询日志(system.profile)找到超过200ms的语句
- 然后再通过.explain()解析影响行数,分析为什么超过200ms
- 决定是不是需要添加索引
开启慢查询
Profiling级别说明
1 | 0:关闭,不收集任何数据。 |
针对数据库设置
登录需要开启慢查询的数据库
1 | use zips-db |
查看慢查询状态
1 | db.getProfilingStatus() |
设置慢查询级别
1 | db.setProfilingLevel(2) |
如果不需要收集所有慢日志,只需要收集小于100ms的慢日志可以使用如下命令
1 | db.setProfilingLevel(1,100) |
注意:
- 以上操作要是在test集合下面的话,只对该集合里的操作有效,要是需要对整个实例有效,则需要在所有的集合下设置或在开启的时候开启参数
- 每次设置之后返回给你的结果是修改之前的状态(包括级别、时间参数)。
全局设置
在mongoDB启动的时候加入如下参数
1 | mongod --profile=1 --slowms=200 |
或在配置文件里添加2行:
1 | profile = 1 |
这样就可以针对所有数据库进行监控慢日志了
关闭Profiling
使用如下命令可以关闭慢日志
1 | db.setProfilingLevel(0) |
Profile 效率
Profiling功能肯定是会影响效率的,但是不太严重,原因是他使用的是system.profile 来记录,而system.profile 是一个capped collection, 这种collection 在操作上有一些限制和特点,但是效率更高。
慢查询分析
通过 db.system.profile.find() 查看当前所有的慢查询日志
1 | db.system.profile.find() |
参数含义
1 | { |
分析
如果发现 millis 值比较大,那么就需要做优化。
- 如果nscanned数很大,或者接近记录总数(文档数),那么可能没有用到索引查询,而是全表扫描。
- 如果 nscanned 值高于 nreturned 的值,说明数据库为了找到目标文档扫描了很多文档。这时可以考虑创建索引来提高效率。
system.profile补充
‘type’的返回参数说明
1 | COLLSCAN #全表扫描 |
对于普通查询,我们最希望看到的组合有这些
1 | Fetch+IDHACK |
不希望看到包含如下的type
1 | COLLSCAN(全表扫),SORT(使用sort但是无index),不合理的SKIP,SUBPLA(未用到index的$or) |
SpringBoot整合MongDB
引入pom坐标
1 | <dependency> |
编写配置文件
1 | server: |
定义实体类
Blog类
1 | @Document("blog") |
DAO
1 | @Component |
Controller
1 | @RestController |
启动测试
启动Controller 插入数据
到数据库查看结果
MongoDB聚合查询
什么是聚合查询
聚合操作主要用于处理数据并返回计算结果。聚合操作将来自多个文档的值组合在一起,按条件分组后,再进行一系列操作(如求和、平均值、最大值、最小值)以返回单个结果。
MongoDB的聚合查询
聚合是MongoDB的高级查询语言,它允许我们通过转化合并由多个文档的数据来生成新的在单个文档里不存在的文档信息。MongoDB中聚合(aggregate)主要用于处理数据(例如分组统计平均值、求和、最大值等),并返回计算后的数据结果,有点类似sql语句中的 count(*)、group by。
在MongoDB中,有两种方式计算聚合:Pipeline 和 MapReduce。Pipeline查询速度快于MapReduce,但是MapReduce的强大之处在于能够在多台Server上并行执行复杂的聚合逻辑。
MongoDB不允许Pipeline的单个聚合操作占用过多的系统内存。
聚合管道方法
MongoDB 的聚合框架就是将文档输入处理管道,在管道内完成对文档的操作,最终将文档转换为聚合结果,MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理,管道操作是可以重复的。
最基本的管道阶段提供过滤器,其操作类似查询和文档转换,可以修改输出文档的形式。其他管道操作提供了按特定字段对文档进行分组和排序的工具,以及用于聚合数组内容(包括文档数组)的工具
此外,在管道阶段还可以使用运算符来执行诸如计算平均值或连接字符串之类的任务。聚合管道可以在分片集合上运行。
聚合流程
db.collection.aggregate()是基于数据处理的聚合管道,每个文档通过一个由多个阶段(stage)组成的管道,可以对每个阶段的管道进行分组、过滤等功能,然后经过一系列的处理,输出相应的结果。
聚合管道方法的流程参见下图
上图的聚合操作相当于 MySQL 中的以下语句:
1 | select cust_id as _id, sum(amount) as total from orders where status like "%A%" group by cust_id; |
详细流程
db.collection.aggregate() 可以用多个构件创建一个管道,对于一连串的文档进行处理。这些构件包括:筛选操作的 match 、映射操作的 project 、分组操作的 group 、排序操作的 sort 、限制操作的 limit 、和跳过操作的 skip 。
db.collection.aggregate() 使用了MongoDB内置的原生操作,聚合效率非常高,支持类似于SQL Group By操作的功能,而不再需要用户编写自定义的JavaScript例程。
每个阶段管道限制为100MB的内存。如果一个节点管道超过这个极限,MongoDB将产生一个错误。为了能够在处理大型数据集,可以设置 allowDiskUse 为 true 来在聚合管道节点把数据写入临时文件。这样就可以解决100MB的内存的限制。
db.collection.aggregate() 可以作用在分片集合,但结果不能输在分片集合, MapReduce 可 以 作用在分片集合,结果也可以输在分片集合。
db.collection.aggregate() 方法可以返回一个指针( cursor ),数据放在内存中,直接操作。跟Mongo shell 一样指针操作。
db.collection.aggregate() 输出的结果只能保存在一个文档中, BSON Document 大小限制为16M。可以通过返回指针解决,版本2.6中: DB.collect.aggregate() 方法返回一个指针,可以返回任何结果集的大小。
聚合语法
1 | db.collection.aggregate(pipeline, options) |
参数说明
注意事项
使用db.collection.aggregate()直接查询会提示错误,但是传一个空数组如db.collection.aggregate([])则不会报错,且会和find一样返回所有文档。
常用聚合管道
与mysql聚合类比
为了便于理解,先将常见的mongo的聚合操作和mysql的查询做下类比
$count
返回包含输入到stage的文档的计数,理解为返回与表或视图的find()查询匹配的文档的计数。
db.collection.count()方法不执行find()操作,而是计数并返回与查询匹配的结果数。
语法
1 | { $count: <string> } |
$count
阶段相当于下面$group+$project
的序列:
1 | db.zips.aggregate([ |
示例
查询人数是100000以上的城市的数量$match
:阶段排除pop小于等于100000的文档,将大于100000的文档传到下个阶段$count
:阶段返回聚合管道中剩余文档的计数,并将该值分配给名为count的字段。
1 | db.zips.aggregate([ |
$group
按指定的表达式对文档进行分组,并将每个不同分组的文档输出到下一个阶段。输出文档包含一个_id
字段,该字段按键包含不同的组。
输出文档还可以包含计算字段,该字段保存由$group
的_id
字段分组的一些accumulator表达式的值。 $group
不会输出具体的文档而只是统计信息。
语法
1 | { $group: { _id: <expression>, <field1>: { <accumulator1> : <expression1> }, ... |
_id
字段是必填的;但是,可以指定_id
值为null来为整个输入文档计算累计值。- 剩余的计算字段是可选的,并使用
<accumulator>
运算符进行计算。 _id
和<accumulator>
表达式可以接受任何有效的表达式。
accumulator操作符
$group
阶段的内存限制为100M,默认情况下,如果stage超过此限制,$group
将产生错误,但是,要允许处理大型数据集,请将allowDiskUse选项设置为true以启用$group
操作以写入临时文件。
注意:
- “
$addToSet
“:expr,如果当前数组中不包含expr,那就将它添加到数组中。 - “
$push
“:expr,不管expr是什么值,都将它添加到数组中,返回包含所有值的数组。
示例
按照state分组,并计算每一个state分组的总人数,平均人数以及每个分组的数量
1 | db.zips.aggregate([ |
查找不重复的所有的state的值
1 | db.zips.aggregate([ |
按照city分组,并且分组内的state字段列表加入到stateItem并显示
1 | db.zips.aggregate([ |
下面聚合操作使用系统变量$$ROOT按item对文档进行分组,生成的文档不得超过BSON文档大小限制
1 | db.zips.aggregate([ |
$match
过滤文档,仅将符合指定条件的文档传递到下一个管道阶段。
$match
接受一个指定查询条件的文档,查询语法与读操作查询语法相同。
语法
1 | { $match: { <query> } } |
管道优化
$match
用于对文档进行筛选,之后可以在得到的文档子集上做聚合,$match
可以使用除了地理空间之外的所有常规查询操作符,在实际应用中尽可能将$match
放在管道的前面位置。这样有两个好处:
- 一是可以快速将不需要的文档过滤掉,以减少管道的工作量;
- 二是如果再投射和分组之前执行
$match
,查询可以使用索引。
使用限制
- 不能在
$match
查询中使用$
作为聚合管道的一部分。 - 要在
$match
阶段使用$text
,$match
阶段必须是管道的第一阶段。 - 视图不支持文本搜索。
示例
使用 $match做简单的匹配查询,查询缩写是NY的城市数据
1 | db.zips.aggregate([ |
使用$match
管道选择要处理的文档,然后将结果输出到$group
管道以计算文档的计数
1 | db.zips.aggregate([ |
$unwind
从输入文档解构数组字段以输出每个元素的文档,简单说就是 可以将数组拆分为单独的文档。
语法
要指定字段路径,在字段名称前加上$符并用引号括起来。
1 | { $unwind: <field path> } |
v3.2+支持如下语法
1 | { |
如果为输入文档中不存在的字段指定路径,或者该字段为空数组,则$unwind
默认会忽略输入文档,并且不会输出该输入文档的文档。
版本3.2中的新功能:要输出数组字段丢失的文档,null或空数组,请使用选项preserveNullAndEmptyArrays。
示例
以下聚合使用$unwind为loc数组中的每个元素输出一个文档:
1 | db.zips.aggregate([ |
$project
$project
可以从文档中选择想要的字段,和不想要的字段(指定的字段可以是来自输入文档或新计算字段的现有字段),也可以通过管道表达式进行一些复杂的操作,例如数学操作,日期操作,字符串操作,逻辑操作。
语法
$project
管道符的作用是选择字段(指定字段,添加字段,不显示字段,_id:0,排除字段等),重命名字段,派生字段。
1 | { $project: { <specification(s)> } } |
specifications有以下形式:
1 | <field>: <1 or true> 是否包含该字段,field:1/0,表示选择/不选择 field |
- 默认情况下,
_id
字段包含在输出文档中。要在输出文档中包含输入文档中的任何其他字段,必须明确指定$project
中的包含。 如果指定包含文档中不存在的字段,$project
将忽略该字段包含,并且不会将该字段添加到文档中。 - 默认情况下,
id
字段包含在输出文档中。要从输出文档中排除id字段,必须明确指定$project
中的_id
字段为0。 - v3.4版新增功能-如果指定排除一个或多个字段,则所有其他字段将在输出文档中返回。 如果指定排除
_id
以外的字段,则不能使用任何其他$project
规范表单:即,如果排除字段,则不能指定包含字段,重置现有字段的值或添加新字段。此限制不适用于使用REMOVE变量条件排除字段。 - v3.6版本中的新功能- 从MongoDB 3.6开始,可以在聚合表达式中使用变量REMOVE来有条件地禁止一个字段。
- 要添加新字段或重置现有字段的值,请指定字段名称并将其值设置为某个表达式。
- 要将字段值直接设置为数字或布尔文本,而不是将字段设置为解析为文字的表达式,请使用
$literal
操作符。否则,$project
会将数字或布尔文字视为包含或排除该字段的标志。 - 通过指定新字段并将其值设置为现有字段的字段路径,可以有效地重命名字段。
- 从MongoDB 3.2开始,
$project
阶段支持使用方括号[]直接创建新的数组字段。如果数组规范包含文档中不存在的字段,则该操作会将空值替换为该字段的值。 - 在版本3.4中更改-如果
$project
是一个空文档,MongoDB 3.4和更高版本会产生一个错误。 - 投影或添加/重置嵌入文档中的字段时,可以使用点符号
示例
以下$project
阶段的输出文档中只包含_id
,city和state字段
1 | db.zips.aggregate([ |
_id
字段默认包含在内。要从$project
阶段的输出文档中排除_id
字段,请在project
文档中将_id
字段设置为0来指定排除_id
字段。
1 | db.zips.aggregate([ |
以下$project
阶段从输出中排除loc字段
1 | db.zips.aggregate([ |
可以在聚合表达式中使用变量REMOVE来有条件地禁止一个字段,
1 | db.zips.aggregate([ |
我们还可以改变数据,将人数大于1000的城市坐标重置为0
1 | db.zips.aggregate([ |
新增字段列
1 | db.zips.aggregate([ |
$limit
限制传递到管道中下一阶段的文档数
语法
1 | { $limit: <positive integer> } |
示例,此操作仅返回管道传递给它的前5个文档。 $limit
对其传递的文档内容没有影响。
1 | db.zips.aggregate({ |
注意
当$sort
在管道中的$limit
之前立即出现时,$sort
操作只会在过程中维持前n个结果,其中n是指定
的限制,而MongoDB只需要将n个项存储在内存中。当allowDiskUse为true并且n个项目超过聚合内存
限制时,此优化仍然适用。
$skip
跳过进入stage的指定数量的文档,并将其余文档传递到管道中的下一个阶段
1 | { $skip: <positive integer> } |
语法
示例,此操作将跳过管道传递给它的前5个文档, $skip对沿着管道传递的文档的内容没有影响。
1 | db.zips.aggregate({ |
$sort
对所有输入文档进行排序,并按排序顺序将它们返回到管道。
语法
1 | { $sort: { <field1>: <sort order>, <field2>: <sort order> ... } } |
$sort
指定要排序的字段和相应的排序顺序的文档。 <sort order>
可以具有以下值之一:
- 1指定升序。
- -1指定降序。
{$meta:“textScore”}
按照降序排列计算出的textScore元数据。
示例
要对字段进行排序,请将排序顺序设置为1或-1,以分别指定升序或降序排序,如下例所示:
1 | db.zips.aggregate([ |
$sortByCount
根据指定表达式的值对传入文档分组,然后计算每个不同组中文档的数量。每个输出文档都包含两个字段:包含不同分组值的_id字段和包含属于该分组或类别的文档数的计数字段,文件按降序排列。
语法
1 | { $sortByCount: <expression> } |
使用示例
下面举了一些常用的mongo聚合例子和mysql对比,假设有一条如下的数据库记录(表名:zips)作为例子:
统计所有数据
SQL的语法格式如下
1 | select count(1) from zips; |
mongoDB的语法格式
1 | db.zips.aggregate([ |
对所有城市人数求合
SQL的语法格式如下
1 | select sum(pop) AS tota from zips; |
mongoDB的语法格式
1 | db.zips.aggregate([ |
对城市缩写相同的城市人数求合
SQL的语法格式如下
1 | select state,sum(pop) AS tota from zips group by state; |
mongoDB的语法格式
1 | db.zips.aggregate([ |
state重复的城市个数
SQL的语法格式如下
1 | select state,count(1) AS total from zips group by state; |
mongoDB的语法格式
1 | db.zips.aggregate([ |
state重复个数大于100的城市
SQL的语法格式如下
1 | select state,count(1) AS total from zips group by state having count(1)>100; |
mongoDB的语法格式
1 | db.zips.aggregate([ |
MapReduce
MongoDB的聚合操作主要是对数据的批量处理,一般都是将记录按条件分组之后进行一系列求最大值,最小值,平均值的简单操作,也可以对记录进行数据统计,数据挖掘的复杂操作,聚合操作的输入是集中的文档,输出可以是一个文档也可以是多个文档。
Pipeline查询速度快于MapReduce,但是MapReduce的强大之处在于能够在多台Server上并行执行复杂的聚合逻辑,MongoDB不允许Pipeline的单个聚合操作占用过多的系统内存,如果一个聚合操作消耗20%以上的内存,那么MongoDB直接停止操作,并向客户端输出错误消息。
什么是MapReduce
MapReduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)
mapreduce使用javascript语法编写,其内部也是基于javascript V8引擎解析并执行,javascript语言的灵活性也让mapreduce可以处理更加复杂的业务场景;当然这相对于aggreation pipleine而言,意味着需要书写大量的脚本,而且调试也将更加困难。(调试可以基于javascript调试,成功后再嵌入到mongodb中)
执行阶段
mapreduce有2个阶段:map和reduce;
- mapper处理每个document,然后emits一个或者多个objects,object为key-value对;
- reducer将map操作的结果进行联合操作(combine)。此外mapreduce还可以有一个finalize阶段,这是可选的,它可以调整reducer计算的结果。在进行mapreduce之前,mongodb支持使用query来筛选文档,也支持sort排序和limit。
语法
MapReduce 的基本语法如下:
1 | db.collection.mapReduce( |
使用 MapReduce 要实现两个函数 Map 函数和 Reduce 函数,Map 函数调用 emit(key, value), 遍历 collection 中所有的记录, 将 key 与 value 传递给 Reduce 函数进行处理。
参数说明
- map:是JavaScript 函数,负责将每一个输入文档转换为零或多个文档,通过key进行分组,生成键值对序列,作为 reduce 函数参数
- reduce:是JavaScript 函数,对map操作的输出做合并的化简的操作(将key-values变成key-
- value,也就是把values数组变成一个单一的值value)
- out:统计结果存放集合 (不指定则使用临时集合,在客户端断开后自动删除)。
- query: 一个筛选条件,只有满足条件的文档才会调用map函数。(query。limit,sort可以随意组合)
- sort: 和limit结合的sort排序参数(也是在发往map函数前给文档排序),可以优化分组机制
- limit: 发往map函数的文档数量的上限(要是没有limit,单独使用sort的用处不大)
- finalize:可以对reduce输出结果再一次修改,跟group的finalize一样,不过MapReduce没有
- group的4MB文档的输出限制
- scope:向map、reduce、finalize导入外部变量
- verbose:是否包括结果信息中的时间信息,默认为fasle
使用示例
按照state分组统计
样例SQL
1 | select by,count(1) from blog group by by having likes>100 |
mapReduce写法
这是统计每一个作者的博客分数是100以上的文章数
1 | db.zips.mapReduce( |
输出结果
将结果输出
1 | # 显示集合 |
编程语法
在mongodb中,mapreduce除了包含mapper和reducer之外,还包含其他的一些选项,不过整体遵循mapreduce的规则:
map
javascript方法,此方法中可以使用emit(key,value),一次map调用中允许返回调用多次emit(也可以不调用),它不需要返回值;其中key用来分组,value将来会被传递给reducer用于“聚合计算”。每条document都会调用一次map方法。
mapper中输入的是当前document,可以通过this.<filedName>
来获取字段的值。mapper应该是封闭的,它不能访问外部资源,比如collection、database,不能修改外部的值,但允许访问“scope”中的变量。emit的值不能大于16M,即document最大的尺寸,否则mongodb将会抛出错误。
1 | function() { |
reduce
javascript方法,此方法接收key和values两个参数,经过mapper处理和“归并之后”,一个key将会对应一组values(分组,key:values),此values将会在reduce中进行“聚合计算”,比如:sum、平均数、数据分拣等等。
reducer和mapper一样是封闭的,它内部不允许访问database、collection等外部资源,不能修改外部值,但可以访问“scope”中的变量;如果一个key只有一个value,那么mongodb就不会调用reduce方法。可能一个key对应的values条数很多,将会调用多次reduce,即前一次reduce的结果可能被包含在values中再次传递给reduce方法,这也要求,reduce返回的结果需要和value的结构保持一致。同样,reduce返回的数据尺寸不能大于8M(document最大尺寸的一半,因为reduce的结果可能会作为input再次reduce)。
1 | //mapper |
此外reduce内的算法需要是幂等的,且与输入values的顺序无关的,因为即使相同的input文档,也无法保证map-reduce的每个过程都是逐字节相同的,但应该确保计算的结果是一致的
out
document结构,包含一些配置选项;用于指定reduce的结果最终如何保存。可以将结果以inline
的方式直接输出(cursor),或者写入一个collection中。
1 | out : { |
out方式默认为inline,即不保存数据,而是返回一个cursor,客户端直接读取数据即可。
action
<action>
表示如果保存结果的<collection>
已经存在时,将如何处理:
replace:替换,替换原collection中的内容;先将数据保存在临时collection,此后rename,再将旧collection删除
- merge:将结果与原有内容合并,如果原有文档中持有相同的key(即_id字段),则直接覆盖原值
- reduce:将结果与原有内容合并,如果原有文档中有相同的key,则将新值、旧值合并后再次应用
- reduce方法,并将得到的值覆盖原值(对于“用户留存”、“数据增量统计”非常有用)。
db
结果数据保存在哪个database中,默认为当前db;开发者可能为了进一步使用数据,将统计结果统一放在单独的database中
sharded
输出结果的collection将使用sharding模式,使用_id
作为shard key;不过首先需要开发者对<collection>
所在的database开启sharding,否则将无法执行。
nonAtomic
“非原子性”,仅对“merge”和“replace”有效,控制output collection,默认为false,即“原子性”;
即mapreduce在输出阶段将会对output collection所在的数据库加锁,直到输出结束,可能性能会有影响;
如果为true,则不会对db加锁,其他客户端可以读取到output collection的中间状态数据。我们通常将ouput collection单独放在一个db中,和application数据分离开,而且nonAtomic为false,我们也不希望用户读到“中间状态数据”。
可以通过指定“out:{inline : 1}”将输出结果保存在内存中,并返回一个cursor,客户端可以直接读取
即可。
query
筛选文档,只需要将符合条件的documents传递给mapper
sort
对筛选之后的文档排序,然后才传递给mapper。如果根据map的key进行排序,则可以减少reduce的操作次数。排序必须能够使用index。
limit
限定输入到map的文档条数
finalize
终结操作,在输出之前调整reduce的结果。它和map、reduce一样,也是一个javascript方法,接收key和value,其中value为reduce输出结果,finalize方法中可以修改value的值作为最终的输出结果:
1 | function(key,value) { |
scope
document结构,保存一些global级别的变量值,它们可以在map、reduce、finalize中被访问。
jsMode
可选值为true或者false;表示是否将map执行的中间结果数据由javascript对象转换成BSON对象,默认为false。
- false表示,在mapper中emit最终输出的是javascript对象,因为是javascript引擎处理的,不过mapper 可能产生大量的数据,这些数据将会被保存在临时的存储中(collection),所以需要将javascript对象转换成BSON;在reduce阶段,这些BSON结果再被转换成javascript对象,传递给reduce方法,转换意味着性能消耗和慢速,它解决的问题就是“临时存储”以适应较大数据集的数据分析。
- 如果为true,将不会进行类型转换,数据被暂存在内存中,reduce阶段直接使用mapper的结果即可,但是key的个数不能超过50W个。在production环境中,此值建议为false。
mongo特性、 搭建 、 springboot 、 索引调优 、 explain分析工具、索引设计,高级特性: geo 、 聚合查询 、 集群,mongodbshell
MongoDB集群管理
集群介绍
为什么使用集群
随着业务数据和并发量的增加,若只使用一台MongoDB服务器,存在着断电和数据风险的问题,故采用Mongodb复制集的方式,来提高项目的高可用、安全性等性能。
MongoDB复制是将数据同步到多个服务器的过程。复制提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性, 并可以保证数据的安全性。复制还允许从硬件故障和服务中断中恢复数据。
使用集群的目的就是提高可用性。高可用性H.A.(High Availability)指的是通过尽量缩短因日常维护操作(计划)和突发的系统崩溃(非计划)所导致的停机时间,以提高系统和应用的可用性。它与被认为是不间断操作的容错技术有所不同。HA系统是目前企业防止核心计算机系统因故障停机的最有效手段。
相关概念
在搭建集群之前,需要首先了解几个概念:路由,分片、副本集、配置服务器等。
mongos
数据库集群请求的入口,所有的请求都通过mongos进行协调,不需要在应用程序添加一个路由选择器,mongos自己就是一个请求分发中心,它负责把对应的数据请求转发到对应的shard服务器上。在生产环境通常有多mongos作为请求的入口,防止其中一个挂掉所有的mongodb请求都没有办法操作。
config server
顾名思义为配置服务器,存储所有数据库元信息(路由、分片)的配置。mongos本身没有物理存储分片服务器和数据路由信息,只是缓存在内存里,配置服务器则实际存储这些数据。mongos第一次启动或者关掉重启就会从 config server 加载配置信息,以后如果配置服务器信息变化会通知到所有的 mongos 更新自己的状态,这样 mongos 就能继续准确路由。在生产环境通常有多个 config server 配置服务器,因为它存储了分片路由的元数据,防止数据丢失!
shard
分片(sharding)是指将数据库拆分,将其分散在不同的机器上的过程。将数据分散到不同的机器上,不需要功能强大的服务器就可以存储更多的数据和处理更大的负载。基本思想就是将集合切成小块,这些块分散到若干片里,每个片只负责总数据的一部分,最后通过一个均衡器来对各个分片进行均衡(数据迁移)。
replica set
中文翻译副本集,其实就是shard的备份,防止shard挂掉之后数据丢失。复制提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性, 并可以保证数据的安全性。
仲裁者
仲裁者(Arbiter),是复制集中的一个MongoDB实例,它并不保存数据。仲裁节点使用最小的资源并且不要求硬件设备,不能将Arbiter部署在同一个数据集节点中,可以部署在其他应用服务器或者监视服务器中,也可部署在单独的虚拟机中。为了确保复制集中有奇数的投票成员(包括primary),需要添加仲裁节点做为投票,否则primary不能运行时不会自动切换primary。
仲裁节点是一种特殊的节点,它本身并不存储数据,主要的作用是决定哪一个备节点在主节点挂掉之后提升为主节点,所以客户端不需要连接此节点。这里虽然只有一个备节点,但是仍然需要一个仲裁节点来提升备节点级别。如果没有仲裁节点的话,主节点挂了备节点还是备节点,所以咱们还是需要它的
集群方案
主从(Master-Slaver)
因为官方已经不推荐使用,并且已经在MongoDB3.6以后慢慢废弃了,这里不过多的介绍了
工作原理:主机工作,备机处于监控准备状况;当主机宕机时,备机接管主机的一切工作,待主机恢复正常后,按使用者的设定以自动或手动方式将服务切换到主机上运行,数据的一致性通过共享存储系统解决。
副本集(Replica Set)
简单来说就是集群当中包含了多份数据,保证主节点挂掉了,备节点能继续提供数据服务,提供的前提就是数据需要和主节点一致。
默认设置下,主节点提供所有增删查改服务,备节点不提供任何服务。但是可以通过设置使备节点提供查询服务,这样就可以减少主节点的压力,当客户端进行数据查询时,请求自动转到备节点上。这个设置叫做Read Preference Modes。
副本集特点
- N个节点的集群
- 任何节点可作为主节点(除了仲裁节点)
- 所有写操作都在主节点上
- 自动故障迁移
- 自动恢复
分片(Sharding)
Sharding和Replica Set类似,都需要一个仲裁节点,但是Sharding还需要配置节点和路由节点。就三种集群搭建方式来说,这种是最复杂的。
为什么Sharding会需要配置Replica Set。其实想想也能明白,多个节点的数据肯定是相关联的,如果不配一个Replica Set,怎么标识是同一个集群的呢。配置方式和之前所说的一样,定一个cfg,然后初始化配置。
副本集说明
什么是副本集
一组Mongodb复制集,就是一组mongod进程,这些进程维护同一个数据集合。复制集提供了数据冗余和高等级的可靠性,这是生产部署的基础。
MongoDB 副本集是将数据同步在多个服务器的过程,复制提供了数据的冗余备份,并在多个服务器上存储数据副本,提高了数据的可用性, 并可以保证数据的安全性,同时还允许从硬件故障和服务中断中恢复数据
副本集的目的
保证数据在生产部署时的冗余和可靠性,通过在不同的机器上保存副本来保证数据不会因为单点损坏而丢失。能够随时应对数据丢失、机器损坏带来的风险。
换一句话来说,还能提高读取能力,用户的读取服务器和写入服务器在不同的地方,而且,由不同的服务器为不同的用户提供服务,提高整个系统的负载。副本集工作原理
一组复制集就是一组mongod实例掌管同一个数据集,实例可以在不同的机器上面。实例中包含一个主导,接受客户端所有的写入操作,其他都是副本实例,从主服务器上获得数据并保持同步。
主服务器很重要,包含了所有的改变操作(写)的日志。但是副本服务器集群包含所有的主服务器数据,因此当主服务器挂掉了,就会在副本服务器上重新选取一个成为主服务器。
每个复制集还有一个仲裁者,仲裁者不存储数据,只是负责通过心跳包来确认集群中集合的数量,并在主服务器选举的时候作为仲裁决定结果。
副本集架构
基本的架构由3台服务器组成,一个三成员的复制集,由三个有数据,或者两个有数据,一个作为
仲裁者。
没有仲裁节点
具有三个存储数据的成员的复制集有:
- 一个主库;
- 两个从库
主库宕机时,这两个从库都可以被选为主库。
当主库宕机后,两个从库都会进行竞选,其中一个变为主库,当原主库恢复后,作为从库加入当前的复制集群即可。
当存在仲裁节点
在三个成员的复制集中,有两个正常的主从,及一台arbiter节点:
- 一个主库
- 一个从库,可以在选举中成为主库
- 一个aribiter节点,在选举中,只进行投票,不能成为主库
说明:
由于arbiter节点没有复制数据,因此这个架构中仅提供一个完整的数据副本。arbiter节点只需要更少的资源,代价是更有限的冗余和容错。
当主库宕机时,将会选择从库成为主,主库修复后,将其加入到现有的复制集群中即可。
Primary选举
复制集通过replSetInitiate命令(或mongo shell的rs.initiate())进行初始化,初始化后各个成员间开始发送心跳消息,并发起Priamry选举操作,获得『大多数』成员投票支持的节点,会成为Primary,其余节点成为Secondary。
『大多数』的定义
假设复制集内投票成员(后续介绍)数量为N,则大多数为 N/2 + 1,当复制集内存活成员数量不足大多数时,整个复制集将无法选举出Primary,复制集将无法提供写服务,处于只读状态。
通常建议将复制集成员数量设置为奇数,从上表可以看出3个节点和4个节点的复制集都只能容忍1个节点失效,从『服务可用性』的角度看,其效果是一样的。(但无疑4个节点能提供更可靠的数据存储)
副本集成员
Secondary
正常情况下,复制集的Seconary会参与Primary选举(自身也可能会被选为Primary),并从
Primary同步最新写入的数据,以保证与Primary存储相同的数据。
Secondary可以提供读服务,增加Secondary节点可以提供复制集的读服务能力,同时提升复制集
的可用性。另外,Mongodb支持对复制集的Secondary节点进行灵活的配置,以适应多种场景的需求。
Arbiter
Arbiter节点只参与投票,不能被选为Primary,并且不从Primary同步数据。
比如你部署了一个2个节点的复制集,1个Primary,1个Secondary,任意节点宕机,复制集将不能提供服务了(无法选出Primary),这时可以给复制集添加一个Arbiter节点,即使有节点宕机,仍能选出Primary。
Arbiter本身不存储数据,是非常轻量级的服务,当复制集成员为偶数时,最好加入一个Arbiter节点,以提升复制集可用性。
搭建副本集群
准备工作
- 安装 docker
docker安装步骤:https://docs.docker.com/engine/install/centos/
- 下载Mongo镜像
下载 mongo 镜像,如有需求可加上版本号
1 | docker pull mongo |
创建挂载目录
1 | mkdir -p /tmp/mongo/mongo{1..3}/data |
建立网络
1 | docker network create mongo-cluster |
生成key
1 | cd /tmp/mongo/conf |
创建配置文件
1 | vi /tmp/mongo/conf/mongodb.conf |
创建3个容器
1 | docker run --net mongo-cluster \ |
参数说明
1 | docker run 从镜像启动一个容器 |
操作容器
登录容器
使用我们的本地客户端登录容器,登录任意一台容器都可以
1 | mongo 127.0.0.1:30001 |
初始化集群
执行下面的命令进行初始化集群
1 | rs.initiate({ |
查看集群信息
1 | db.hello() |
主从复制测试
主节点添加数据
在主节点执行下面的命令
1 | # 创建 mytest数据库 |
从节点查看数据
切换到从节点,进行查看数据
1 | mongo 127.0.0.1:30002 |
我们发现无法进行查看,报错不是master节点,这个时候需要配置主节点可以查看db.setSecondaryOk很关键,代表允许连接读取非 primary 实例数据,没有设置进行查询时报错
1 | use mytest; |
这个时候就可以查看数据了,但是有一个警告
主从切换测试
停掉主节点
1 | docker stop mongo1 |
查看从节点信息
我们看刚才的从节点已经变成了主节点
1 | rs.isMaster() |
启动停止的节点
1 | docker start mongo1 |
连接节点查看信息
登录后我们发现已经变成了从节点
1 | mongo 127.0.0.1:30001 |
扩缩容
扩容节点
新增一个docker节点
1 | docker run --net mongo-cluster \ |
从主节点新增节点
1 | rs.add("shard1-server4:27017") |
查看节点信息
1 | rs.isMaster() |
到新增的副本节点查看数据
1 | mongo 127.0.0.1:30004 |
我们发现数据已经同步过来了
缩容节点
将我们刚才添加的模拟mongo4节点删除,在主节点执行以下命令
1 | rs.remove("mongo4:27017") |
查看节点信息
1 | rs.isMaster() |
MongoDB分片搭建
分片(sharding)是MongoDB用来将大型集合分割到不同服务器(或者说一个集群)上所采用的方法。尽管分片起源于关系型数据库分区,但MongoDB分片完全又是另一回事。
和MySQL分区方案相比,MongoDB的最大区别在于它几乎能自动完成所有事情,只要告诉
MongoDB要分配数据,它就能自动维护数据在不同服务器之间的均衡。
分片介绍
分片的目的
高数据量和吞吐量的数据库应用会对单机的性能造成较大压力,大的查询量会将单机的CPU耗尽,大的数据量对单机的存储压力较大,最终会耗尽系统的内存而将压力转移到磁盘IO上。为了解决这些问题,有两个基本的方法: 垂直扩展和水平扩展。
- 垂直扩展:增加更多的CPU和存储资源来扩展容量。
- 水平扩展:将数据集分布在多个服务器上。水平扩展即分片。
分片设计思想
分片为应对高吞吐量与大数据量提供了方法。使用分片减少了每个分片需要处理的请求数,因此,通过水平扩展,集群可以提高自己的存储容量和吞吐量。举例来说,当插入一条数据时,应用只需要访问存储这条数据的分片.
使用分片减少了每个分片存储的数据。
例如,如果数据库1tb的数据集,并有4个分片,然后每个分片可能仅持有256 GB的数据。如果有40个分片,那么每个切分可能只有25GB的数据。
分片机制的优势
分片机制提供了如下三种优势
自动路由
对集群进行抽象,让集群“不可见”
MongoDB自带了一个叫做mongos的专有路由进程。mongos就是掌握统一路口的路由器,其会将客户端发来的请求准确无误的路由到集群中的一个或者一组服务器上,同时会把接收到的响应拼装起来发回到客户端。
保证高可用
保证集群总是可读写
MongoDB通过多种途径来确保集群的可用性和可靠性。将MongoDB的分片和复制功能结合使用,在确保数据分片到多台服务器的同时,也确保了每份数据都有相应的备份,这样就可以确保有服务器挂掉时,其他的从库可以立即接替坏掉的部分继续工作。
易于扩展
使集群易于扩展
当系统需要更多的空间和资源的时候,MongoDB使我们可以按需方便的扩充系统容量。
分片架构
分片集群的构造
mongos
数据路由,和客户端打交道的模块。mongos本身没有任何数据,他也不知道该怎么处理这数据,去找config server
Mongos本身并不持久化数据,Sharded cluster所有的元数据都会存储到Config Server,而用户的数据会分散存储到各个shard。Mongos启动后,会从配置服务器加载元数据,开始提供服务,将用户的请求正确路由到对应的分片。
config server
所有存、取数据的方式,所有shard节点的信息,分片功能的一些配置信息。可以理解为真实数据的元数据。
shard
真正的数据存储位置,以chunk块为单位存数据。
Mongos的路由功能
当数据写入时,MongoDB Cluster根据分片键设计写入数据。当外部语句发起数据查询时,MongoDB根据数据分布自动路由至指定节点返回数据。
集群中数据分布
Chunk是什么
在一个shard server内部,MongoDB还是会把数据分为chunks,每个chunk代表这个shard server内部一部分数据,chunk的产生,会有以下两个用途:
- Splitting:当一个chunk的大小超过配置中的chunk size时,MongoDB的后台进程会把这个chunk切分成更小的chunk,从而避免chunk过大的情况
- Balancing:在MongoDB中,balancer是一个后台进程,负责chunk的迁移,从而均衡各个shard
- server的负载,系统初始1个chunk,chunk size默认值64M,生产库上选择适合业务的chunk size是最好的。mongoDB会自动拆分和迁移chunks。
chunk的特点
- 使用chunk来存储数据
- 集群搭建完成之后,默认开启一个chunk,大小是64M,
- 存储需求超过64M,chunk会进行分裂,如果单位时间存储需求很大,设置更大的chunk
- chunk会被自动均衡迁移。
chunksize的选择
- 适合业务的chunksize是最好的。
- chunk的分裂和迁移非常消耗IO资源;chunk分裂的时机:在插入和更新,读数据不会分裂。
- 小的chunksize:数据均衡时迁移速度快,数据分布更均匀,数据分裂频繁,路由节点消耗更多资源。
- 大的chunksize:数据分裂少。数据块移动集中消耗IO资源,通常100-200M
chunk分裂及迁移
随着数据的增长,其中的数据大小超过了配置的chunk size,默认是64M,则这个chunk就会分裂成两个。数据的增长会让chunk分裂得越来越多。
这时候,各个shard 上的chunk数量就会不平衡。这时候,mongos中的一个组件balancer 就会执行自动平衡。把chunk从chunk数量最多的shard节点挪动到数量最少的节点。
chunkSize对分裂及迁移的影响
MongoDB 默认的 chunkSize 为64MB,如无特殊需求,建议保持默认值;chunkSize 会直接影响到 chunk 分裂、迁移的行为。
- chunkSize 越小,chunk 分裂及迁移越多,数据分布越均衡;反之,chunkSize 越大,chunk 分裂及迁移会更少,但可能导致数据分布不均。
- chunkSize 太小,容易出现 jumbo chunk(即shardKey 的某个取值出现频率很高,这些文档只能放到一个 chunk 里,无法再分裂)而无法迁移;chunkSize 越大,则可能出现 chunk 内文档数太多(chunk 内文档数不能超过 250000 )而无法迁移。
- chunk 自动分裂只会在数据写入时触发,所以如果将 chunkSize 改小,系统需要一定的时间来将
- chunk 分裂到指定的大小。
- chunk 只会分裂,不会合并,所以即使将 chunkSize 改大,现有的 chunk 数量不会减少,但
- chunk 大小会随着写入不断增长,直到达到目标大小。
数据分片
分片键shard key
MongoDB中数据的分片是、以集合为基本单位的,集合中的数据通过片键(Shard key)被分成多部分。其实片键就是在集合中选一个键,用该键的值作为数据拆分的依据。
所以一个好的片键对分片至关重要。分片键必须是一个索引,通过sh.shardCollection会自动创建索引(前提是此集合不存在的情况下)。一个自增的分片键对写入和数据均匀分布就不是很好,因为自增的片键总会在一个分片上写入,后续达到某个阀值可能会写到别的分片。但是按照片键查询会非常高效。
随机片键对数据的均匀分布效果很好。注意尽量避免在多个分片上进行查询。在所有分片上查询,mongos会对结果进行归并排序。
对集合进行分片时,你需要选择一个片键,片键是每条记录都必须包含的,且建立了索引的单个字段或复合字段,MongoDB按照片键将数据划分到不同的数据块中,并将数据块均衡地分布到所有分片中。
为了按照片键划分数据块,MongoDB使用基于范围的分片方式或者 基于哈希的分片方式。
注意事项
- 分片键一经设置,不可修改,不可删除。
- 执行了数据分片操作后,均衡器会对满足条件的数据进行拆分,这将占用实例的资源,请在业务低峰期操作
- 分片键必须有索引。
- 分片键大小限制512bytes。
- 分片键用于路由查询。
- MongoDB不接受已进行collection级分片的collection上插入分片
- 分片键不支持空值插入
分片键分类
范围分片
MongoDB按照片键的值的范围将数据拆分为不同的块(chunk),每个块包含了一段范围内的数据。
Sharded Cluster支持将单个集合的数据分散存储在多个shard上,用户可以指定根据集合内文档的某个字段即shard key来进行范围分片(range sharding)。
对于基于范围的分片,MongoDB按照片键的范围把数据分成不同部分。
- 优点: mongos可以快速定位请求需要的数据,并将请求转发到相应的Shard节点中。
- 缺点: 可能导致数据在Shard节点上分布不均衡,容易造成读写热点,且不具备写分散性。
哈希分片
MongoDB计算单个字段的哈希值作为索引值,并以哈希值的范围将数据拆分为不同的块。
分片过程中利用哈希索引作为分片的单个键,且哈希分片的片键只能使用一个字段,而基于哈希片键最大的好处就是保证数据在各个节点分布基本均匀。
对于基于哈希的分片,MongoDB计算一个字段的哈希值,并用这个哈希值来创建数据块。在使用基于哈希分片的系统中,拥有”相近”片键的文档很可能不会存储在同一个数据块中,因此数据的分离性更好一些。
Hash分片与范围分片互补,能将文档随机的分散到各个chunk,充分的扩展写能力,弥补了范围分片的不足,但不能高效的服务范围查询,所有的范围查询要分发到后端所有的Shard才能找出满足条件的文档。
- 优点:可以将数据更加均衡地分布在各Shard节点中,具备写分散性。
- 缺点:不适合进行范围查询,进行范围查询时,需要将读请求分发到所有的Shard节点。
什么情况下使用分片
当您遇到如下两个问题时,您可以使用Sharded cluster来解决您的问题:
- 存储容量受单机限制,即磁盘资源遭遇瓶颈。
- 读写能力受单机限制,可能是CPU、内存或者网卡等资源遭遇瓶颈,导致读写能力无法扩展。
如何确定容量
当需要决定使用Sharded cluster时,到底应该部署多少个shard、多少个mongos?shard、mongos的数量归根结底是由应用需求决定:
存储型
如果您使用sharding只是解决海量数据存储问题,访问并不多。
假设单个shard能存储M(1G), 需要的存储总量是N(75G),那么您可以按照如下公式来计算实际需要的shard、mongos数量:
- numberOfShards = N/M/0.75 (假设容量水位线为75%)
- numberOfMongos = 2+(对访问要求不高,至少部署2个mongos做高可用即可)
计算型
如果您使用sharding是解决高并发写入(或读取)数据的问题,总的数据量其实很小。
您要部署的shard、mongos要满足读写性能需求,容量上则不是考量的重点。假设单个shard最大QPS(Query Per Second)为M,单个mongos最大QPS为Ms,需要总的QPS为Q。那么您可以按照如下公式来计算实际需要的shard、mongos数量:
- numberOfShards = N/M/0.75 (假设容量水位线为75%)
- numberOfMongos = 2+(对访问要求不高,至少部署2个mongos做高可用即可)
如何选择shard key
如果sharding要同时解决上述2个问题,则按需求更高的指标来预估。以上估算是基于sharded cluster里数据及请求都均匀分布的理想情况。但实际情况下,分布可能并不均衡,为了让系统的负载分布尽量均匀,就需要合理的选择shard key。
分片类型
MongoDB Sharded cluster支持2种分片方式:
- 范围分片,通常能很好的支持基于shard key的范围查询。
- Hash 分片,通常能将写入均衡分布到各个shard。
问题
上述2种分片策略都无法解决以下3个问题:
- shard key取值范围太小(low cardinality),比如将数据中心作为shard key,而数据中心通常不会很多,分片的效果肯定不好。
- shard key某个值的文档特别多,这样导致单个chunk特别大(及 jumbo chunk),会影响chunk迁移及负载均衡。
- 根据非shardkey进行查询、更新操作都会变成scatter-gather查询,影响效率。
如何评估分片
好的shard key应该拥有如下特性:
- key分布足够离散(sufficient cardinality)
- 写请求均匀分布(evenly distributed write)
- 尽量避免scatter-gather查询(targeted read)
数据分片-分片案例
我们要对我们的Blog数据进行分片,假如数据量是百万以及千万级别的数据
1 | { |
分片方案
likes范围分片
likes作为shard key,范围分片
- 新的写入都是连续的likes,都会请求到同一个shard,写分布不均。
- 根据by的查询会分散到所有shard上查询,效率低。
likes哈希分
- 写入能均分到多个shard。
- 根据by的查询会分散到所有shard上查询,效率低。
by哈希分片
by作为shardKey,hash分片(如果ID没有明显的规则,范围分片也一样)
- 写入能均分到多个shard。
- 同一个by对应的数据无法进一步细分,只能分散到同一个chunk,会造成jumbo chunk根据by的查询只请求到单个shard。不足的是,请求路由到单个shard后,根据likes的范围查询需要全表扫描并排序。
组合分片
(by,likes)组合起来作为shardKey,范围分片(Better)
- 写入能均分到多个shard。
- 同一个by的数据能根据likes进一步分散到多个chunk。
- 根据likes查询时间范围的数据,能直接利用(by,likes)复合索引来完成。
开启分片
上面我们已经搭建好了三个分片集群了,但是mongos不知道该如何切分数据,也就是我们先前所说的片键,在mongodb中设置片键要做两步
开启数据库分片
开启数据库分片功能,命令很简单 enablesharding(),这里我就开启test数据库。
1 | # 对集合所在的数据库启用分片功能 |
对片键的字段建立索引
分片键必须要有索引才可以,并且一个集合只能有一个分片健;
1 | db.blog.createIndex({title:1}) |
因为我们要对by分片,索引需要对likes字段做升序索引
指定分片键
使用如下命令可以指定分片键
1 | sh.shardCollection("<database>.<collection>",{ "<key>":<value> } ) |
说明
<database>
:数据库名。<collection>
:集合名。<key>
:分片的键,MongoDB将根据片键的值进行数据分片。<value>
- 1:表示基于范围分片,通常能很好地支持基于片键的范围查询。
- “hashed”:表示基于哈希分片,通常能将写入均衡分布到各Shard节点中
指定集合中分片的片键,并且指定使用哈希分片和范围分片
1 | sh.shardCollection("test.blog", {"by":"hashed","likes":1}) |
查看分片状态
1 | sh.status() |
查看数据分布
通过该命令可以查看数据的分布
1 | db.blog.getShardDistribution(); #可以查看数据分布 |
我们发现现在是没有分片的,数据只在一个分片中
插入数据
我们对blog中设置分片键后,我们需要大量插入数据进行测试,我们插入十万条数据
调用Controller插入数据
1 | http://localhost:8080/blog/batchAdd |
查看分片情况
查看分片状态
1 | sh.status() |
我们发现负载均衡正在运行
下面是具体分片的信息
查看分片分布情况
1 | db.blog.getShardDistribution() |
扩缩容
扩容
准备工作
创建挂载目录
我们先创建挂载目录
1 | # 创建配置文件目录 |
创建密钥文件
因为集群只需要一个密钥文件,我们可以将config-server中的密钥文件复制过来
1 | cp /tmp/mongo-cluster/config-server/conf/mongo.key /tmp/mongo-cluster/shard |
配置配置文件
因为有多个容器,配置文件是一样的,我们只需要创建一个配置文件,其他的容器统一读取该配置文件即可
1 | echo " |
启动新分片
增加分片节点
增加shard4-server节点
1 | version: '2' |
启动服务
1 | docker-compose up -d |
初始化分片组
登录节点后进行初始化分片2
1 | docker exec -it shard4-server1 bin/bash |
执行下面的命令进行初始化分片4,arbiterOnly:true参数是设置为仲裁节点
1 | #进行副本集配置 |
返回ok就表示
创建用户
因为我们需要对用户进行权限管理,我们需要创建用户,这里为了演示,我们创建超级用户 权限是root
1 | use admin |
添加到mongos
要将新增节点添加到mongos中
进入容器处理
1 | docker exec -it mongos-server1 /bin/bash |
查看均衡信息
1 | db.blog.getShardDistribution() |
查看分片信息
1 | sh.status() |
缩容
Mongodb分片集群shard节点缩容相对是比较简单的,可以利用MongoDB自身的平衡器来将预下线中的分片中存储的数据进行转移,待预下线shard节点中无任何数据库,进行下线处理。所有的下线操作通过mongos进行管理实现。
查看分片状态
查看分片集群是否开启平衡器
1 | sh.getBalancerState(); |
删除分片
发起删除分片节点命令,平衡器开始自动迁移数据
1 | use admin |
等待平衡器将需要删除的分片节点中数据全部迁移完毕
正在进行负载均衡
1 | sh.status() |
查看迁移数据
1 | sh.status() |
我们发现数据已经迁移完成
再次删除节点
1 | use admin |
真正从分片集群中删除shard副本集信息
发现数据还在迁移,稍等,然后在执行删除命令
1 | use admin |
这次是真正的删除了
查看集群状态
检查分片缩容后的分片集群状态
1 | sh.status() |
我们发现只剩下三个节点
MongoDB集群搭建
MongoDB集群简介
mongodb 集群搭建的方式有三种:
- 主从备份(Master - Slave)模式,或者叫主从复制模式。
- 副本集(Replica Set)模式
- 分片(Sharding)模式
其中,第一种方式基本没什么意义,官方也不推荐这种方式搭建。另外两种分别就是副本集和分片的方式。
Mongo分片高可用集群搭建
概述
为解决mongodb在replica set每个从节点上面的数据库均是对数据库的全量拷贝,从节点压力在高并发大数据量的场景下存在很大挑战,同时考虑到后期mongodb集群的在数据压力巨大时的扩展性,应对海量数据引出了分片机制。
什么是分片
分片是将数据库进行拆分,将其分散在不同的机器上的过程,无需功能强大的服务器就可以存储更多的数据,处理更大的负载,在总数据中,将集合切成小块,将这些块分散到若干片中,每个片只负载总数据的一部分,通过一个知道数据与分片对应关系的组件mongos的路由进程进行操作。
基础组件
其利用到了四个组件:mongos,config server,shard,replica set
mongos
数据库集群请求的入口,所有请求需要经过mongos进行协调,无需在应用层面利用程序来进行路由选择,mongos其自身是一个请求分发中心,负责将外部的请求分发到对应的shard服务器上,mongos作为统一的请求入口,为防止mongos单节点故障,一般需要对其做HA(高可用,Highly Available缩写)。
config server
配置服务器,存储所有数据库元数据(分片,路由)的配置。mongos本身没有物理存储分片服务器和数据路由信息,只是缓存在内存中来读取数据,mongos在第一次启动或后期重启时候,就会从config server中加载配置信息,如果配置服务器信息发生更新会通知所有的mongos来更新自己的状态,从而保证准确的请求路由,生产环境中通常也需要多个config server,防止配置文件存在单节点丢失问题。
shard
在传统意义上来讲,如果存在海量数据,单台服务器存储1T压力非常大,考虑到数据库的硬盘,网络IO,还有CPU,内存的瓶颈,如果多台进行分摊1T的数据,到每台上就是可估量的较小数据,在mongodb集群只要设置好分片规则,通过mongos操作数据库,就可以自动把对应的操作请求转发到对应的后端分片服务器上。
replica set
在总体mongodb集群架构中,对应的分片节点,如果单台机器下线,对应整个集群的数据就会出现部分缺失,这是不能发生的,因此对于shard节点需要replica set来保证数据的可靠性,生产环境通常为2个副本+1个仲裁。
整体架构
整体架构涉及到15个节点,我们这里使用Docker容器进行部署
那么我们先来总结一下我们搭建一个高可用集群需要多少个Mongo
- mongos: 3台
- configserver : 3台
- shard : 3片; 每个分片由三个节点构成
容器部署情况
整体架构预览
基础环境准备
安装Docker
本次使用Docker环境进行搭建,需要提前准备好Docker环境
创建Docker网络
因为需要使用Docker搭建MongoDB集群,所以先创建Docker网络
1 | docker network create mongo-cluster |
搭建ConfigServer副本集
我们先来搭建ConfigServer的副本集,这里面涉及到三个节点,我们需要创建配置文件以及启动容器
创建挂载目录
我们需要创建对应的挂载目录来存储配置文件以及日志文件
1 | # 创建配置文件目录 |
创建密钥文件
因为我们知道搭建的话一定要高可用,而且一定要权限,这里mongo之间通信采用秘钥文件,所以我们先进行生成密钥文件
1 | # 创建密钥文件 |
创建配置文件
因为由多个容器,配置文件是一样的,我们只需要创建一个配置文件,其他的容器统一读取该配置
文件即可
1 | echo " |
启动容器
启动config-server1
1 | docker run --name config-server1 -d \ |
启动config-server2
1 | docker run --name config-server2 -d \ |
启动config-server3
1 | docker run --name config-server3 -d \ |
初始化config-server
登录容器
进入第一台容器
1 | docker exec -it config-server1 bash |
执行命令
执行以下命令进行MongoDB容器的初始化
1 | rs.initiate( |
如果出现OK表示MongoDB配置服务器已经初始化成功
创建用户
因为我们需要对用户进行权限管理,我们需要创建用户,这里为了演示,我们创建超级用户 权限是root
1 | use admin |
这样就在MongoDB的admin数据库添加了一个用户名为root 密码是root的用户
搭建Shard分片组
由于mongos是客户端,所以我们先搭建好config以及shard之后再搭建mongos。
创建挂载目录
我们先创建挂载目录
1 | # 创建配置文件目录 |
搭建shard1分片组
在同一台服务器上初始化一组分片
创建密钥文件
因为集群只需要一个密钥文件,我们可以将config-server中的密钥文件复制过来
1 | cp /tmp/mongo-cluster/config-server/conf/mongo.key /tmp/mongo-cluster/shard1- |
配置配置文件
因为有多个容器,配置文件是一样的,我们只需要创建一个配置文件,其他的容器统一读取该配置文件即可
1 | echo " |
启动shard1-server1
1 | docker run --name shard1-server1 -d \ |
启动shard1-server2
1 | docker run --name shard1-server2 -d \ |
启动shard1-server3
1 | docker run --name shard1-server3 -d \ |
初始化shard1分片组
并且制定第三个副本集为仲裁节点
1 | docker exec -it shard1-server1 bin/bash |
登录后进行初始化节点,这里面arbiterOnly:true是设置为仲裁节点
1 | #进行副本集配置 |
显示OK即副本集创建成功
创建用户
因为我们需要对用户进行权限管理,我们需要创建用户,这里为了演示,我们创建超级用户 权限是root
1 | use admin |
查看节点信息
1 | rs.isMaster() |
搭建shard2分片组
创建密钥文件
因为集群只需要一个密钥文件,我们可以将config-server中的密钥文件复制过来
1 | cp /tmp/mongo-cluster/config-server/conf/mongo.key /tmp/mongo-cluster/shard2- |
配置配置文件
因为有多个容器,配置文件是一样的,我们只需要创建一个配置文件,其他的容器统一读取该配置文件即可
1 | echo " |
启动shard2-server1
1 | docker run --name shard2-server1 -d \ |
启动shard2-server2
1 | docker run --name shard2-server2 -d \ |
启动shard2-server3
1 | docker run --name shard2-server3 -d \ |
初始化shard2分片组
登录节点后进行初始化分片2
1 | docker exec -it shard2-server1 bin/bash |
执行下面的命令进行初始化分片2,arbiterOnly:true参数是设置为仲裁节点
1 | #进行副本集配置 |
返回ok就表示
创建用户
因为我们需要对用户进行权限管理,我们需要创建用户,这里为了演示,我们创建超级用户 权限是root
1 | use admin |
搭建shard3分片组
创建密钥文件
因为集群只需要一个密钥文件,我们可以将config-server中的密钥文件复制过来
1 | cp /tmp/mongo-cluster/config-server/conf/mongo.key /tmp/mongo-cluster/shard3- |
配置配置文件
因为有多个容器,配置文件是一样的,我们只需要创建一个配置文件,其他的容器统一读取该配置文件即可
1 | echo " |
启动shard3-server1
1 | docker run --name shard3-server1 -d \ |
启动shard3-server2
1 | docker run --name shard3-server2 -d \ |
启动shard3-server3
1 | docker run --name shard3-server3 -d \ |
初始化shard3分片组
登录节点后进行初始化分片2
1 | docker exec -it shard3-server1 bin/bash |
执行下面的命令进行初始化分片3,arbiterOnly:true参数是设置为仲裁节点
1 | #进行副本集配置 |
创建用户
因为我们需要对用户进行权限管理,我们需要创建用户,这里为了演示,我们创建超级用户 权限是root
1 | use admin |
搭建Mongos
mongos负责查询与数据写入的路由,是实例访问的统一入口,是一个无状态的节点,每一个节点
都可以从config-server节点获取到配置信息
创建挂载目录
我们需要创建对应的挂载目录来存储配置文件以及日志文件
1 | # 创建配置文件目录 |
创建密钥文件
因为集群只需要一个密钥文件,我们可以将config-server中的密钥文件复制过来
1 | cp /tmp/mongo-cluster/config-server/conf/mongo.key /tmp/mongo-cluster/mongos- |
创建配置文件
因为有多个容器,配置文件是一样的,我们只需要创建一个配置文件,其他的容器统一读取该配置文件即可,因为Mongos只负责路由,就不需要数据文件了,并且mongos服务是不负责认证的,需要将authorization配置项删除
1 | echo " |
启动mongos集群
启动mongos1
1 | docker run --name mongos-server1 -d \ |
启动mongos2
1 | docker run --name mongos-server2 -d \ |
启动mongos3
1 | docker run --name mongos-server3 -d \ |
配置mongos-server1
因为mongos是无中心的配置,所有需要每一台都需要进行分片配置
进入容器
1 | docker exec -it mongos-server1 /bin/bash |
登录Mongos
使用前面设置的root用户密码
1 | use admin; |
配置分片
进行配置分片信息
1 | sh.addShard("shard1/shard1-server1:27017,shard1-server2:27017,shard1- |
配置mongos-server2
因为mongos是无中心的配置,所有需要每一台都需要进行分片配置
进入容器
1 | docker exec -it mongos-server2 /bin/bash |
登录Mongos
使用前面设置的root用户密码
1 | use admin; |
配置分片
进行配置分片信息
1 | sh.addShard("shard1/shard1-server1:27017,shard1-server2:27017,shard1- |
配置mongos-server3
因为mongos是无中心的配置,所有需要每一台都需要进行分片配置
进入容器
1 | docker exec -it mongos-server3 /bin/bash |
登录Mongos
使用前面设置的root用户密码
1 | use admin; |
配置分片
进行配置分片信息
1 | sh.addShard("shard1/shard1-server1:27017,shard1-server2:27017,shard1- |
Docker-compose方式搭建
环境准备
初始化目录脚本
1 | # 创建config-server 目录 |
生成密钥文件
1 | # 创建密钥文件 |
创建配置文件
1 | echo " |
启动服务
docker-compos配置文件
使用docker-compos方式启动Docker容器
1 | version: '2' |
启动服务
1 | docker-compose up -d |
初始化文件
执行下面脚本进行容器初始化
1 | docker exec -it config-server1 bash |
初始化分片
1 | docker exec -it mongos-server1 /bin/bash |