分布式文件存储数据库 —— 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

  浏览器访问

你可能感兴趣的:(分布式文件存储数据库 —— MongoDB)