大数据存储:MongoDB实战指南(txt+pdf+epub+mobi电子书下载)


发布时间:2020-09-12 06:44:40

点击下载

作者:郭远威

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

大数据存储:MongoDB实战指南

大数据存储:MongoDB实战指南试读:

前言

多年来,我一直在和数据库存储技术打交道,深知数据存储技术在整个IT系统中起着至关重要的作用,尤其是随着云计算时代的到来,所有企业都面临着海量的数据信息,如何处理这些数据成为当前研究的热点。在过去二十几年中,数据的存储是关系数据库的天下,它以高效、稳定、支持事务的优势几乎统治了整个行业的存储业务;但是随着互联网的发展,许多新兴产业如社交网络、微博、数据挖掘等业务快速增长,数据规模变得越来越庞大,高效存储、检索、分析这些海量的数据,关系数据库变得不再适用。前几年我们还可以看到网络上关于关系数据库与NoSQL数据库谁优谁劣的激烈讨论,如今NoSQL几乎占据了各大数据库论坛讨论的大部分版块。一些行业领头公司也逐渐将业务迁移到非关系数据库上,NoSQL类型的数据库也变得越来越成熟。当然,在未来一段时间里关系数据库如Oracle、DB、SQL Server等仍会在事务性要求比较高的行业(如银行、电信等)发挥它的作用。

另一方面,在信息技术领域,计算与存储一直是密不可分的,当前我们身处云计算的浪潮中,因此对应的各种云存储技术也呼之欲出。本书将介绍的NoSQL数据库MongoDB正是众多分布式海量数据存储技术中最出色的一种。MongoDB是一种面向文档的分布式数据库,可扩展,表结构自由,支持丰富的查询语句与数据类型,旨在为未来的大数据应用提供高性能的云存储解决方案。当然MongoDB幵不是万能的,随着了解的深入,我们也会发现它的缺点,这也是本书的宗旨,尽量让读者明白它的长处与短处,对于特定的业务选择最合适的数据库存储方案。最后我们希望本书介绍的MongoDB知识能为您在未来的项目中处理海量数据时提供帮助。

本书内容

本书尽量仍一个学习与实践者的角度,本着力求精简、突出精髓的原则,剖析了MongoDB在生产环境中使用需要知道的所有内容,全书分4部分,共13章,每章的内容简单介绍如下。

第1章 本章主要从什么是MongoDB以及几个核心迚程两方面概述了MongoDB,使读者整体上对MongoDB的体系结构有个认识。

第2章 本章主要介绍了MongoDB的查询语言系统,包含各种查询选择器以及查询选项,这是对任何一个数据库都有的内容。

第3章 本章主要介绍了MongoDB的索引与查询优化。

第4章 本章主要介绍了MongoDB的增、删、改语句。

第5章 本章主要仍底层存储视图与写操作流程剖析了MongoDB的Journaling日志功能。

第6章 本章主要介绍了MongoDB的聚集分析框架与MapReduce的编程模型。

第7章 本章主要介绍了复制集的功能与工作机制,包含数据同步、故障转移、写关注等,这些是MongoDB的核心。

第8章 本章主要介绍了分片集群,包含部署架构、分片、读写分离、片键选择等内容,这是MongoDB不同于传统关系数据库地方,也是实现海量数据分布式存储的关键。

第9章 本章主要介绍了分布式文件系统的GridFS文件,实现二迚制数据的存储。

第10章 本章主要介绍了对MongoDB的管理与监控,包括数据的导入导出、备份恢复以及运行状态的监控。

第11章 本章主要介绍权限控制,实现不同数据库对不同角色用户的权限分配。

第12章 本章主要仍应用开发角度,介绍了MongoDB的PHP驱动接口。

第13章 本章主要介绍了一个完整的电商平台,数据库使用的是MongoDB幵对前面所有章节的知识迚行总结,内容包含电商平台数据库表的设计、核心代码的编写、前台界面的原型图设计等,还介绍了开发Web应用程序常用的PHp框架Codeigniter和前端开发框架Bootstrap等。

本书特色

● 注重实践,本书为多年一线数据库存储,部署开发经验的总结。

● 注重效率,本书用最精简的篇幅直接阐明问题的本质,节省宝贵的阅读时间。

● 注重基础,本书用计算机领域相关的基础理论知识来解释某些难于理解的概念。

● 案例丰富,本书使用完整的例子与代码注释,使读者可以直接上手操作。

● 把握未来,大数据势不可挡,本书介绍的MongoDB特性与此息息相关。

读者对象

● 有海量数据存储需求的人员。

● 数据库管理与开发人员。

● 数据挖掘与分析人员。

● 各类基于数据库的应用程序开发人员。

谨以此书献给热爱技术、热爱MongoDB的朋友们!

第一部分 基础知识

这一部分主要介绍 MongoDB 方面的基础知识,熟悉关系数据库的读者能够快速地认识到 MongoDB 是什么以及与其他数据库的区别,这一部分的基础知识很重要,贯穿整本书,建议多实践和测试。

第1章 本章介绍了大数据、云计算的基本概念以及云存储与 MongoDB的关系,还介绍了 MongoDB 是什么、它的特点以及如何在各种平台上部署MongoDB等,最后介绍了MongoDB部署启动后一些关键的进程。

第2章 本章介绍了各种查询操作,这是数据库上最常用的一个操作。MongoDB的查询与关系数据库的语法区别很大,但它们很多设计思想是相同的,查询选择器相当于关系数据库中经常用到的where语句,查询选项相当于过滤出需要返回的字段。最后介绍了一种特殊对象的查询操作,这在关系数据库中是没有的。

第3章 本章介绍了查询用到的索引以及利用索引对查询的优化,这个思想和关系数据库也是一致的,利用索引来提高查询效率。

第4章 本章介绍了对 MongoDB 插入、删除、修改操作,至此一系列完整的增删改查的操作都介绍完了,对于一般的应用程序开发都能支持了。

第1章 大数据与云计算

1.1 什么是大数据

对于各种规模大小的组织机构而言,由于数据爆炸式的增长,传统的数据处理技术变得越来越难适应,需要有变革的技术来存储、分析这些大数据。谁能够掌握这些存储、分析技术,谁就有可能成为未来市场的主导者。财富500强公司在这个方面已走在前列,他们认识到大数据不仅仅是一门技术,而且是未来商业的发展趋势,并且已经开始从创新的大数据业务中受益。例如,企业能够分析用户的Web点击习惯,总结出用户喜好,进而有针对性地开展促销;政府部门能够利用大数据预测疾病的传播趋势,进而提前进行干预。

具体来说,大数据技术涉及到数据的创造、存储、获取和分析,数据的主要特点有以下几个。

数据量大。一个典型的PC机在2000年前后其存储空间可能有10GB,今天Facebook一天增加的数据量就将近有 500TB;一架波音 737 的飞机围绕美国飞行一周将会产生240TB 的数据;移动互联网的发展,智能手机的普及,人们每时每刻都在产生数以百万计的数据。

数据变化快。高速的股票交易市场,产生的数据以微秒计算;基础设施系统、实施系统每秒都产生大量的变化的日志,每秒都处理大量的并发。

数据多样性。大数据的类型不仅仅是简单的数字、日期和字符串,它可能包含地理数据、3D数据、音视频以及无结构的文档,而且这么多类型的数据可能需要保存在一起。

大数据技术的战略意义不仅在于掌握庞大的数据信息,而且也在于对这些含有意义的数据进行专业化处理。换言之,如果把大数据比作一种产业,那么这种产业实现盈利的关键在于提高对数据的“存储和加工能力”,通过“加工”实现数据的“增值”。大数据技术能够利用修改过的硬件取代原来高消耗和昂贵的老系统。由于许多大数据技术是开源的,它们实施起来更快且更便宜,例如,将它的数据存储技术迁移到MongoDB上来。

1.2 什么是云计算

云计算的定义有多种说法,对于到底什么是云计算,我们至少可以找到100种解释。目前广为接受的是美国国家标准与技术研究院定义:云计算是一种按使用量付费的模式,这种模式提供可用的、便捷的、按需的网络访问,进入可配置的计算资源共享池(资源包括网络、服务器、存储、应用软件、服务),这些资源能够被快速提供,只需投入很少的管理工作,或与服务供应商进行很少的交互,本质上就是虚拟化技术的延伸,以服务的形式提供客户。按照服务的形式,目前主要有如下3种形式的云计算。

1.IaaS:基础设施即服务

IaaS(Infrastructure-as-a-Service):基础设施即服务。消费者通过Internet可以从完善的计算机基础设施获得服务,例如硬件服务器租用。

2.SaaS:软件即服务

SaaS(Software-as-a- Service):软件即服务。它是一种通过Internet提供软件的模式,用户无需购买软件,而是向提供商租用基于Web的软件,来管理企业经营活动。例如:阳光云服务器。

3.PaaS:平台即服务

PaaS(Platform-as-a- Service):平台即服务。PaaS 实际上是指将软件研发的平台作为一种服务,以SaaS的模式提交给用户。因此,PaaS也是SaaS模式的一种应用。但是PaaS的出现可以加快SaaS的发展,尤其是加快SaaS应用的开发速度,例如软件的个性化定制开发。

1.3 大数据与云计算

从技术上看,大数据与云计算的关系就像一枚硬币的正反面一样密不可分。大数据必然无法用单台的计算机进行处理,必须采用分布式计算架构。它的特色在于对海量数据的挖掘,但它必须依托云计算的分布式处理,也就说大数据就像做饭用的一堆原材料,云计算就像做饭用的工具。云计算解决了大数据的运算工具问题,而对大数据的存储我们需要相应的云存储工具。云存储是在云计算概念上延伸和发展出来的一个新的概念,是指通过集群应用或分布式文件系统等功能,将网络中大量的存储设备通过应用软件集合起来协同工作,共同对外提供数据存储和业务访问功能的一个系统。所以云存储是一个以数据存储和管理为核心的云计算系统,本书介绍的MongoDB就可以当作一个云存储系统使用。

