分布式文件存储数据库 —— MongoDB
一、MongoDB 介绍
MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写,旨在为WEB应用提供可扩展的高性能数据存储解决方案。
MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 json 的 bson 格式,是一种文档型的 NoSQL 数据库,因此可以存储比较复杂的数据类型。
MongoDB 最大的特点是它支持的查询语言非常强大,其语法有点类似面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
二、文档型数据库
1、定义
文档数据库(也称为面向文档的数据库或文档存储)是在文档中存储信息的数据库,是非关系型数据库的一种。
如:
{
_id:1,
"name":"Tony",
"age":20,
"phone":13417135801,
"likes":[
"swimming",
"shopping",
"Computer Game"
]
}
什么是文档?
一个文档就是文档型数据库中的一条记录。文档通常存储关于一个对象及其任何相关元数据的信息。
文档是以字段-值成对的形式存储数据。值的类型和结构可以有多种,包括字符串、数字、日期、数组等。文档存储的格式可以是 JSON,BSON(二进制形式的JSON)和XML。
什么是集合?
集合就是一组文档。集合里的文档通常都有相似的结构。
一个集合里的所有文档不需要有一致的字段。有些文档型的数据库会提供格式校验功能,因此如果需要的话,一个集合的字段也可以固定下来。
2、关键特性
- 文档模型:数据被存储在文档中(而不是像其他数据库那样,以结构化的形式,比如表格、图形的方式存储)。
- 结构灵活:文档型数据库不要求太严格的数据格式,一个集合中文档和文档之间的字段可以不一致。
- 分布和弹性:扩展性强。
-
查询语言:有自己的查询语言和API。
3、和关系型数据库对比
(1)区别
- 数据模式的直观性:文档型数据库中的存储单位“文档”直接对应应用层代码中的“对象”,开发者使用起来更加方便,不需要跨表或者表连接等操作。
- 使用JSON带来的好处:JSON已经成为数据交换和存储的标准。JSON文档是轻量级的、独立于语言的、人类可读的。开发人员可以按照应用程序需要的方式构造数据——丰富的对象、键值对、表、地理空间和时间序列数据,或者图形的节点和边。
-
模式的灵活性:因为结构比较灵活,开发者在创建文档时,不用像使用关系型数据库那样,首先去定义它的结构;开发者可以在任意时间根据需要去修改数据的结构。
(2)关系
文档数据库可以兼容其他类型的数据库,包括键值对、关系型、图表型等,意味着使用其他数据库存储的数据都可以使用文档数据库代替。
键值对可以用文档中的字段和值建模。文档中的任何字段都可以被索引,这为开发人员查询数据提供了额外的灵活性。
关系型数据在文档数据库中可以存储在一个文档中,也可以像原来那样分开存储在不同的文档中,它们可以手动指定联系(比如互相存储对方的id)也可以使用文档数据提供的关联功能。
为什么不在关系型数据库中使用 JSON 结构?
一些关系型数据库已经增加了对JSON的支持,那为什么不直接定义一个列,类型为JSON呢?简单来说,这种做法会降低开发人员的效率,增加开发成本。它有以下几个缺点:
- 关系型数据库提供的JSON操作方式比较复杂
- 在关系型数据库中JSON数据不区分数据类型,像是字符串和数字,这会使数据的计算、比较和排序变得复杂和容易出错,而原生文档数据库(如MongoDB)支持的丰富数据类型。
- 数据质量无法控制:关系型数据库中JSON无法校验,而且开发人员依旧无法避免定义表结构这样的操作。
-
性能差:关系型数据无法对JSON列的查询进行优化,也不能像文档数据库那样对其中的某些字段添加索引。
4、缺点
一个众所周知的缺点是不支持多文档事务。所谓多文档事务,指的是在一个事务中,需要同时操作多个文档(包括一个集合的多个文档,或者跨集合之间)。
不过有些文档数据库比如 MongoDB已经可以支持多文档事务了。
5、实例
下面举个能充分展示文档型数据库优点的例子。
在一些影评网站上,能看到电影的名称、类型、所属地区、片长等信息,下面还有对电影的评论列表,每条评论可能还有子评论列表(假设评论表只有两级)。
如果用关系型数据库来存储影评网站的信息,首先需要设计一个存储电影信息的 movie 表,存储一级影评的 comments 表,存储二级影评的 subcomments 表。可想而知,每个表的数据量都是很大的,要在页面渲染展示的数据需要对三张表做个联表查询,效率很低。
而如果使用文档型数据库,则可以将这些电影基本信息、一二级影评信息同时存储在一个文档中,一个文档即数据库中的一条数据,索引查找的效率很高。
三、安装配置
1、依赖环境
安装依赖组件
yum install -y net-snmp*
yum install -y cyrus*
若出现以下错误
错误:软件包:1:net-snmp-5.7.2-28.el7.x86_64 (rhel-yum)
需要:libmysqlclient.so.18()(64bit)
您可以尝试添加 --skip-broken 选项来解决该问题
您可以尝试执行:rpm -Va --nofiles --nodigest
则需要安装以下 rpm 包
rpm -ivh Percona-XtraDB-Cluster-shared-55-5.5.37-25.10.756.el6.x86_64.rpm
2、安装 MongoDB
# 解压安装包
tar -zxvf mongodb-linux-x86_64-enterprise-rhel70-3.4.10.tgz -C ~/training/
# 重命名安装目录
mv mongodb-linux-x86_64-enterprise-rhel70-3.4.10 mongodb
安装目录的 bin 子目录下提供了很多的工具和命令,其中 mongod 是启动 server 的工具
启动 MongoDB,默认端口是 27017
cd ~/training/mongodb
bin/mongod
若启动出现以下报错,则先创建目录 /data/db(mongodb 存储数据的默认目录)再重新启动
也可以通过以下命令指定启动使用的目录,如果想要后台启动,则在末尾再加一个 &
bin/mongod --dbpath=/data/db &
通过 ps 命令查看启动的 mongo 进程
ps -aux | grep mongo
四、体系结构
MongoDB 是一个可移植的数据库,它在流行的每一个平台上都可以使用,即所谓的跨平台性,在不同的操作系统上虽然略有差别,但是从整体架构上来看,MongoDB 在不同的平台上是一样的,如数据逻辑结构和物理存储结构等。
1、实例和数据库
基本的概念:
(1)数据库:就是硬盘上的文件
(2)数据库的实例:指的是数据库文件在内存中镜像
一个运行着的 MongoDB 数据库就可以看成是一个 MongDB Server,该 Server 由实例和数据库组成,在一般情况下,一个 MongoDB Server 机器上包含着一个实例或者多个与之对应的数据库,但是在特殊情况下,如硬件投入成本或者特殊的应用需求,也允许一个 Server 机器上可以有多个实例或者多个数据库。
而对 Oracle 来说,数据库只有一个,实例可以在内存中存在多个,并且可以借助多实例实现 HA 的功能,这就是 RAC(Real Application Cluster)。
MongoDB 中一系列物理文件(数据文件、日志文件等)的集合与之对应的逻辑结构(集合、文档等)被称之为数据库,简单地说,就是数据库是由一系列与磁盘有关的物理文件组成。
2、逻辑存储结构
MongoDB 的逻辑结构是一种层次结构,主要由:文档(Document)、集合(Collection)、数据库(database)这三部分组成,是面向用户的,用户使用 MongoDB 开发应用程序使用的就是逻辑结构。
- 数据库:databases(一个MongoDB实例支持多个数据库,多个集合组成了数据库)
- 集合:Collection(多个文档组成一个集合,相当于关系数据库的表)
- 文档:Document(MongoDB中的文档,相当于关系数据库中的一行记录)
3、物理存储结构
MongoDB 默认的数据目录是 /data/db,它负责存储所有的 MongoDB 的数据文件,在 MongoDB 内部,每个数据库都包含一个 .ns 文件、多个数据文件和日志文件,而这些文件会随着数据量的增加变得越来越多,具体如下:
-
命名空间文件:后缀是ns,文件大小:16M。
-
数据文件:后缀是0、1、2......,.0文件16M、.1文件32M,依次往后翻倍,最大值为2G,这样可以让小数据库不浪费太多空间,大数据库能够使用磁盘上连续的空间,通过牺牲空间,获取时间。
-
日志文件
-
系统日志:记录系统的启动日志、告警信息等。
-
journal日志:重做日志,即:redolog,用于恢复集合中的数据。
-
oplog:复制操作日志,只有主动复制才有。
-
慢查询日志:需要配置,一般生产中大于200毫秒。
MongoDB的存储引擎:WiredTiger(默认)、MMAPv1(内存映射)、InMemory。
不显式指定引擎则使用的就是 WiredTiger 引擎,数据目录下不会有 ns 等文件
也可以在启动 Mongod 时指定要使用的引擎,MMAPv1 是之前的默认引擎
使用 mongo shell,创建一个数据库,可以在对应的数据目录下看到对应的 ns 文件和数字后缀的数据文件
五、Mongo Shell
1、启动配置
启动 Mongo 时,它会检查用户 HOME 目录下的一个 Js 文件 .mongorc.js。如果找到,mongo 在首次显示提示信息前解析 .mongorc.js 的内容,并在启动 shell 时定制地回显一些信息,可以使用 --norc
选项来阻止加载该 js。
如果使用 shell 执行一个 JavaScript 文件或计算表达式,那么通过在命令行使用 --eval
选项或者指定一个 .js 文件给 mongo,mongo 会在完成 JavaScript 的处理后读取 .mongorc.js 文件。
下面演示通过该 js 来实现一些功能(官方文档:https://www.mongodb.com/docs/mongodb-shell/mongoshrc/#std-label-mongoshrc-js)。
# 创建与当前会话发出操作的数量提示的配置
cmdCount = 1;
prompt = function(){
return "mongo " + (cmdCount++) + ">";
}
效果如下,shell 会话会对指令进行计数
# 显示数据库名称和主机名
host = db.serverStatus().host;
cmdCount = 1;
prompt = function(){
return db +"@"+host+" " + (cmdCount++) + ">";
}
效果如下
# 显示数据库已启动时间和当前数据库中所有集合中的文档计数
prompt = function() {
return "Uptime:" + db.serverStatus().uptime +
" Documents:" + db.stats().objects +
" > ";
}
效果如下
2、基本操作和数据类型
(1)基本操作
# 1、显示所有数据库(默认数据库为 test)
# mongo shell 语法
show dbs;
# 兼容 SQL 标准语法
show databases;
# 2、切换数据库
use dbname;
# 3、显示数据库中所有表
# mongo shell 语法
show collections;
# 兼容 SQL 标准语法
show tables;
# 4、显示当前数据库
db
【注意】不存在的库和表会自动创建,无需先创建。
(2)数据类型
MongoDB 支持多种数据类型,一些常用的如下:
数据类型 | 说明 |
---|---|
字符串 | 存储数据最常用的数据类型,MongoDB中的字符串必须为UTF-8 |
整型 | 存储数值,整数可以是 32 位或 64 位,具体取决于服务器 |
布尔类型 | 用于存储布尔值(true/false) |
双精度浮点数 | 存储浮点值 |
数组 | 用于将数组、列表或多个值存储到一个键中 |
时间戳 | ctimestamp,当文档被修改和添加时,可以方便地进行录制 |
对象 | 此数据类型用于嵌入式文档 |
Null | 用于存储 Null 值 |
对象ID | 用于存储文档的ID |
二进制数据 | 此数据类型用于存储二进制数据 |
正则表达式 | 此数据类型用于正则表达式 |
日期类型:Date
- Date():表示当前时间,插入的是一个字符串类型
- new Date():插入的是一个isodate类型,表示格林尼治标准时间
- ISODate():效果等同 new Date()
ObjectId
ObjectId 是每个文档必须有的字段,如果不指定会自动生成,作用类似于 MySQL 数据库的表主键。在关系型数据中,主键都是设置成自增的,但是在分布式环境下,这种方式就不可行了,会产生冲突,为此 MongoDB 采用了一个称之为 ObjectId 的类型来做主键。ObjectId 是一个12字节的 BSON 类型字符串。按照字节顺序,一次代表:
- 前4个字节表示时间戳
- 接下来3个字节是机器识别码
- 紧接的两个字节由进程id组成(PID)
- 最后3个字节是随机数
数字:NumberInt、NumberLong、NumberDecimal
通常情况下,mongo shell 将所有的数据当做浮点数对待,mongo shell 提供 NumberInt() 包装类处理 32 位整型数据,提供 NumberLong() 包装类处理 64 位整型数据。
MongoDB 3.4 新增对 decimal128 format 的支持,最多支持 34 位小数位。跟 Double 不同,decimal 存储的是实际的数据,不存在精度问题,以 9.99 为例,decimal NumberDecimal("9.99") 的值就是 9.99;而 Double 类型的 9.99 实际用得是一个近似值如 9.99000000000002131628....
下面测试对着多种数据类型的插入和查询,看看有什么区别。
# 插入数据
db.testnumber.insert({ "_id" : 1, "val" : NumberDecimal( "9.99" ), "description" : "Decimal" })
db.testnumber.insert({ "_id" : 2, "val" : 9.99, "description" : "Double" })
db.testnumber.insert({ "_id" : 3, "val" : 10, "description" : "Double" })
db.testnumber.insert({ "_id" : 4, "val" : NumberLong(10), "description" : "Long" })
db.testnumber.insert({ "_id" : 5, "val" : NumberDecimal( "10.0" ), "description" : "Decimal" })
查询数据,Mongo shell的find相当于 SQL 中的 where
# 查询文档中 val 字段值为 9.99 的所有文档
db.testnumber.find({"val":9.99})
# 查询文档中 val 字段值为 NumberDecimal("9.99") 的所有文档
db.testnumber.find({"val":NumberDecimal( "9.99" )})
# 查询文档中 val 字段值为 10 的所有文档
db.testnumber.find({"val":10})
db.testnumber.find({"val":NumberLong(10)})
可以看到,对于浮点数,如果是字面值,默认数据类型是浮点型,所以查询条件 9.99 和 NumberDecimal(9.99) 的结果是不一样的;而对于整数,不管是字面值 10,还是 NumberLong(10)、NumberInt(10) 查询的结果是一样的。
3、CURD
参考官网:https://www.mongodb.com/docs/manual/crud/
(1)增
可以使用 insert、insertOne、insertMany。
插入一条数据
db.test1.insertOne({"_id":"stu001", name:"Mary", age:25})
插入多条数据
db.test1.insertMany([
{"_id":"stu002",name:"Tom",age:25},
{"_id":"stu003",name:"Jerry",age:25},
{"_id":"stu004",name:"Mike",age:25},
{"_id":"stu005",name:"Jone",age:25}
])
insert 是对 insertOne、insertMany 的统一,可以插入一条或多条数据。
(2)删
使用 deleteOne 删除一条数据,使用 deleteMany 删除多条数据。
db.test1.deleteOne({"_id":"stu001"})
(3)查
普通查询
先造数
db.emp.insert(
[
{_id:7369,ename:'SMITH' ,job:'CLERK' ,mgr:7902,hiredate:'17-12-80',sal:800,comm:0,deptno:20},
{_id:7499,ename:'ALLEN' ,job:'SALESMAN' ,mgr:7698,hiredate:'20-02-81',sal:1600,comm:300 ,deptno:30},
{_id:7521,ename:'WARD' ,job:'SALESMAN' ,mgr:7698,hiredate:'22-02-81',sal:1250,comm:500 ,deptno:30},
{_id:7566,ename:'JONES' ,job:'MANAGER' ,mgr:7839,hiredate:'02-04-81',sal:2975,comm:0,deptno:20},
{_id:7654,ename:'MARTIN',job:'SALESMAN' ,mgr:7698,hiredate:'28-09-81',sal:1250,comm:1400,deptno:30},
{_id:7698,ename:'BLAKE' ,job:'MANAGER' ,mgr:7839,hiredate:'01-05-81',sal:2850,comm:0,deptno:30},
{_id:7782,ename:'CLARK' ,job:'MANAGER' ,mgr:7839,hiredate:'09-06-81',sal:2450,comm:0,deptno:10},
{_id:7788,ename:'SCOTT' ,job:'ANALYST' ,mgr:7566,hiredate:'19-04-87',sal:3000,comm:0,deptno:20},
{_id:7839,ename:'KING' ,job:'PRESIDENT',mgr:0,hiredate:'17-11-81',sal:5000,comm:0,deptno:10},
{_id:7844,ename:'TURNER',job:'SALESMAN' ,mgr:7698,hiredate:'08-09-81',sal:1500,comm:0,deptno:30},
{_id:7876,ename:'ADAMS' ,job:'CLERK' ,mgr:7788,hiredate:'23-05-87',sal:1100,comm:0,deptno:20},
{_id:7900,ename:'JAMES' ,job:'CLERK' ,mgr:7698,hiredate:'03-12-81',sal:950,comm:0,deptno:30},
{_id:7902,ename:'FORD' ,job:'ANALYST' ,mgr:7566,hiredate:'03-12-81',sal:3000,comm:0,deptno:20},
{_id:7934,ename:'MILLER',job:'CLERK' ,mgr:7782,hiredate:'23-01-82',sal:1300,comm:0,deptno:10}
]
)
查询所有员工数据
# mongo shell
db.emp.find()
# 相当于SQL: select * from emp;
查询职位是经理的员工
# mongo shell
db.emp.find({job:"MANAGER"})
# 相当于SQL: select * from emp where job = "MANAGER";
查询职位是经理或文员的员工
# mongo shell
db.emp.find({job:{$in:["MANAGER", "CLERK"]}})
# 相当于SQL: select * from emp where job in ("MANAGER", "CLERK");
# mongo shell
db.emp.find({$or:[{job:"MANAGER"},{job:"CLERK"}]})
# 相当于SQL: select * from emp where job = "MANAGER" or job = "CLERK";
查询10号部门,工资大于2000的员工
# mongo shell
db.emp.find({deptno:10, sal:{$gt:2000}})
# SQL: select * from emp where deptno = 10 and sal > 2000;
嵌套查询
- 文档嵌套查询
造数
db.student2.insertMany([
{_id:"stu0001",name:"Mary",age:25,grade:{chinese:80,math:85,english:90}},
{_id:"stu0002",name:"Tom",age:25,grade:{chinese:86,math:82,english:95}},
{_id:"stu0003",name:"Mike",age:25,grade:{chinese:81,math:90,english:88}},
{_id:"stu0004",name:"Jerry",age:25,grade:{chinese:95,math:87,english:89}}
])
查询语文成绩是81,英语成绩是88的数据
# 查无数据的 shell
db.student2.find({grade:{chinese:81,english:88}})
# 必须把嵌套的 grade 的所有字段按顺序列出,并且条件匹配才能查到数据
db.student2.find({grade:{chinese:81,math:90,english:88}})
如果只想过滤嵌套文档中的部分字段,则语法如下
# 查询语文成绩是81,英语成绩是88的数据
db.student2.find({"grade.chinese":81,"grade.english":88})
# 查询英语成绩大于88,语文成绩大于85的文档
db.student2.find({"grade.english":{$gt:88}, "grade.chinese":{$gt:85}})
- 数组嵌套查询
造数
db.studentbook.insert([
{_id:"stu001",name:"Tom",books:["Hadoop","Java","NoSQL"]},
{_id:"stu002",name:"Mary",books:["C++","Java","Oracle"]},
{_id:"stu003",name:"Mike",books:["Java","MySQL","PHP"]},
{_id:"stu004",name:"Jerry",books:["Hadoop","Spark","Java"]},
{_id:"stu005",name:"Jone",books:["C","Python"]}
])
查询演示
# 查询有Hadoop、Java的文档
# 错误的shell
db.studentbook.find({books:["Hadoop","Java"]})
# 等同于拥有的书为 ["Hadoop","Java"] 的文档
# 如果 {_id:"stu001",name:"Tom",books:["Hadoop","Java","NoSQL"]} 数据改为
# {_id:"stu001",name:"Tom",books:["Hadoop","Java"]} 则该数据可被查询到
# 正确的shell
db.studentbook.find({books:{$all:["Hadoop","Java"]}})
- 数组和文档的嵌套查询
造数
db.studentbook2.insertMany([
{_id:"stu001",name:"Tome",books:[{"bookname":"Hadoop", quantity:2},{"bookname":"Java", quantity:3},{"bookname":"NoSQL", quantity:4}]},
{_id:"stu002",name:"Mary",books:[{"bookname":"C++", quantity:4}, {"bookname":"Java", quantity:3},{"bookname":"Oracle", quantity:5}]},
{_id:"stu003",name:"Mike",books:[{"bookname":"Java", quantity:4}, {"bookname":"MySQL", quantity:1},{"bookname":"PHP", quantity:1}]},
{_id:"stu004",name:"Jone",books:[{"bookname":"Hadoop", quantity:3},{"bookname":"Spark", quantity:2},{"bookname":"Java", quantity:4}]},
{_id:"stu005",name:"Jane",books:[{"bookname":"C", quantity:1}, {"bookname":"Python", quantity:5}]}])
查询演示
# 查询有4本Java的文档
db.studentbook2.find({books:{"bookname":"Java","quantity":4}})
# 查询书数组中第一类书数量大于等于3的文档
db.studentbook2.find({"books.0.quantity":{$gte:3}})
# 查询书数组中至少有一类书数量大于3的文档
db.studentbook2.find({"books.quantity":{$gte:3}})
# 查询Java等于4本的文档(对数组中的每个元素进行匹配)
db.studentbook2.find({"books":{$elemMatch:{"bookname":"Java","quantity":4}}})
查询null或缺失的列
造数
db.student3.insertMany([
{ _id: 1,name:"Tom",age:null },
{ _id: 2,name:"Mary"}
])
查询演示
# 查询值age为null的文档(将缺失的列也一并返回)
db.student3.find({age:null})
# 只返回列存在且值为null的文档,BSON中用10来表示null值
db.student3.find({age:{$type:10}})
# 检查是否缺失某列
# 返回缺失某列的文档
db.student3.find({age:{$exists:false}})
# 返回不缺失某列的文档
db.student3.find({age:{$exists:true}})
(4)改
使用 updateOne、updateMany 来分别更新一条或多条数据。
演示
# 更新7839的工资
db.emp.updateOne({_id:7839},{$set:{sal:8000}})
# SQL: update emp set sal = 8000 where id = 7839;
# 给10号部门的员工涨100块钱
db.emp.updateMany({deptno:{$eq:10}}, {$inc:{sal:100}})
(5)批处理
官方文档:https://www.mongodb.com/docs/manual/core/bulk-write-operations/
通过 bulkWrite 方法来封装要批处理的多个写操作,支持insert、update、remove
演示
db.student2.bulkWrite([
{insertOne:{"document":{ "_id":100, "name":"Tom", "age":25 }}},
{insertOne:{"document":{ "_id":101, "name":"Mary", "age":24 }}},
{updateOne:{"filter":{ "_id":100}, "update":{$set:{"name":"Tom123"}}}}
]
);
六、Web Console
启动时加上 --httpinterface
选项即可,访问端口为 28017
bin/mongod --httpinterface
浏览器访问