1.4 什么是MongoDB

MongoDB是一个可扩展、开源、表结构自由、用C++语言编写且面向文档的数据库,旨在为Web应用程序提供高性能、高可用性且易扩展的数据存储解决方案。

MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富、最像关系数据库的 NoSQL 数据库;它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,可以实现类似关系数据里单表查询的绝大部分功能,而且还支持对数据建立索引。

MongoDB不是在实验室里面凭空想象出来的产品,它是10gen公司的工程师根据实际的需求而设计的,主要基于以下几点考虑,需要一种新的数据库技术来满足数据存储层的水平扩展,而且要容易开发,能够存储海量的数据;一种非关系的结构是使数据库能支持水平扩展的最好方案;文档数据模型(BSON)容易编码和管理,将内部相关的数据放在一起能够提高数据库的操作性能。

MongoDB 服务端可运行在 Linux、Windows 或 OS X 平台,支持 32 位和 64 位应用,默认监听端口为 27017。MongoDB 的内存管理依赖于操作系统的自动内存管理机制,而且通过Map对数据文件进行内存映射,因此推荐MongoDB运行在64位平台上,否则在32位模式受虚拟内存地址大小的限制,而且运行时支持的最大文件尺寸也只能为2GB。当然对于测试和开发环境我们可以在 32 位模式下进行,生产环境上最好是部署在64位上。

MongoDB发展迅速,无疑是当前NoSQL领域的人气王,就算与传统的关系数据库比较也不甘落后,数据库知识网站DB-Engines根据搜索结果对223个数据库系统进行流行度排名,2014年7月的数据库流行度排行榜前12名如图1-1所示。

我们可以看到前三甲依然是Oracle、MySQL 和微软的SQL Server,值得关注的是,第五名MongoDB与第四名PostgreSQL之间的积分差距已不足1分。前四名由于历史原因都是关系数据库,许多大型的垄断行业仍然在使用这些关系数据库。图1-1 2014年7月DB-Engines上的数据库排行榜

MongoDB只通过6年时间就将公司市值发展到12亿美元,其成果相当于著名开源公司Red Hat 20 年的发展。MongoDB 的成功之路,一大部分归功于Web开发者。作为一个面向文档数据库,在许多场景下它都优于 RDBMS,同时还可以获得非常高的读写性能。此外,动态、灵活的模式更可以让用户在商用服务器上轻松地进行横向扩展。

1.5 大数据与MongoDB

大数据意味着新的机会,企业能够创造新的商业价值。MongoDB这样的数据库可以支撑很多大数据系统,它不仅可以作为一个实时的可操作的大数据存储系统,也能在离线大数据分析系统中使用。利用MongoDB作为大数据的云存储系统,企业能够在全世界范围内存储更多的数据,吸引更多的用户,挖掘更多用户的喜好,创造更多的价值。

选择正确的大数据存储技术,对使用者的应用和目标是非常重要的。MongoDB 公司提供的产品和服务能让使用者担更少的风险、花更少的精力提供更好的生产系统产品。事实上,MongoDB 天生就是为云计算而生的,其原生的可扩展架构,通过启用分片和水平扩展,能提供云存储所需的技术;此外,它的自动管理被称为“副本集”的冗余服务器,以保持数据的可用性和完整性。MongoDB 目前已经成为多家领先的云计算供应商,其中包括亚马逊网络服务、微软和SoftLayer等。

MongoDB还支持Google提出的MapReduce并行编程模式,为大数据的分析提供了强有力的保障。MongoDB同时提供了与Hadoop的接口,与其他第三方数据分析工具完美结合。

1.6 MongoDB特点

它的存储模型与关系数据库的比较如表1-1所示。表1-1 MongoDB存储模型与MySQL的对比

关系数据库中最基本的单元是行,而MongoDB中最基本存储单元是document,典型结构如下所示。

{

"_id" : ObjectId("51e0c391820fdb628ad4635a"),

"author" : { "name" : "Jordan","email" : "Jordan@123.com" },

"postcontent" : "jordan is the god of basketball",

"comments" : [

{"user" : "xiaoming", "text" : "great player"},

{ "user" : "xiaoliang", "text" : "nice action" }

]

}

它用与JSON格式类似的键值对来存储(在MongoDB中叫BSON对象),其中值的数据类型有常见的字符串、数字、日期,还可以是BSON对象、数组以及数组的元素,也可以是BSON对象,通过这种嵌套的方式,使MongoDB的数据类型变得相当丰富。

MongoDB 与传统关系数据库还有一个重大区别就是:可扩展的表结构。也就是说collection(表)中的document(一行记录)所拥有的字段(列)是可以变化的,下面文档对象document(一行记录)比上面列出的文档对象document(一行记录)多一个time字段,但它们可以共存在同一个collection(表)中。

{

"_id" : ObjectId("51e0c391820fdb628ad4635a"),

"author" : { "name" : "Jordan","email" : "Jordan@123.com" },

"postcontent" : "jordan is the god of basketball",

"comments" : [

{"user" : "xiaoming", "text" : "great player"},

{ "user" : "xiaoliang", "text" : "nice action" }

],

"time": "2013-07-13"

}

MongoDB查询语句不是按照SQL的标准来开发的,它围绕JSON这种特殊格式的文档型存储模型开发了一套自己的查询体系,这就是现在非常流行的 NoSQL 体系。关系数据库中常用的 SQL 语句在 MongoDB 中都有对应的解决方案。当然也有例外,MongoDB不支持JOIN语句。我们知道传统关系数据库中JOIN操作可能会产生笛卡尔积的虚拟表,消耗较多系统资源,而MongoDB的文档对象集合collection可以是任何结构,我们可以通过设计较好的数据模型尽量避开这样的操作需求。如果真的需要从多个collection(表)中检索数据,那我们可以通过多次查询得到。

在关系数据库中经常用到的 group by 等分组聚集函数,在 MongoDB 中也有,而且MongoDB提供了更加强大的MapReduce方案(GOOGLE提出的并行编程),为海量数据的统计、分析提供了便利。

MongoDB支持日志功能Journaling,对数据库的增、删、改操作会记录在日志文件中。MongoDB 每100ms将内存中的数据刷到磁盘上,如果意外停机,在数据库重新启动时, MongoDB能通过Journaling日志功能恢复。

MongoDB支持复制集(Replset),一个复制集在生产环境中最少需要3台独立的机器(测试的时候为了方便可能都部署在一台机器上),一台作主节点(primary),一台作次节点(secondary),一台 作仲裁节点(只负责选出主节点),备份、自动故障转移,这些特性都是复制集支持的。

MongoDB支持自动分片Sharding,分片的功能实现海量数据的分布式存储,分片通常与复制集配合起来使用,实现读写分离、负载均衡,当然如何选择片键是实现分片功能的关键。如何实现读写分离我们后面会详细分析。

总之,MongoDB 最吸引人的地方应该就是自由的表结构、MapReduce、分片、复制集,通过这些功能实现海量数据的存储、高效地读写以及数据的分析。

1.7 安装MongoDB

MongoDB 官方已经提供了Linux、Windows、Mac OS X 以及Solaris 4 种平台的二进制分发包,最新的稳定版本是 2.6.3,下载地址是:http://www.mongodb.org/downloads ,如图1-2所示。图1-2 各平台二进制分发包

下载完成后,解压,我们就能直接运行里面的二进制文件,这里所讨论的安装MongoDB,一般指的是运行MongoDB服务器端的进程mongod。

解压后,在bin目录下,我们可以看到一个名为mongod.exe的可执行程序,这个就是服务器端进程对应的程序。因为MongoDB启动时需要指定数据文件所在的目录,所以先要建立一个保存数据文件的目录,如 D:\mongodb-win32-i386-2.6.3\ test_single_instance\data;启动时也可以指定一个日志文件,如D:\ mongodb-win32-i386-2.6.3\test_single_instance\logs\ 123.log,我们通过以下命令就可以启动。

> mongod --config E:\MongoDB-win32-i386-2.6.3\test_single_instance\123.conf

上述步骤在Linux平台上也是一样的,只不过要注意目录和文件的读写权限。

还有一种安装方式就是直接通过各 Linux 分发版本对应的包管理器,如 RedHat、Debian、Ubuntu等都有自己的包管理器,通过包管理器安装时,系统会自动创建数据目录和日志文件,找到这些目录和文件所在的位置,后续分析问题可能会经常要读取日志文件。

1.8 几个重要的进程介绍

通过官网下载的二进制包中有几个重要的可执行文件,这些可执行文件运行后都会对应一个相应的进程。1.8.1 mongod进程

Mongod.exe为启动此数据库实例进程对应的可执行文件,是整个MongoDB中最核心的内容,负责数据库的创建、删除等各项管理工作,运行在服务器端为客户端提供监听,相当于MySQL数据库中的mysqld进程。

启动数据库实例会用到以下命令。

>mongod --config E:\MongoDB-win32-i386-2.6.3\test_single_instance\123.conf

配置文件123.conf内容如下所示。

dbpath = E:\MongoDB-win32-i386-2.6.3\test_single_instance\data

logpath = E:\MongoDB-win32-i386-2.6.3\test_single_instance\logs\123.log

journal = true

port = 50000

auth = true

dbpath为数据库文件存储路径;logpath为数据库实例启动、运行、错误日志文件;journal启动数据库实例的日志功能,数据库宕机后重启时依赖它恢复;port 数据库实例的服务监听端口;auth启动数据库实例的权限控制功能。其他可选参数可以通过mongod–help查看。1.8.2 mongo进程

mongo是一个与mongod 进程进行交互的JavaScript Shell进程,它提供了一些交互的接口函数用于系统管理员对数据库系统进行管理,如下面命令所示。

>mongo --port 50000–username xxx–password xxx–authenticationDatabase admin

mongo的参数port为mongod进程监听的端口,参数username为连接数据库的用户名,参数password为连接数据库的密码,参数authenticationDatabase为要连接的数据库。上述命令连接成功后,进程就会提供给用户一个JavaScript Shell环境,通过一些函数接口来管理数据库,其他参数可通过mongo--help选项查看。1.8.3 其他进程

1.mongodump 提供了一种从 mongod 实例上创建 BSON dump 文件的方法,mongorestore能够利用这些dump文件重建数据库,常用命令格式如下。

mongodump --port 50000 --db eshop --out e:\bak

参数--port表示mongod实例监听端口,--db表示数据库名称,--out表示备份文件保存目录,更多可选参数可通过mongodump–help查看。

2.mongoexport是一个将MongoDB数据库实例中的数据导出来生产JSON或CSV文件的工具,常用命令格式如下。

mongoexport --port 50000 --db eshop --collection goods --out e:\goods.json

3.mongoimport是一个将JSON或CSV文件内容导入到MongoDB实例中的工具,常用命令格式如下。

mongoimport --port 50000 --db eshop --collection goods --file e:\goods.json

4.mongos 是一个在分片中用到的进程。所有应用程序端的查询操作都会先由它分析,然后将查询定位到具体某一个分片上,它的作用与mongod类似,客户端的mongo与它连接。

5.mongofiles提供了一个操作MongoDB分布式文件存储系统的命令行接口,常用命令如下。

mongofiles--port 40009 --db mydocs --local D:\算法导论学习资料.pdf put algorithm_introduction.pdf

它表示将本地文件D:\算法导论学习资料.pdf上传到数据库mydoc中保存。

6.mongostat 提供了一个展示当前正在运行的 mongod 实例的状态工具,相当于UNIX/Linux上的文件系统工具vmstat,但是它提供的数据只与运行着的mongod或mongos的实例相关。

7.mongotop提供了一个分析MongoDB 实例花在读写数据上的时间的跟踪方法。它提供的统计数据在每一个collection(表)级别上。

1.9 适合哪些业务

当前各行各业都离不开数据的存储与检索需求,传统关系数据库发展了这么多年,在有些垄断性行业如电信、银行等仍然是首选,因为这些行业需要数据的高度一致性,只有支持事务的数据库才能满足它们的要求。但随着这几年互联网业务的发展,数据量越来越大,并发请求也越来越高,一个大系统中只用一种数据库并不能很好地满足全部业务的发展,同时以MongoDB为代表的NoSQL数据库快速发展,在某些方面展示了它们的优越性,逐渐被采用并取代了系统中的某些部件,总的来说以下几个方面比较适合使用 MongoDB这类的数据库。

1.Web应用程序

Web应用是一种基于BS模式的程序,业务的特点是读写请求都比较高,早期系统的数据量可能很少,但是发展到一定程度后数据量会暴增,这就需要数据存储架构能够适应业务的扩展。传统的关系数据库表结构都是固定的,增加一个业务或者横向扩展数据库都会带来巨大的工作量。MongoDB 支持无固定结构的表模型,因此很容易增加或减少表中的字段,适应业务的变化;同时MongoDB本身就支持分片集群,很容易实现水平扩展,将数据分散到集群中的各个片上,提高了系统的存储容量和读写吞吐量。Web应用程序还有一个特点就是“热数据”读并发很高,也就是说最新的数据被请求的次数会最多。为了提供读的性能,在传统的关系数据将中会采用其他的缓存技术来将这部分数据放在内存中,而MongoDB本身就支持这一点,它是通过内存映射数据文件来实现的。它会维护一个工作集,将最热的数据放在内存中,不需要其他技术的协助,这为系统开发提供了简便性,如图1-3所示。图1-3 Web应用中的MongoDB架构

2.缓存系统

这种使用场景是与关系数据库搭配使用,作为关系数据库的缓存前端。目前缓存技术有很多种,最常见的就是使用memcached,但是这些缓存系统都有个缺点,就是支持的数据类型有限,查询语句也有限,只能保存少量的数据且不能持久化。而MongoDB这些都能支持,因此可以作为缓存使用,如图1-4所示。图1-4 MongoDB作为关系数据库的缓存使用

3.日志分析系统

这类系统的特点是数据量大,允许部分数据丢失,不会影响整个系统的可靠性。以前将日志直接保存到操作系统的文件上,我们需要用其他工具打开日志文件或编写工具读日志进行分析,这样的话对于大量的日志查询会比较困难。如果用MongoDB数据库来保存这些日志,一来可以利用分片集群使日志系统的容量海量大,二来使用MongoDB特有的查询语句能够快速找到某条日志记录。最重要的是MongoDB支持聚集分析甚至 MapReduce的能力,为大数据的分析和决策提供了强有力的支持,如图1-5所示。图1-5 日志系统中的MongoDB应用

1.10 小结

MongoDB 是一个面向文档的数据库,不支持关系数据库中的 join 操作和事务。它用集合的概念代替了关系数据库中的表,用最小逻辑单元文档代替关系数据库中的行。它的集合结构是动态的,没有必要像关系数据库一样插入数据前先定义表结构,而且可以随时增加、修改、删除组成文档的字段。

MongoDB 支持当前所有主流编程语言的客户端驱动,使用方便,应用广泛,非常适合文档管理系统的应用、移动APP应用、游戏开发、电子商务应用、分析决策系统、归档和日志系统等应用。MongoDB支持所有主流平台的安装,但在32位的平台上部署时会有所限制,这是由它采用内存映射数据文件机制决定的,生产环境中最好部署在64位平台上。

第2章 查询语言系统

查询就是获取存储在数据库中的数据。在MongoDB中,查询通常针对一个集合来操作。查询可以指定查询条件,只返回匹配的文档;还可以指定投影项,只返回指定的字段,减少返回数据到客户端的网络流量。

为了进行测试,我们先假想一个常用的电子商务网站上可能用到的数据结构模型。在关系数据库MySQL中我们可能需要设计3个表如客户表customers、订单表orders、商品表products,其中 customers表中的主键为cust_id,products表中的主键为prod_id,orders表中主键order_id,外键cust_id和prod_id分别与客户和产品关联,这就是在关系数据库中经常干的事情,整个结构如图2-1所示。图2-1 数据库模型图

查询某个客户所订购的所有商品名称的SQL语句则为以下格式。

select t1.name,t3.prod_name from customer t1

join orders t2 on t1.cust_id = t2.cust_id

join products t3 on t2.prod_id = t3.prod_id;

在MongoDB中我们抛弃了这种关联的思路来设计表结构,正如10gen公司的工程师所说:“如果还用以前关系数据库的思路来建模,有时会适得其反”。在MongoDB中提倡的设计思路可能是建立一个客户表,在表中包含业务需要的尽可能多的数据信息,这样最终会产生一些冗余信息,但是在 NoSQL 的世界里是提倡这样做的。与上面的业务需求相同,则最终插入的文档对象document结构如下所示。

db.customers.insert

({

cust_id:123,

name:"xiaoming",

address:"china chasha ",

mobile:"999999",

orders:[

{order_id:1,

createTime:"2013-7-13",

products:[{prod_name:"surface Pro64G",prod_manufacture:"micosoft"},

{prod_name:"mini Apple",prod_manufacture:"Apple"}

]

},

{order_id:2,

createTime:"2013-7-12",

products:[{prod_name:"xbox",prod_manufacture:"micosoft"},

{prod_name:"iphone",prod_manufacture:"Apple"}]

}]

}

)

查询某个客户下的所订购的商品信息的查询语句则为如下格式。

db.customers.find({cust_id:123},{name:1,orders:1})

上面从一个简单的业务需求对比了关系数据库和MongoDB的不同,当然为了支持企业级的业务需求,MongoDB能做的绝不只是这些,下面我们将系统地介绍MongoDB的各种查询语句。

2.1 查询选择器

$lte表示的是小于或等于。我们先插入10条记录,便于测试。

for(var i =1; i<11; i++) db.customers.insert({id:i,name:"xiaoming",age:100+i})

最简单的查询语句为:db.customers.find(),按照插入的顺序返回前 20 个文档,如果记录总数比20大,则我们可以通过命令“it”获取更多文档。

> db.customers.find({id:9})

精确匹配选择器,返回包含键值对id:9的文档。

> db.customers.find({name:"xiaoming",age:101})

精确匹配选择器,但查询条件是要返回同时匹配键值对name:"xiaoming"且age:101的文档。

> db.customers.find({age:{$lt:102}})

$lt表示的是小于

> db.customers.find({age:{$lte:102}})

$lte表示的小于或等于

> db.customers.find({age:{$gt:119}})

$gt表示的是大于

> db.customers.find({age:{$gte:119}})

$gte表示的是大于或等于

> db.customers.find({age:{$lt:120,$gte:119}})

范围选择器,age:{$lt:120,$gte:119}表示的是小于120,大于或等于119

> db.customers.find({id:{$in:[1,2]}})

$in表示返回key的值在某些value范围内

> db.customers.find({id:{$nin:[1,2]}})

$nin表示返回key的值不在某些value范围内,$nin是一种比较低效的查询选择器,它会进行全表扫描,因此最好不要单独使用$nin

> db.customers.find({id:{$ne:1}})

$ne表示不等于。单独使用$ne,它也不会利用索引的优势,反而会进行全表扫描,我们最好与其他查询选择器配合使用。

> db.customers.find({$or:[{id:11},{age:119}]})

$or表示或运算的选择器,主要用于对两个不同key对应的文档进行连接。

> db.customers.find({$and:[{id:11},{age:111}]})

$and表示与运算的选择器,对于两个不同的key,要同时满足条件。

> db.customers.find({id:{$exists:false}})

$exists与关系数据库中的exists不一样,因为MongoDB的表结构不是固定的,有的时候需要返回包含有某个字段的所有记录或者不包含某个字段的所有记录,$exists这时就可以派上用场了,上面的语句就是返回不包含字段id的所有记录。当然为了实现这种需求,还有另外一种可替代的语句即db.customers.find({id:null})。

> db.customers.find({'detail.age':105})

这是一种嵌套查询的形式,detail字段的值也是一个BSON对象,文档结构如下。

{

"_id" : ObjectId("51e3dd1791fa05a27697ab4b"),

"id" : 1, "name" : "xiaohong", "

detail" : { "sex" : "male", "age" : 105 }

}

嵌套查询时匹配的key如果有多级嵌套深度,一级一级地用点号展开。

最后我们看一个比较复杂的查询来结束本话题,假设系统中有如下所示的这样一种文档类型。

{

"_id" : ObjectId("51e3e28e91fa05a27697ab55"),

"id" : 1, "name" : "xiaohong", "

detail" : [ { "sex" : "femle", "age" : 105 },

{ "address" : "china", "post" : 5}

]

}

查询post等于5的文档,则查询语句如下。

> db.customers.find({'detail.1.post':5})

匹配字符串中先取需要匹配的key(detail),由于detail键对应的value为数组,detail.1表示要取数组中第二个位置处的元素,又鉴于数组的元素也是个BSON文档对象,我们可以通过detail.1.post定位到需要匹配的键。

2.2 查询投射

上面介绍的查询选择器用来返回匹配的文档集,有时我们需要对返回的结果集作进一步的处理,如只需要返回指定的字段,此时查询投射项就可发挥它的功效了,下面我们逐一剖析。

> db.customers.find({'detail.1.post':5},{_id:0,id:1,name:1})

此条语句执行的效果就是按照条件'detail.1.post':5 来返回结果集,但是只选择 id 和name 两个字段的值进行显示,同时过滤掉默认生成的字段_id,如果不加上_ id:0 则会显示此默认的字段。总的来说第一个{}内为查询选择器;第二个{}内为对前面返回的结果集进行进一步过滤的条件,即投射项。

> db.customers.find({}).sort({id:-1})

对查询的结果集按照id的降序进行排序后返回。我们知道排序是很费时间的,对于排序有一点很重要,就是确保排序的字段上建立了索引,而且排序执行计划能够高效地利用索引。

> db.customers.find({}).skip(10).limit(5).sort({id:-1})

此条语句执行的过程是先对结果集进行排序,然后跳过 10 行,从这个位置开始返回接下来的5行。但是我们要注意如果传递给skip的参数很大,那么查询语句将会扫描大量的文档,这样执行性能将会很低下。因此我们在查询语句中尽量不要用skip,用其他办法替换skip想要实现的功能。

2.3 数组操作

我们来看下面这个查询语句。

db.customers.find({'detail.1.post':5},{_id:0,id:1,name:1})

投射选项{_id:0,id:1,name:1}针对的值是简单类型,包括查询选择器也是嵌套文档,可以通过操作符“.”一点点嵌套进去,如果值为数组类型并且数组的元素又是文档类型,查询语句将有所变化。下面我们展开分析。

假如有一个类似下面结构的数据。

{

"_id" : 4,

"AttributeName" : "material",

"AttributeValue" : ["牛仔", "织锦", "雪纺", "蕾丝"],

"IsOptional" : 1

}

{

"_id" : 5,

"AttributeName" : "version",

"AttributeValue" : ["收腰型", "修身型", "直筒型", "宽松型", "其他"],

"IsOptional" : 1

}

1.精确匹配数组值

我们可以通过简单的精确匹配得到某条记录,如以下语句所示。

> db.DictGoodsAttribute.find({"AttributeValue": ["收腰型", "修身型", "直筒型",

"宽松型", "其他"]})

返回值如下。

{ "_id" : 5, "AttributeName" : "version", "AttributeValue" : [ "收腰型", "修身型", "直筒型", "宽松型", "其他" ], "IsOptional" : 1 }

2.匹配数组中的一个元素值

假如数组有多个元素,只要这些元素中包含有这个值,就会返回这条文档,如下面语句所示。

> db.DictGoodsAttribute.find({"AttributeValue" :"收腰型"})

假如此时集合中有如下两条记录:{"_id": 5, "AttributeName":"version", "AttributeValue":[ "收腰型", "修身型" ]}和{"_id": 5, "AttributeName":"version", "AttributeValue": ["修身型", "收腰型", "直筒型"]},则返回值中这两条记录都会存在。

3.匹配指定位置的元素值

如下面语句所示。

> db.DictGoodsAttribute.find({"AttributeValue.0" :"收腰型"})

它表示数组中第0个位置的元素值为"收腰型"的记录才返回。上面查询结果只返回如下记录:{"_id": 5, "AttributeName":"version", "Attribute Value": [ "收腰型", "修身型" ]}。

下面我们再看一个更复杂的数据模型,数组的元素不是简单的值类型,而是一个文档,如下记录。

{

"_id" : 1,

"StatusInfo" : [

{

"status" : 9,

"desc" : "已取消"

},

{

"status" : 2,

"desc" : "已付款"

}]

}

字段"StatusInfo"是一个嵌套文档的数组。

4.指定数组索引并匹配嵌套文档中的字段值

如下面语句所示。

>db.Order.find({"StatusInfo.0.status":2})

返回数组中索引0处且嵌套文档中status值为2的所有文档。如果不指定索引值,效果与第2种情况中介绍的一样,只要数组中包含有status值为2的文档,都会被返回。这里与第1、2、3种情况有点不一样的地方是数组的元素值是文档,而不是简单的值,所以要指定键名。

上面对数组操作的语句会返回所有字段,我们可以通过投影只返回指定的字段值,如下面语句所示。

>db.Order.find({"_id" : 2},{_id:0,StatusInfo:1})

返回值如下。

{

"StatusInfo" : [

{

"status" : 9,

"desc" : "已取消"

},

{

"status" : 2,

"desc" : "已付款"

}]

}

返回的结果中数组包含的信息仍然较多,需求可能只需要返回状态的描述"desc"即可,我们可以通过下面语句实现。

>db.Order.find({"StatusInfo.status":2},{_id:0,"StatusInfo.desc":1})

加上了一个键名来过滤,返回值如以下所示。

{

"StatusInfo" : [{ "desc" : "已付款" }, { "desc" : "已发货" } ]

}

现实的需求可能更挑剔,需要返回当前订单的最新状态(数组中最后一个元素),此时需要用专门针对数组投射的操作符$slice来完成了,如下语句所示。

>db.Order.find({"_id":2},{_id:0,"StatusInfo":{"$slice":-1},"StatusInfo.desc":1}

返回值如下所示。

{

"StatusInfo" : [ { "desc" : "已发货" } ]

}

流程如图2-2所示。图2-2 执行流程

关系数据库中常用的查询语句,在MongoDB中一般都有类似的实现。同所有其他数据库一样,索引选择的数据结构是一样的,查询语句的性能问题应该引起足够的重视,第3章我们将分析索引与查询优化方面的问题。

2.4 小结

MongoDB不支持关系数据库中标准的SQL查询,它有一套自己的查询语言,基本上能实现关系数据库中那样的查询要求;查询选择器就相当于关系数据库中where语句后面的内容,查询投射项相当于关系数据库中select语句后面需要返回的字段。MongoDB的字段数据类型可以嵌套,可以为数组等多种复杂的结构模型,用于弥补没有join操作的不足,因此针对字段值类型为数组的查询,它提供了一些特殊的查询方式。

第3章 索引与查询优化

索引是个与数据存储和查询相关的古老话题,目的只有一个:“提高数据获取的性能”。我们知道一本书的前面几页肯定会有一个目录,这个目录式的索引能使我们快速查询想看的内容;把目光转移到计算机上,索引则变得抽象,有时候不好理解。索引保存在哪里,是个什么样的数据结构,计算机领域的索引无外乎也是这两个主题。磁盘上保存有大量的文件,文件系统对这些文件进行管理。文件系统将磁盘抽象为4个部分,依次如下所示。

这当中索引节点表保存了所有文件或目录对应的inode节点(Linux文件系统),通过文件名或目录找到对应的inode节点,通过inode节点定位到文件数据在文件系统中的逻辑块号,最后根据磁盘驱动程序将逻辑块号映射到磁盘上具体的块号。回到数据库方面,数据库保存记录的机制是建立在文件系统上的,索引也是以文件的形式存储在磁盘上,在数据库中用到的最多的索引结构就是B树。尽管索引在数据库领域是不可缺少的,但是对一个表建立过多的索引也会带来一些问题,索引的建立要花费系统时间,同时索引文件也会占用磁盘空间。如果并发写入的量很大,每个插入的文档都要建立索引,可想而知,性能会较低。因此合理地建立索引是关键,搞清楚哪些字段上需要建立索引、索引以什么样的方式建立,我们需要对每个查询过程进行分析,才能得出合理的结论。

3.1 索引

在MongoDB上,索引能够提高读操作及查询性能。没有索引,MongoDB必须扫描集合中的每一个文档,然后选择与查询条件匹配的文档,这种全表扫描的方式是非常低效的。MongoDB索引的数据结构也是B+树,它能存储一小部分集合的数据,具体来说就是存储集合中建有索引的一个或多个字段的值,而且按照值的升序或降序排列。对于一个查询来说,如果存在合适的索引,MongoDB 能够利用这个索引减少文档的扫描数量,如图 3-1所示查询商品价格低于 50 的所有商品;甚至对于某些查询能够直接从索引中返回结果,不需要再去扫描数据集合,这种查询是非常高效的,如图3-2所示。图3-1 利用了索引的查询图3-2 利用索引的查询只返回索引字段并从索引文件返回数据3.1.1 单字段索引

MongoDB默认为所有集合都创建了一个_id字段的单字段索引,而且这个索引是唯一的,不能被删除,_id 字段作为一个集合的主键,值是唯一的,对于一个集合来说,也可以在其他字段上创建单字段的唯一索引,如下面所述。

为了测试展开,我们先按照下面的语句插入如下一些数据。

>for(vari=1;i<10;i++)db.customers.insert({name:"jordan"+i,country:"American"})

> for(var i = 1;i < 10;i++) db.customers.insert({name:"gaga"+i,country:"American"})

> for(var i = 1;i < 10;i++) db.customers.insert({name:"ham"+i,country:"UK"})

> for(var i = 1;i < 10;i++) db.customers.insert({name:"brown"+i,country:"UK"})

> for(var i = 1;i < 10;i++) db.customers.insert({name:"ramda"+i,country:"Malaysia"})

建立单字段唯一索引或者去掉{unique:true}选项就是一个普通的单字段索引。

> db.customers.ensureIndex({name:1},{unique:true})

索引结构如图3-3所示。图3-3 单字段唯一索引

唯一索引成功创建后,会在相应数据库的系统集合 system.indexes 中增加一条索引记录,如下所示。

> db.system.indexes.find()

{"v": 1, "key":{"_id": 1 }, "ns":"eshop.customers", "name":"_id_"}

{"v": 1, "key":{"name": 1 }, "unique": true, "ns":"eshop.customers","name" : "name_1" }

我们可以看到有两条记录,第一条为系统默认创建在生成的键_id 上;第二条为以上创建的唯一索引。索引记录中v表示索引的版本;key表示索引建立在哪个字段上;1表示索引按照升序排列;索引记录所在的命名空间,name 表示唯一的索引名称。唯一索引与普通索引的区别是要求插入的所有记录在创建索引的键值上唯一。

执行查询,一个用索引字段作为查询选择器;一个不用索引字段作为查询选择器进行比较。

> db.customers.find({name:"ramda9"}) .explain()

{

"cursor" : "BtreeCursor name

"isMultiKey" : false,

"n" : 1,

"nscannedObjects" : 1,

"nscanned" : 1,

"nscannedObjectsAllPlans" :1

"nscannedAllPlans" : 1,

"scanAndOrder" : false,

"indexOnly" : false,

"nYields" : 0,

"nChunkSkips" : 0,

"millis" : 7,

"indexBounds" : {

"name" : [

[

"ramda9"

"ramda9"

]

]

},

"server" : "GUO:50000"

}

以上查询语句执行返回的结果中"cursor":"BtreeCursor name表示此查询用到了索引;isMultiKey 表示查询是否用到了多键复合索引;n 反映了查询选择器匹配的文档数量;nscannedObjects表示查询过程中扫描的总的文档数;nscanned表示在数据库操作中扫描的文档或索引条目的总数量;nscannedObjectsAllPlans 表示反映扫描文档总数在所有查询计划中;nscannedAllPlans 表示在所有查询计划中扫描的文档或索引条目的总数量;scanAndOrder表示MongoDB从游标取回数据时,是否对数据排序;nYields表示产生的读锁数;millis表示查询所需的时间,单位是毫秒。

> db.customers.find({country:"Malaysia"}).explain()

{

"cursor" : "BasicCursor",

"isMultiKey" : false,

"n" : 9,

"nscannedObjects" : 45,

"nscanned" : 45,

"nscannedObjectsAllPlans" : 45,

"nscannedAllPlans" : 45,

"scanAndOrder" : false,

"indexOnly" : false,

"nYields" : 0,

"nChunkSkips" : 0,

"millis" : 0,

"indexBounds" : {

},

"server" : "GUO:50000"

}

这是一个没用到索引的查询,匹配的文档数为9,但是扫描的总文档数为45,进行了全表扫描。3.1.2 复合索引

MongoDB支持多个字段的复合索引,复合索引支持匹配多个字段的查询。

在customers表中再插入一些测试数据。

>for(vari=1;i<10;i++)db.customers.insert({name:"lanbo"+i,country:"Malaysia"})

查询语句如下。

> db.customers.find({country:"Malaysia"}).explain()

该查询会扫描54个文档,全表扫描,匹配上的文档只有18个,没有用到索引。

接着我们在 country 字段上建立一个索引,db.customers.ensureIndex({country:1}),重新执行以下查询语句。

>db.customers.find({country:"Malaysia"}).explain()

该查询则会扫描 18 个文档,同时匹配 18 个文档,查询用到了刚才创建的索引"BtreeCursor country_1"。

到此数据库已有的索引如下所示。

> db.system.indexes.find()

{"v": 1, "key":{"_id": 1 }, "ns":"eshop.customers", "name":"_id_"}

{"v": 1, "key":{"name": 1 }, "unique": true, "ns":"eshop.customers","name" : "name_1" }

{"v": 1, "key":{"country": 1 }, "ns":"eshop.customers", "name":"country_1" }

最后创建一个复合索引,索引结构如图3-4所示。

> db.customers.ensureIndex({name:1,country:1})

执行如下查询语句。

> db.customers.find({name:"lanbo2",country:"Malaysia"}).explain()

扫描的文档数为 1;匹配的文档数也为 1 ,查询用到了复合索引"BtreeCursor name_1_country_1"。图3-4 一个复合索引3.1.3 数组的多键索引

如果对一个值为数组类型的字段创建索引,则会默认对数组中的每一个元素创建索引,我们来看下面结构的文档集合。

{

"_id" : 1,

"AttributeName" : "price",

"AttributeValue" : ["0-99", "100-299", "300-499", "500-899", "900-1499", "1500 以上"],

"IsOptional" : 1

}

字段AttributeValue值为数组类型,在其上面创建如下一个索引。

> db.DictGoodsAttribute.ensureIndex({AttributeValue:1})

创建成功后,字段会添加一个如下索引条目,结果如图3-5所示:

{

"v" : 1, "key" : { "AttributeValue" : 1 },

"name" : "AttributeValue_1", "ns" : "haoyf.DictGoodsAttribute"

}图3-5 基本数组索引

如果数组的元素值为一个嵌套的文档,如下面文档结构所示。

{

"_id" : 1,

"StatusInfo" : [{

"status" : 9,

"desc" : "已取消"

}]

}

我们可以创建一个{"StatusInfo.desc": 1 }的多键索引,如下命令所示。

>db.Order.ensureIndex({"StatusInfo.desc":1})

索引结构如图3-6所示。图3-6 数组多键索引

查询订单状态为"已取消"的执行计划如下。

> db.Order.find({"StatusInfo.desc":"已取消"}.explain()

"cursor":"BtreeCursor StatusInfo.desc","nscannedObjects": 1, "nscanned": 1,说明利用了刚才创建的索引。3.1.4 索引管理

通过上面创建的索引我们可以看到,索引记录都保存在特殊的集合system.indexes中。此文档集合的表结构在上面我们已经有过描述。创建索引的语法也很简单,如下所示。

>db.collection.ensureIndex(keys, options)

keys 是一个document文档,包含需要添加索引的字段和索引的排序方向;option 是可选参数,控制索引的创建方式。

索引的删除并不是直接找到索引所在的集合system.indexes,通过在集合上执行remove命令来删除,而是通过执行集合上的命令dropIndex来删除的。如删除上面创建的如下复合索引。

> db.customers.dropIndex("name_1_country_1")

其中参数为索引的名称。

3.2 查询优化

查询优化的目的就是找出慢的查询语句,分析慢的原因,然后优化此查询语句。

MongoDB 对于超过100ms 的查询语句,会自动地输出到日志文件里面,因此找出慢查询的第一步是查看MongoDB的日志文件,如果觉得这100ms阈值过大,我们可以通过mongod的服务启动选项slowms来设置,它的默认值是100ms。

用上面的方法找出慢查询可能比较粗糙,第二种定位慢查询的方法是打开数据库的监视功能,它默认是关闭的,我们可以通过下面的命令打开。

db.setProfilingLevel(level,[ slowms] )

参数level是监视级别,值为0表示关闭数据库的监视功能,为1表示只记录慢查询,为2表示记录所有的操作;slowms为可选参数,设定慢查询的阈值。

所有监视的结果都将保存到一个特殊的集合system.profile中。

通过上面的两种方法我们可以找出慢查询的语句,然后通过建立相应的索引基本可以解决绝大部分的问题。但是我们有时需要更加精细的优化代码,这就需要分析这些慢查询的执行计划,查看查询是否用到索引,是否与我们想要的执行计划相同,用MongoDB的explain命令可以查看执行计划,正如3.1.1小节所做的那样。

3.3 小结

MongoDB可以在一个集合上建立一个或多个索引,而且必须为在字段_id建立一个索引,建索引的目的与关系数据库一样,就是为了提高对数据库的查询效率;一旦索引创建好,MongoDB 会自动地根据数据的变化维护索引,如果索引太大而不能全部保存在内存中,将被移到磁盘文件上,这样会影响查询性能,因此要时刻监控索引的大小,保证合适的索引在内存中;监控一个查询是否用到索引,可以在查询语句后用 explain 命令。并不是所有的字段都要建立索引,我们应该根据自己业务所涉及的查询,建立合适的索引。

如果系统有大量的写操作,由于需要维护索引的变化,会导致系统性能降低。我们在对大数据建立索引时最好在后台进行,否则会导致数据库停止响应。要注意虽然我们在某些字段上建了索引,但是查询时可能用不上索引,如使用$ne和$nin表达式等。

第4章 增改删操作

4.1 插入语句

与其他关系数据库类似,增加记录可以使用insert语句来完成,下面来看一个例子。

> db.customer.insert({name:"gyw",mobile:"12345678901",email:"xxx@163.com"})

查询看是否插入成功,如下所示。

> db.customer.find()

{

"_id": ObjectId("528c67f5c95c154966513547"), "name":"gyw", "mobile":"1234

5678901", "email" : xxx@163.com

}

这里有几点需要说明。

1.第一次插入数据时,不需要预先创建一个集合(customer),插入数据时会自动创建。

2.每次插入数据时如果没有显示的指定字段"_id",则会默认创建一个主键"_id"。在关系数据库中主键大多数是数值类型,且是自动增长的序列。而MongoDB中的主键值类型则为ObjectId类型,这样设计的好处是能更好的支持分布式存储。其中ObjectId类型的值由12个字节组成,前面4个字节表示的是一个时间截,精确到秒,紧接着的3个字节表示的是机器唯一标示,接着2个字节表示的进程id,最后 3个字节是一个随机的计数器。

3.在MongoDB中,每一个集合都必须有一个"_id"字段,不管是自动生成的还是指定的,值都必须唯一,如果插入重复值将会抛出异常。下面是一个指定"_id"插入的例子。

> db.customer.insert({"_id":1,name:"wjb",mobile:"12345678901",email:"xxx@123.com"})

如果再次插入"_id":1的记录,则会抛出如下错误。

E11000 duplicate key error index: eshop.customer.$_id_ dup key:{: 1.0 }

很明显提示主键值重复了。

4.2 修改语句

与关系数据库类似,修改也是由update来完成的,只是monogDB中的update有一些可选参数。总体里说修改分为两种,一种是只针对具体的目标字段,其他不变;另一种是取代性的更改,即修改具体目标字段后,其他的字段会被删除。update语法格式如下。

db.collection.update(query, update, <upsert>, <multi>)

query参数是一个查询选择器,值类型为document。

update参数为需要修改的地方,值类型为document,如果update参数只包含字段选项,没有操作符,则会发生取代性的更改。

upsert为一个可选参数,boolen类型,默认值为false。当值为true时,update方法将更新匹配到的记录,如果找不到匹配的文档,则将插入一个新的文档到集合中。

multi为一个可选参数,boolean类型,表示是否更新匹配到的多个文档,默认值为false,此时update方法只会更新匹配到的第一个文档;当为true时,update方法将更新所有匹配到的文档。

下面通过一些例子来使用update语句。

1.更改指定的字段值。

> db.goods.update({name:"apple"},{$set:{name:"apple5s"},$inc:{price:4000}})

这个操作将更改集合中与 name:"apple"匹配的第一个文档,将其中的字段 name 设为"apple5s",字段price增加4000,其他字段保持不变。

2.更改指定字段而其他字段被清除掉。

> db.goods.update({name:"htc"},{name:"htc one"})

这个操作将更改集合中与name:"htc"匹配的第一个文档,将其中的字段name设为"htc one",文档中除了主键_id字段外,其他字段都被清除。

3.更改多个文档中的指定字段。

> db.goods.update({name:"surface"},{$set:{price:6999}},{multi:true})

由于利用了可选参数multi,这个操作将更改集合中与name:" surface "匹配的所有文档,将其中的字段price设为6999,其他字段不变。

4.update找不到匹配的文档时则插入新文档。

>db.goods.update({name:"iphone"},{$set:{price:5999}},{upsert:true})

因为利用了可选参数upsert,这个操作如果找不到匹配的文档,就会插入一个新的文档。

4.3 删除语句

MongoDB中的删除操作remove也是数据库CRUD操作中最基本的一种,与关系数据库中的delete类似,remove方法的格式如下。

>db.collection.remove( <query>, <justOne> )

参数query为可选参数,查询选择器,类似关系数据库中where条件语句。

justOne参数也是可选参数,是一个boolean类型的值,表示是否只删除匹配的第一个文档,相当于关系数据库中的limit 1 条件。

有一点要注意:如果 remove 没有指定任何参数,它将删除集合中的所有文档,但是不会删除集合对应的索引数据。如果想删除集合中的所有文档,同时也删除集合的索引,我们可以使用MongoDB针对集合提供的drop方法。下面看几个例子。

1.删除匹配的所有文档。

> db.goods.remove({name:"htc "})

2.删除匹配的第一个文档。

> db.goods.remove({name:"huawei"},1)

3.删除所有文档,但不会删除索引。

> db.goods.remove()

当利用remove删除一个文档后,文档对象也会从磁盘上相应的数据文件中删去。

4.4 锁机制

结合第2章的读操作,我们有必要介绍一下MongoDB中的锁机制。与关系数据一样,MongoDB也是通过锁机制来保证数据的完整性和一致性,MongoDB利用读写锁来支持并发操作,读锁可以共享,写锁具有排它性。当一个读锁存在时,其他读操作也可以用这个读锁;但是当一个写锁存在时,其他任何读写操作都不能共享这把锁,当一个读和写都等待一个锁时,MongoDB将优先分配锁给写操作。

从版本2.2开始,MongoDB在每一个数据库上实现锁的粒度,当然对于某些极少数的操作,在实例上的全局锁仍然存在,锁粒度的降低能够提高系统的并发性。成熟的关系数据库锁的粒度更低,它可以在表中的某一行上,即“行级锁”。常见的操作和产生的锁类型如下:查询产生读锁、增删改产生写锁、默认情况下在前台创建索引会产生写锁、聚集aggregate操作产生读锁等。

4.5 小结

本章介绍了MongoDB数据库中最常用的3种操作,即插入操作,修改操作和删除操作,对每一种操作给出了详尽的实例参考,类似于关系数据库中的SQL语句编写。本章最后也介绍了MongoDB的锁机制,包括锁的粒度和各种数据库操作产生的不同锁类型。

第二部分 深入理解MongoDB

这一部分介绍的内容对深入理解 MongoDB 很有帮助,如果能完全掌握这部分内容,对整个 MongoDB 数据库的设计有很大帮助,同时对于日常运维中的监控以及遇到各种疑难问题都能快速地定位问题,找到方法解决。

第5章 本章介绍了 Journaling 日志功能,对 MongoDB 的读写操作所发生的所有动作都将透彻分析。这个日志相当于关系数据库中的事物日志或redo日志,保证了数据的完整性和一致性。

第6章 本章将介绍 3 种聚集分析,针对不同的数据处理需求灵活选择对应的分析方法,这部分的内容对大数据分析有借鉴意义。

第7章 本章全面介绍了 MongoDB 的复制集功能,它能实现数据库的故障自动转移、数据的同步备份,保证了系统的可靠性。

第8章 本章介绍了 MongoDB 的集群功能,它能实现海量数据的分布式存储,能够提高系统的吞吐量。

第9章 本章介绍了 MongoDB 所特有的分布式文件存储功能,我们利用它的二进制数据类型可以构造一个分布式文件系统。

第5章 Journaling日志功能

MongoDB 的 Journaling 日志功能与常见的 log 日志是不一样的。MongoDB 也有 log日志,它只是简单记录了数据库在服务器上的启动信息,慢查询记录,数据库异常信息,客户端与数据库服务器连接、断开等信息。Journaling 日志功能则是 MongoDB 里面非常重要的一个功能,它保证了数据库服务器在意外断电、自然灾害等情况下数据的完整性。尽管 MongoDB 还提供了其他的复制集等备份措施(后面会分析),但Journaling 的功能在生产环境中是不可缺少的,它依靠了较小的 CPU 和内存消耗,带来的是数据库的持久性和稳定性。本篇章将分析 Journaling 涉及的功能细节问题和Journaling的工作流程。

5.1 两个重要的存储视图

Journaling 功能用到了两个重要的内存视图:private view和shared view。这两个内存视图都是通过MMAP(内存映射)来实现的,其中对private view的映射的内存修改不会影响到磁盘上;shared view 中数据的变化会影响到磁盘上的文件,系统会周期性地刷新shared view 中的数据到磁盘。

shared view 在MongoDB 启动的过程中,操作系统会将磁盘上的数据文件映射到内存中的 shared view。操作系统只是完成映射,并没有立即加载数据到内存,MongoDB 会根据需要加载数据到shared view。

private view 内存视图是为读操作保存数据的位置,是MongoDB 保存新的写操作的第一个地方。

磁盘上的Journaling日志文件是实现写操作持久化保存的地方,MongoDB实例启动时会读这个文件。

5.2 Journaling工作原理

当 mongod 进程启动后,首先将数据文件映射到 shared 视图中,假如数据文件的大小为 4000 个字节,它会将此大小的数据文件映射到内存中,地址可能为 1000000~1004000。如果直接读取地址为1000060的内存,我们将得到数据文件中第60个字节处的内容。有一点要注意,这里只是完成了数据文件的内存映射,并不是将全部文件加载到内存中,只有读取到某个地址时才会将相应的文件内容加载到内存中,相当于按需加载,如图5-1所示。图5-1 mongod启动时的内存映射

当写操作或修改操作发生时,进程首先会修改内存中的数据,此时磁盘上的文件数据就与内存中的数据不一致了。如果mongod启动时没有打开Journaling功能,操作系统将每 60 秒刷新 shared 视图对应的内存中变化的数据并将它写到磁盘上。如果打开了Journaling日志功能,mongod将额外产生一个private视图,MongoDB会将private视图与shared视图同步,如图5-2所示。

当写操作发生时,MongoDB首先将数据写到内存中的private视图处,注意private视图并没有直接与磁盘上的文件连接,因此此时操作系统不会将变化刷新到磁盘上,如图5-3所示。

然后MongoDB将写操作批量复制到journal,journal会将写操作存储到磁盘上的文件上,使其持久化保存,journal日志文件上的每一个条目都描述了写操作更改了数据文件上的哪些字节,如图5-4所示。图5-2 shared视图与private视图保持同步图5-3 MongoDB写数据到private视图图5-4 将数据文件的变化写到journal日志文件中

由于数据文件的变化(如在哪个位置数据变成了什么)被持久化到了 journal 日志文件中,即使此时MongoDB服务器崩溃了,写操作也是安全的。因为当数据库重新启动时,会先读 journal 日志文件,将写操作引起的变化重新同步到数据文件中去。我们通过下面的启动日志也可以看到这个动作,日志截图如图5-5所示。

当上面的步骤完成后,接下来MongoDB会利用journal日志中的写操作记录引起的数据文件变化来更新shared视图中的数据,如图5-6所示。

当所有的变化操作都更新到 shared视图中后,MongoDB将重新利用 shared视图来映射private视图,防止private视图变得“太脏”,使其占用的内存空间恢复到初始值,约为0。此时shared视图内存中的数据与磁盘上的数据变得不一致。按照默认值60秒,MongoDB会周期性地要求操作系统将shared视图中变化的数据刷新到磁盘上,使磁盘上的数据与内存中的数据保持一致,如图5-7所示。图5-5 mongod启动时利用journal日志进行恢复处理图5-6 刷新shared视图图5-7 重新同步private视图并flush到磁盘

当执行完刷新内存中变化的数据到磁盘后,MongoDB会删除掉journal中这个时间点后面的所有写操作,这一点与关系数据库中的checkpoint类似。

MongoDB的Journaling日志功能,在2.0版本后是默认启动的,可以在实例mongod启动时通过启动选项控制;上面提到的步骤中,有一个地方是将写操作周期性批量写到journal日志文件中,这个周期的大小是通过可选启动参数journalCommitInterval来控制的,默认值是100ms。MongoDB经过60s的周期刷新内存中变化的数据到磁盘,这个值是通过启动可选参数 syncdelay 来控制的。这些默认值一般适用于大多数情况,不要轻易更改。通过上面的分析,数据库服务器仍然有100ms的丢失数据的风险,因为Journaling日志写到磁盘上的周期是100ms,假如刚好一批写操作还在内存中,还没来得及刷新到Journaling在磁盘上对应的文件上,服务器突然故障,这些在内存中的写操作就会丢失。

MongoDB在启动时,专门初始化一个线程不断循环,用于在一定时间周期内将从defer队列中获取要持久化的数据写入到磁盘的 journal(日志)和 mongofile(数据)处。当然因为它不是在用户添加记录时就写到磁盘上,所以从MongoDB开发来说,它不会造成性能上的损耗,因为看过代码发现,当进行CUD操作时,记录(Record类型)都被放入到defer 队列中以供延时批量(group commit)提交写入。

5.3 小结

Journaling 是 MongoDB 中非常重要的一项功能,类似于关系数据库中的事务日志。Journaling能够使数据库由于其他意外原因故障后快速恢复。从MongoDB版本2.0以后,启动mongd实例时这项功能就自动打开了,数据库实例每次启动时都会检查磁盘上journal文件看是否需要恢复。尽管Journaling对写操作会有一些性能方面的影响,但对读操作没有任何影响,在生产环境中开启它是很有必要的。MongoDB 的复制集功能通过冗余数据保护数据的安全,但Journaling更是通过日志的方式保证数据库的一致性,两者的关注点不一样。

第6章 聚集分析

聚集操作是对数据进行分析的有效手段。MongoDB 主要提供了三种对数据进行分析计算的方式:管道模式聚集分析、MapReduce聚集分析、简单函数和命令的聚集分析。

6.1 管道模式进行聚集

这里所说的管道类似于UNIX上的管道命令。数据通过一个多步骤的管道,每个步骤都会对数据进行处理,最后返回需要的结果集。管道提供了高效的数据分析流程,是MongoDB中首选的数据分析方法。一个典型的管道操作流程如图6-1所示。图6-1 管道聚集操作流程图

图6-1对应的操作语句如下。

db.books.aggregate(

[

{

$match: { status: "normal"}

},

{

$group: {_id: "$book_id", total:{ $sum: "$num"}}

}

]

)

数据依次通过数组中的各管道操作符进行处理,常用的管道操作符有以下几个。

$match:过滤文档,只传递匹配的文档到管道中的下一个步骤。

$limit:限制管道中文档的数量。

$skip:跳过指定数量的文档,返回剩下的文档。

$sort:对所有输入的文档进行排序。

$group:对所有文档进行分组然后计算聚集结果。

$out:将管道中的文档输出到一个具体的集合中,这个必须是管道操作中的最后一步。

与$group操作一起使用的计算聚集值的操作符有以下几个。

$first:返回group操作后的第一值。

$first:返回group操作后的最后一个值。

$max:返回group操作后的最大值。

$min:返回group操作后的最小值。

$avg:返回group操作后的平均值。

$sum:返回group操作后所有值的和。

常用的关系数据库中的SQL语句与MongoDB聚集操作语句比较如表6-1所示。表6-1 常用SQL语句与MongoDB语句对比续表

6.2 MapReduce模式聚集

MongoDB也提供了当前流行的MapReduce的并行编程模型,为海量数据的查询分析提供了一种更加高效的方法,用MongoDB做分布式存储,然后再用MapReduce来做分析。典型MapReduce流程如图6-2所示。图6-2 MapReduce操作流程图

典型代码如下所示。

db.books.mapReduce (

function()

{

emit ( this.boo_id,this.num) ;

},

function(key, values)

{

return Array.sum( values )

},

{

query: { status: "normal" },

outresult: "books_totals"

}

)

上面的需求实际上是要统计出每种类型的书可以销售的总数。

在传统关系数据库上SQL语句如下所示。

select sum(num) as value, book_id as _id

from books where status = "normal"group by book_id;

接下来我们看看MapReduce方式如何解决这种问题,首先定义了一个map函数,如下所示。

function()

{

emit ( this.boo_id,this.num) ;

}

接着定义reduce函数,如下所示。

function(key, values)

{

return Array.sum( values )

}

最后在集合上执行MapReduce函数,如下所示。

>db.books.mapReduce (

function()

{

emit ( this.boo_id,this.num) ;

},

function(key, values)

{

return Array.sum( values )

},

{

query: { status: "normal" },

outresult: "books_totals"

}

)

这里有一个查询过滤条件query:{status:"normal"},返回状态为normal的值,同时定义了保存结果的集合名,最后的输出结果将保存在集合books_totals中,执行以下命令可以看到结果。

> db.books_totals.find()

{ "_id" : 1, "value" : 300 }

{ "_id" : 2, "value" : 200 }

这里的map、reduce函数都是利用JavaScript编写的函数,其中map函数的关键部分是emit(key, value)函数,此函数的调用使集合中的document对象按照key值生成一个value,形成一个键值对。其中key可以单一filed,也可以由多个filed组成,MongoDB会按照key生成对应的value值,value为一个数组。

reduce函数的定义中有参数key和value,其中 key就是上面map函数中指定的key值, value就是对应key对应的值,Array.sum(value)这里是对数组中的值求和,按照不同的业务需要,我们可以编写自己的javascript函数来处理。

6.3 简单聚集函数

管道模式和MapReduce模式都是重型武器,基本上可以解决数据分析中的所有问题,但有时在数据量不是很大的情况下,直接调用基于集合的函数会更简单,常用的简单聚集函数有以下几种。

1.distinct函数,用于返回不重复的记录,返回值是数组,函数原型如下。

db.orders.distinct( key,<query>)

第一个参数为filed,第二个参数为查询选择器,返回值不能大于系统规定的单个文档的最大值,如图6-3所示。图6-3 distinct简单聚集函数

2.count函数,用于统计查询返回的记录总数,函数原型如下。

db.collection.find(<query>).count()

执行如下命令。

> db.goods.find().count()

统计集合goods中的商品总数为99。

3.group函数与distinct一样,返回的结果集不能大于16MB,不能在分片集群上进行操作且group不能处理超过10000个唯一键值。如果我们的聚集操作超过了这个限制,只有使用上面介绍的管道聚集或MapReduce方案。

group的函数原型如下。

db.collection.group( { key : ..., initial: ..., reduce : ...[, cond: ...] } )

我们假设有如下结构的文档集合。

{ "_id" : 1, "value" : 100 }

{ "_id" : 4, "value" : 200 }

{ "_id" : 2, "value" : 200 }

{ "_id" : 1, "value" : 500 }

{ "_id" : 2, "value" : 100 }

如果需要统计_id小于3,按照_id分组求value值的和,执行如下命令即可。

db.books.group(

{

key: { _id: 1 },

cond: { _id: { $lt: 3 } },

reduce: function(cur, result)

{

result.count += cur.count

},

initial: { count: 0 }

}

)

得到的结果如下所示。

[

{ _id: 1, count: 600},

{ _id: 2, count: 300}

]

6.4 小结

MongoDB的聚集操作是为大数据分析准备的,尤其是MapReduce可以在分片集群上进行操作。MongoDB既提供了简单的类似于关系数据库中的聚集函数,2.1版本后又提供了增强版的聚集框架,以满足一般的统计分析应用。

MapReduce是一种编程模型,最早有Google提出,MongoDB也在这方面不断地完善、改进,提高其性能,使之成为处理大规模数据集(大于1TB)的利器。Map(映射)和Reduce(归约),和它们的主要思想,都是从函数式编程语言里借来的,还有从矢量编程语言里借来的特性,它极大地方便了编程人员,使他们在不会分布式并行编程的情况下可以将自己的程序运行在分布式系统上。当前的软件实现是指定一个Map(映射)函数,用来把一组键值对映射成一组新的键值对,指定并发的Reduce(归约)函数,用来保证所有映射的键值对中的每一个共享相同的键组。

第7章 复制集

复制集Replica Sets与第8章要介绍的分片Sharding 是MongoDB 最具特色的功能,其中复制集实现了数据库的冗余备份、故障转移,这两大功能应该是所有数据库管理人员追求的目标;分片实现了数据的分布式存储、负载均衡,这些都是海量数据的云存储平台不可或缺的功能,下面我们先从复制集介绍。

7.1 复制集概述

数据库总是会遇到各种失败的场景,如网络连接断开、断电等。尽管 Journaling 日志功能也提供了数据恢复的功能,但它通常是针对单个节点来说的,只能保证单节点数据的一致性。而复制集通常是由多个节点组成,每个节点除了 Journaling 日志恢复功能外,整个复制集还具有故障自动转移的功能,这样能保证数据库的高可用性。在生产环境中一个复制集最少应该包含三个节点,其中有一个必须是主节点,典型的部署结构如图7-1所示。图7-1 复制集结构图

每个节点都是一个mongod进程对应的实例,节点之间互相周期性地通过心跳检查对方的状态。默认情况下primary节点负责数据的读、写,second节点备份primary节点上的数据,但是arbiter节点不会从primary节点同步数据。从它的名字arbiter可以看出,它起到的作用只是当primary节点故障时,能够参与到复制集剩下的节点中,选择出一个新的primary节点,它自己永远不会变为primary节点,也不会参与数据的读写。也就是说,数据库的数据会存在primary和second节点中,second节点相当于一个备份。当然second节点可以有多个,当primary节点故障时,second节点有可能变为primary节点,典型流程如图7-2所示。图7-2 故障转移流程图

下面我们就配置一个这样的复制集,后面很多操作都会依赖于这个复制集。

1.创建复制集中每个节点存放数据的目录。

E:\MongoDB-win32-i386-2.6.3\db_rs0\data\rs0_0

E:\MongoDB-win32-i386-2.6.3\db_rs0\data\rs0_1

E:\MongoDB-win32-i386-2.6.3\db_rs0\data\rs0_2

2.创建复制集中每个节点的日志文件。

E:\MongoDB-win32-i386-2.6.3\db_rs0\logs\rs0_0.log

E:\MongoDB-win32-i386-2.6.3\db_rs0\logs\rs0_1.log

E:\MongoDB-win32-i386-2.6.3\db_rs0\logs\rs0_2.log

3.创建复制集中的每个节点启动时所需的配置文件。

第一个节点配置文件为:E:\MongoDB-win32-i386-2.6.3\configs_rs0\rs0_0.conf,内容如下所示。

dbpath = E:\MongoDB-win32-i386-2.6.3\db_rs0\data\rs0_0

logpath = E:\MongoDB-win32-i386-2.6.3\db_rs0\logs\rs0_0.log

journal = true

port = 40000

replSet = rs0

文件中dbpath指向数据库数据文件存放的路径(在第1步中已创建好),logpath指向数据库的日志文件路径(第2步中已创建好),journal表示对于此mongod实例是否启动日志功能,port 为实例监听的端口号,rs0 为实例所在的复制集名称,更多参数的意思可以参考MongoDB手册。

第二个节点配置文件为:E:\MongoDB-win32-i386-2.6.3\configs_rs0\rs0_1.conf,内容如下所示。

dbpath = E:\MongoDB-win32-i386-2.6.3\db_rs0\data\rs0_1

logpath = E:\MongoDB-win32-i386-2.6.3\db_rs0\logs\rs0_1.log

journal = true

port = 40001

replSet = rs0

第三个节点配置文件为:E:\MongoDB-win32-i386-2.6.3\configs_rs0\rs0_2.conf,内容如下所示。

dbpath = E:\MongoDB-win32-i386-2.6.3\db_rs0\data\rs0_2

logpath = E:\MongoDB-win32-i386-2.6.3\db_rs0\logs\rs0_2.log

journal = true

port = 40002

replSet = rs0

4.启动上面三个节点对应的MongoDB实例。

mongod–config E:\MongoDB-win32-i386-2.6.3\configs_rs0\rs0_0.conf

mongod–config E:\MongoDB-win32-i386-2.6.3\configs_rs0\rs0_1.conf

mongod–config E:\MongoDB-win32-i386-2.6.3\configs_rs0\rs0_2.conf

我们观察一下每个实例的启动日志,日志中都有如下内容。

[rsStart] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)

[rsStart] replSet info you may need to run replSetInitiate -rs.initiate() in the shell -- if that is not already done

上面日志说明虽然已经成功启动了3个实例,但是复制集还没配置好,复制集的信息会保存在每个mongod实例上的local数据库中,即local.system.replset上。按照图7-1所描述的那样,我们应该通过配置确定哪个节点为 primary、哪个为 second、哪个为 arbiter。下面开始配置复制集。

5.启动一个mongo客户端,连接到上面的一个mongod实例。

>mongo --port 40000

我们来运行以下命令初始化复制集。

> rs.initiate()

{

"info2" : "no configuration explicitly specified -- making one",

"me" : "Guo:40000",

"info":"Config now saved locally.Should come online in about a min e.",

"ok" : 1

}

这个时候的复制集还只有刚才这个初始化的成员,通过如下命令查看到。

> rs.conf()

{

"_id" : "rs0",

"version" : 1,

"members" : [

{

"_id" : 0,

"host" : "Guo:40000"

}

]

}

按照MongoDB的默认设置,刚才执行初始化命令的这个mongod实例将成为复制集中的primary节点。

6.接下来在复制集中添加图7-1中的second节点和arbiter节点,继续在上面的mongod实例上执行如下命令。

rs0:PRIMARY> rs.add("Guo:40001")

{ "ok" : 1 }

rs0:PRIMARY> rs.addArb("Guo:40002")

{ "ok" : 1 }

注意此时命令的前缀已变为 rs0:PRIMARY ,说明当前执行命令的机器是复制集中primary机器,上面的命令通过rs.add()添加一个默认的second节点,rs.addArb()添加一个默认的arbiter节点,命令成功执行后,就会生成图7-1所示那样的一个复制集。

7.观察整个复制集的状态信息,几个重要参数我们会在后面说明。

rs0:PRIMARY> rs.status()

{

"set" : "rs0",//复制集的名称

"date" : ISODate("2013-08-18T09:03:49Z"),

"myState":1,//当前节点成员在复制集中的位置,如1表示primary,2表示secondry

"members" : [//复制集的所有成员信息

{

"_id" : 0, //成员编号

"name" : "Guo:40000",//成员所在的服务器名称

"health" : 1,//成员在复制集中是否运行,1表示运行,0失败

"state" : 1,//成员在复制集中的状态,1是primary

"stateStr" : "PRIMARY",//成员在复制集中的状态名称

"uptime" : 2186,//成员的在线时间,单位是秒

"optime" : {//这个是用来进行同步用的,后面重点分析

"t" : 1376816431,

"i" : 1

},

"optimeDate" : ISODate("2013-08-18T09:00:31Z"),

"self" : true //成员为当前命令所在的服务器

},

{

"_id" : 1,

"name" : "Guo:40001",

"health" : 1, ,//成员在复制集中是否运行,1表示运行

"state" : 2 ,//成员在复制集中的状态,2是secondary

"stateStr" : "SECONDARY",

"uptime" : 306,

"optime" : {

"t" : 1376816431,

"i" : 1

},

"optimeDate" : ISODate("2013-08-18T09:00:31Z"),

"lastHeartbeat" : ISODate("2013-08-18T09:03:47Z"),

"lastHeartbeatRecv" : ISODate("2013-08-18T09:03:47Z"),

"pingMs" : 0,//此远端成员到本实例间一个路由包的来回时间

"syncingTo" : "Guo:40000"//此成员需要从哪个实例同步数据

},

{

"_id" : 2,

"name" : "Guo:40002",

"health" : 1,

"state" : 7, //成员在复制集中的状态位置,7是arbiter

"stateStr" : "ARBITER",

"uptime" : 198,

"lastHeartbeat" : ISODate("2013-08-18T09:03:49Z"),

"lastHeartbeatRecv" : ISODate("1970-01-01T00:00:00Z"),

"pingMs" : 0,//此远端成员到本实例间一个路由包的来回时间

}

],

"ok" : 1

}

上面复制集状态信息的输出是基于 primary 实例的,也可以在 secondary 实例上输出复制集的状态信息,包含的字段与上面大致相同。上面的输出有些地方还需进一步解释,如在arbiter成员节点上没有字段syncingTo,说明它不需要从primary节点上同步数据,因为它只是一个当主节点发生故障时、在复制集剩下的secondary节点中选择一个新priamry节点的仲裁者,因此运行此实例的机器不需要太多的存储空间。

上面输出的字段中还有几个时间相关的字段,如"date"表示当前实例所在服务器的时间,"lastHeartbeat"表示当前实例到此远端成员最近一次成功发送与接收心跳包的时间,通过比较这个两个时间我们可以判断当前实例与此成员相差的时间间隔。比如某个成员宕机了,本实例发像此宕机成员的心跳包就不会被成功接收,随着时间推移,本实例的 data字段值与此成员上的lastHeartbeat差值就会逐渐增加。

上面还有一个optime字段,这个字段的值说明了本实例最近一次更改数据库的时间"t":1376816431 以及每秒执行的操作数据库的次数"i" : 1。此字段的值实际上是从本实例上的local数据库中的oplog.rs集合上读取的,这个集合还详细记录了具体是什么操作,如插入语句、修改语句等。复制集中的每一个实例都会有一个这样的数据库和集合,如果复制集运行正常,理论上来说,每一个mongod实例上此集合中的记录应该相同。实际上MongoDB也是根据此集合来实现复制集中primary节点与secondary节点间的数据同步。

7.2 复制集工作机制

7.2.1 数据同步

7.1节概述了复制集,我们整体上对复制集有了个概念,但是复制集最重要的功能—自动故障转移是怎么实现的?复制集又是怎样实现数据同步的呢?带着这两个问题,我们下面展开分析。

我们先利用mongo客户端登录到复制集的primary节点上。

>mongo --port 40000

查看实例上所有数据库。

rs0:PRIMARY> show dbs

local 0.09375GB

我们可以看到只有一个 local 数据库,因为此时还没有在复制集上创建其他任何数据库,local数据库为复制集所有成员节点上默认创建了一个数据库。在primary节点上查看local数据上的集合,如下所示。

rs0:PRIMARY> show collections

oplog.rs

slaves

startup_log

system.indexes

system.replset

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载