MongoDB-秘籍-全-
MongoDB 秘籍(全)
原文:
zh.annas-archive.org/md5/9F335F41611FE256D46F623124D9DAEC译者:飞龙
前言
MongoDB 是一种面向文档的领先 NoSQL 数据库,提供线性可扩展性,因此成为高容量、高性能系统在所有业务领域的良好竞争者。它在易用性、高性能和丰富功能方面胜过大多数 NoSQL 解决方案。
本书提供了详细的配方,描述了如何使用 MongoDB 的不同功能。这些配方涵盖了从设置 MongoDB、了解其编程语言 API 和监控和管理,到一些高级主题,如云部署、与 Hadoop 集成,以及一些用于 MongoDB 的开源和专有工具。配方格式以简洁、可操作的形式呈现信息;这使您可以参考配方,以解决并了解手头的用例的详细信息,而无需阅读整本书。
本书涵盖的内容
第一章,“安装和启动服务器”,全都是关于启动 MongoDB。它将演示如何以独立模式、副本集模式和分片模式启动服务器,使用命令行或配置文件提供的启动选项。
第二章,“命令行操作和索引”,有简单的配方,用于在 Mongo shell 中执行 CRUD 操作,并在 shell 中创建各种类型的索引。
第三章,“编程语言驱动程序”,讨论了编程语言 API。虽然 Mongo 支持多种语言,但我们只会讨论如何使用驱动程序仅从 Java 和 Python 程序连接到 MongoDB 服务器。本章还探讨了 MongoDB 的线协议,用于服务器和编程语言客户端之间的通信。
第四章,“管理”,包含了许多用于管理或 MongoDB 部署的配方。本章涵盖了许多经常使用的管理任务,如查看集合和数据库的统计信息,查看和终止长时间运行的操作以及其他副本集和分片相关的管理。
第五章,“高级操作”,是第二章的延伸,我们将看一些稍微高级的功能,如实现服务器端脚本、地理空间搜索、GridFS、全文搜索,以及如何将 MongoDB 与外部全文搜索引擎集成。
第六章,“监控和备份”,告诉您有关管理和一些基本监控的所有内容。然而,MongoDB 提供了一流的监控和实时备份服务,MongoDB 监控服务(MMS)。在本章中,我们将看一些使用 MMS 进行监控和备份的配方。
第七章,“在云上部署 MongoDB”,涵盖了使用 MongoDB 服务提供商进行云部署的配方。我们将在 AWS 云上设置自己的 MongoDB 服务器,以及在 Docker 容器中运行 MongoDB。
第八章,“与 Hadoop 集成”,涵盖了将 MongoDB 与 Hadoop 集成的配方,以使用 Hadoop MapReduce API 在 MongoDB 数据文件中运行 MapReduce 作业并将结果写入其中。我们还将看到如何使用 AWS EMR 在云上运行我们的 MapReduce 作业,使用亚马逊的 Hadoop 集群 EMR 和 mongo-hadoop 连接器。
第九章,开源和专有工具,介绍了使用围绕 MongoDB 构建的框架和产品来提高开发人员的生产力,或者简化使用 Mongo 的一些日常工作。除非明确说明,本章中将要查看的产品/框架都是开源的。
附录,参考概念,为您提供了有关写入关注和读取偏好的一些额外信息。
您需要什么来阅读本书
用于尝试配方的 MongoDB 版本是 3.0.2。这些配方也适用于版本 2.6.x。如果有特定于版本 2.6.x的特殊功能,将在配方中明确说明。除非明确说明,所有命令都应在 Ubuntu Linux 上执行。
涉及 Java 编程的示例已在 Java 版本 1.7 上进行了测试和运行,Python 代码则使用 Python v2.7 运行(与 Python 3 兼容)。对于 MongoDB 驱动程序,您可以选择使用最新可用版本。
这些是相当常见的软件类型,它们的最低版本在不同的配方中使用。本书中的所有配方都将提到完成它所需的软件及其各自的版本。一些配方需要在 Windows 系统上进行测试,而另一些需要在 Linux 上进行测试。
这本书是为谁准备的
这本书是为对了解 MongoDB 并将其用作高性能和可扩展数据存储的管理员和开发人员设计的。它也适用于那些了解 MongoDB 基础知识并希望扩展知识的人。本书的受众预期至少具有一些 MongoDB 基础知识。
约定
在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些这些样式的示例,以及它们的含义解释。
在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些这些样式的示例和它们的含义解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:"创建/data/mongo/db 目录(或您选择的任何目录)"。
代码块设置如下:
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
任何命令行输入或输出都按如下方式编写:
$ sudo apt-get install default-jdk
新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:"由于我们想要启动一个免费的微实例,请在左侧勾选仅免费层复选框"。
注意
警告或重要提示会以以下方式显示在框中。
提示
提示和技巧会以这种方式出现。
读者反馈
我们的读者反馈总是受欢迎的。让我们知道你对这本书的想法——你喜欢什么,或者可能不喜欢什么。读者的反馈对我们开发能让你真正受益的标题非常重要。
要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在消息主题中提及书名。
如果您在某个专题上有专业知识,并且有兴趣撰写或为一本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
既然您已经是 Packt 书籍的自豪所有者,我们有很多事情可以帮助您充分利用您的购买。
下载示例代码
您可以从www.packtpub.com的帐户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误确实会发生。如果您在我们的书籍中发现错误——可能是文本或代码中的错误,我们将不胜感激地希望您向我们报告。通过这样做,您可以帮助其他读者避免挫折,并帮助我们改进本书的后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表格链接,并输入您的勘误详情。一旦您的勘误经过验证,您的提交将被接受,并且勘误将被上传到我们的网站上,或者添加到该标题的勘误列表中的任何现有勘误下的勘误部分。您可以通过从www.packtpub.com/support选择您的标题来查看任何现有的勘误。
盗版
互联网上盗版版权材料是所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上发现我们作品的任何非法副本,请立即向我们提供位置地址或网站名称,以便我们采取补救措施。
请通过<copyright@packtpub.com>与我们联系,并附上涉嫌盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值的内容的能力方面的帮助。
问题
如果您在书籍的任何方面遇到问题,可以通过<questions@packtpub.com>与我们联系,我们将尽力解决。
第一章:安装和启动服务器
在本章中,我们将涵盖以下配方:
-
安装单节点 MongoDB
-
使用命令行选项启动单个节点实例
-
使用配置文件从安装单节点 MongoDB
-
在 Mongo shell 中使用 JavaScript 连接到单个节点
-
从 Java 客户端连接到单个节点
-
从 Python 客户端连接到单个节点
-
作为副本集的一部分启动多个实例
-
连接到副本集以查询和插入数据
-
从 Java 客户端连接到副本集以查询和插入数据
-
从 Python 客户端连接到副本集以查询和插入数据
-
启动包含两个分片的简单分片环境
-
在 shell 中连接到分片并执行操作
介绍
在本章中,我们将看看如何启动 MongoDB 服务器。虽然对于开发目的以默认设置启动服务器很容易,但有许多可用于微调启动行为的选项。我们将作为单个节点启动服务器,然后介绍各种配置选项。我们将通过设置一个简单的副本集并运行一个分片集群来结束本章。因此,让我们开始以最简单的方式安装和设置 MongoDB 服务器,以用于简单的开发目的。
安装单节点 MongoDB
在这个配方中,我们将看看如何以独立模式安装 MongoDB。这是启动 MongoDB 服务器的最简单和最快的方法,但很少用于生产用例。然而,这是开发目的中启动服务器的最常见方式。在这个配方中,我们将在不看很多其他启动选项的情况下启动服务器。
准备工作
嗯,假设我们已经从下载站点下载了 MongoDB 二进制文件,解压缩并将生成的 bin 目录放在操作系统的路径变量中。(这不是强制性的,但这样做后确实变得更加方便。)可以从www.mongodb.org/downloads下载二进制文件,然后选择您的主机操作系统。
如何做…
-
创建目录
/data/mongo/db(或您选择的任何目录)。这将是我们的数据库目录,并且需要由mongod(mongo 服务器进程)进程具有写入权限。 -
我们将从控制台启动服务器,数据目录为
/data/mongo/db,如下所示:
> mongod --dbpath /data/mongo/db
它是如何工作的…
如果您在控制台上看到以下行,则已成功启动服务器:
[initandlisten] waiting for connections on port 27017
启动服务器再也没有比这更容易的了。尽管启动服务器的简单性,但有许多配置选项可用于调整服务器在启动时的行为。大多数默认选项是合理的,不需要更改。使用默认值,服务器应该监听端口27017以进行新连接,并且日志将打印到标准输出。
另请参阅
有时我们希望在服务器启动时配置一些选项。在安装单节点 MongoDB配方中,我们将使用一些更多的启动选项。
使用命令行选项启动单个节点实例
在这个配方中,我们将看到如何使用一些命令行选项启动独立的单节点服务器。我们将看一个例子,我们想要做以下事情:
-
启动服务器监听端口
27000 -
日志应写入
/logs/mongo.log -
数据库目录是
/data/mongo/db
由于服务器已经为开发目的启动,我们不希望预先分配完整大小的数据库文件。(我们很快会看到这意味着什么。)
准备工作
如果您已经看过并执行了安装单节点 MongoDB配方,则无需做任何不同的事情。如果所有这些先决条件都得到满足,那么我们就可以开始本配方了。
如何做…
-
数据库的
/data/mongo/db目录和日志的/logs/应该在您的文件系统上创建并存在,并具有适当的权限进行写入。 -
执行以下命令:
> mongod --port 27000 --dbpath /data/mongo/db –logpath /logs/mongo.log --smallfiles
工作原理…
好的,这并不太困难,与之前的配方类似,但这次我们有一些额外的命令行选项。MongoDB 实际上在启动时支持相当多的选项,我认为我们将看到一些最常见和最重要的选项列表:
| 选项 | 描述 |
|---|---|
--help 或 -h |
用于打印可用的各种启动选项的信息。 |
--config 或 -f |
这指定包含所有配置选项的配置文件的位置。我们将在以后的配方中更多地了解这个选项。这只是一种方便的方式,可以在文件中指定配置,而不是在命令提示符中指定;特别是当指定的选项数量更多时。使用一个共享的配置文件跨不同的 MongoDB 实例也将确保所有实例都使用相同的配置运行。 |
--verbose 或 -v |
这会使日志更冗长;我们可以添加更多的 v 来使输出更冗长,例如,-vvvvv。 |
--quiet |
这会产生更安静的输出;这与冗长或 -v 选项相反。它将使日志更少,更整洁。 |
--port |
如果您希望启动服务器侦听除默认端口 27017 以外的某个端口,则使用此选项。每当我们希望在同一台机器上启动多个 mongo 服务器时,我们会经常使用此选项,例如,--port 27018 将使服务器侦听端口 27018 以获取新连接。 |
--logpath |
这提供了一个日志文件的路径,日志将被写入其中。该值默认为 STDOUT。例如,--logpath /logs/server.out 将使用 /logs/server.out 作为服务器的日志文件。请记住,提供的值应该是一个文件,而不是日志将被写入的目录。 |
--logappend |
如果有的话,此选项将追加到现有的日志文件。默认行为是重命名现有的日志文件,然后为当前启动的 mongo 实例的日志创建一个新文件。假设我们已经将日志文件命名为 server.out,并且在启动时该文件存在,则默认情况下此文件将被重命名为 server.out.<timestamp>,其中 <timestamp> 是当前时间。时间是 GMT 时间,而不是本地时间。假设当前日期是 2013 年 10 月 28 日,时间是 12:02:15,则生成的文件将具有以下值作为时间戳:2013-10-28T12-02-15。 |
--dbpath |
这为您提供了一个新数据库将被创建或现有数据库存在的目录。该值默认为 /data/db。我们将使用 /data/mongo/db 作为数据库目录启动服务器。请注意,该值应该是一个目录,而不是文件的名称。 |
--smallfiles |
这在开发过程中经常使用,当我们计划在本地机器上启动多个 mongo 实例时。Mongo 在启动时会在 64 位机器上创建一个大小为 64MB 的数据库文件。出于性能原因,这种预分配会发生,并且文件将被创建并写入零以填充磁盘上的空间。在启动时添加此选项将仅创建一个预分配文件,大小为 16MB(同样,在 64 位机器上)。此选项还会减小数据库和日志文件的最大大小。不要在生产部署中使用此选项。另外,默认情况下,文件大小会增加到最大 2GB。如果选择了 --smallfile 选项,则最大增加到 512MB。 |
--replSet |
此选项用于将服务器启动为复制集的成员。此arg的值是复制集的名称,例如,--replSet repl1。在以后的食谱中,您将更多地了解这个选项,我们将启动一个简单的 mongo 复制集。 |
--configsvr |
此选项用于将服务器启动为配置服务器。当我们在本章的后续食谱中设置一个简单的分片环境时,配置服务器的角色将更加清晰。 |
--shardsvr |
这通知启动的 mongod 进程,该服务器正在作为分片服务器启动。通过给出此选项,服务器还会监听端口27018,而不是默认的27017。当我们启动一个简单的分片服务器时,我们将更多地了解这个选项。 |
--oplogSize |
Oplog 是复制的支柱。它是一个有上限的集合,主实例写入的数据存储在其中,以便复制到次要实例。此集合位于名为local的数据库中。在初始化复制集时,oplog 的磁盘空间被预先分配,并且数据库文件(用于本地数据库)被填充为占位符的零。默认值为磁盘空间的 5%,对于大多数情况来说应该足够好。oplog 的大小至关重要,因为有上限的集合是固定大小的,当超过其大小时,它们会丢弃其中的最旧文档,从而为新文档腾出空间。oplog 大小非常小可能导致数据在复制到次要节点之前被丢弃。oplog 大小很大可能导致不必要的磁盘空间利用和复制集初始化的持续时间很长。对于开发目的,当我们在同一主机上启动多个服务器进程时,我们可能希望将 oplog 大小保持在最小值,快速启动复制集,并使用最小的磁盘空间。 |
--storageEngine |
从 MongoDB 3.0 开始,引入了一个名为 Wired Tiger 的新存储引擎。以前(默认)的存储引擎现在称为mmapv1。要使用 Wired Tiger 而不是mmapv1启动 MongoDB,请使用此选项的wiredTiger值。 |
--dirctoryperdb |
默认情况下,MongoDB 的数据库文件存储在一个公共目录中(如--dbpath中提供的)。此选项允许您将每个数据库存储在上述数据目录中的自己的子目录中。具有这样细粒度的控制允许您为每个数据库拥有单独的磁盘。 |
还有更多…
要获取可用选项的详尽列表,请使用--help或-h选项。这些选项列表并不详尽,我们将在以后的食谱中看到更多的选项,只要我们需要它们。在下一个食谱中,我们将看到如何使用配置文件而不是命令行参数。
另请参阅
-
使用配置文件提供启动选项的 MongoDB 单节点安装
-
启动多个实例作为复制集的一部分 来启动一个复制集
-
启动一个包含两个分片的简单分片环境 来设置一个分片环境
使用配置文件进行 MongoDB 的单节点安装
正如我们所看到的,从命令行提供选项可以完成工作,但是一旦我们提供的选项数量增加,情况就开始变得尴尬了。我们有一个干净而好的选择,可以从配置文件而不是作为命令行参数来提供启动选项。
准备工作
如果您已经执行了安装单节点 MongoDB食谱,那么您无需做任何不同的事情,因为此食谱的所有先决条件都是相同的。
如何做…
数据库的/data/mongo/db目录和日志的/logs/应该在您的文件系统上创建并存在,并具有适当的权限以写入它并执行以下步骤:
- 创建一个可以有任意名称的配置文件。在我们的情况下,假设我们在
/conf/mongo.conf中创建了这个文件。然后编辑文件并添加以下行:
port = 27000
dbpath = /data/mongo/db
logpath = /logs/mongo.log
smallfiles = true
- 使用以下命令启动 mongo 服务器:
> mongod --config /config/mongo.conf
工作原理…
我们在配置文件中提供了前面一篇文章中讨论的所有命令行选项,使用命令行选项启动单个节点实例。我们只是将它们提供在一个配置文件中。如果您还没有阅读前一篇文章,我建议您这样做,因为那里我们讨论了一些常见的命令行选项。属性被指定为<property name> = <value>。对于所有没有值的属性,例如smallfiles选项,给定的值是一个布尔值,true。如果我们需要有详细的输出,我们会在我们的配置文件中添加 v=true(或多个 v 以使其更详细)。如果您已经知道命令行选项是什么,那么猜测属性在文件中的值就很容易了。它几乎与去掉连字符的命令行选项相同。
使用 JavaScript 在 Mongo shell 中连接到单个节点
这个示例是关于启动 mongo shell 并连接到 MongoDB 服务器。在这里,我们还演示了如何在 shell 中加载 JavaScript 代码。虽然这并不总是必需的,但当我们有一大块带有变量和函数的 JavaScript 代码,并且这些函数需要经常从 shell 中执行并且我们希望这些函数始终在 shell 中可用时,这是很方便的。
准备就绪
虽然可能会在不连接到 MongoDB 服务器的情况下运行 mongo shell,但我们很少需要这样做。要在本地主机上启动服务器而不费吹灰之力,请查看第一篇文章安装单节点 MongoDB,并启动服务器。
如何做…
- 首先,我们创建一个简单的 JavaScript 文件并将其命名为
hello.js。在hello.js文件中输入以下内容:
function sayHello(name) {
print('Hello ' + name + ', how are you?')
}
-
将此文件保存在位置
/mongo/scripts/hello.js。(这也可以保存在任何其他位置。) -
在命令提示符上执行以下操作:
> mongo --shell /mongo/scripts/hello.js
- 执行此命令时,我们应该在控制台上看到以下内容打印出来:
MongoDB shell version: 3.0.2
connecting to: test
>
- 通过输入以下命令来测试 shell 连接的数据库:
> db
这应该在控制台上打印出test。
- 现在,在 shell 中输入以下命令:
> sayHello('Fred')
- 您应该收到以下响应:
Hello Fred, how are you?
注意
注意:本书是使用 MongoDB 版本 3.0.2 编写的。您可能正在使用更新的版本,因此在 mongo shell 中可能看到不同的版本号。
工作原理…
我们在这里执行的 JavaScript 函数没有实际用途,只是用来演示如何在 shell 启动时预加载函数。.js文件中可能包含有效的 JavaScript 代码,可能是一些复杂的业务逻辑。
在没有任何参数的情况下执行mongo命令时,我们连接到在本地主机上运行的 MongoDB 服务器,并在默认端口27017上监听新连接。一般来说,命令的格式如下:
mongo <options> <db address> <.js files>
在没有传递参数给 mongo 可执行文件的情况下,它相当于将db 地址传递为localhost:27017/test。
让我们看一些db 地址命令行选项的示例值及其解释:
-
mydb:这将连接到在本地主机上运行并监听端口27017上的连接的服务器。连接的数据库将是mydb。 -
mongo.server.host/mydb:这将连接到在mongo.server.host上运行并使用默认端口27017的服务器。连接的数据库将是mydb。 -
mongo.server.host:27000/mydb:这将连接到在mongo.server.host上运行并使用端口27000的服务器。连接的数据库将是mydb。 -
mongo.server.host:27000:这将连接到运行在mongo.server.host上的服务器,端口为27000。连接的数据库将是默认数据库 test。
现在,Mongo 客户端也有很多选项可用。我们将在下表中看到其中一些:
| 选项 | 描述 |
|---|---|
--help或-h |
这显示有关各种命令行选项使用的帮助。 |
--shell |
当给定.js文件作为参数时,这些脚本将被执行,mongo 客户端将退出。提供此选项可以确保在 JavaScript 文件执行后,shell 保持运行。在启动时,这些.js文件中定义的所有函数和变量都可在 shell 中使用。与前面的情况一样,JavaScript 文件中定义的sayHello函数可在 shell 中调用。 |
--port |
指定客户端需要连接的 mongo 服务器的端口。 |
--host |
这指定了客户端需要连接的 mongo 服务器的主机名。如果db 地址提供了主机名、端口和数据库,那么--host和--port选项都不需要指定。 |
--username或-u |
当 Mongo 启用安全性时,这是相关的。它用于提供要登录的用户的用户名。 |
--password或-p |
当 Mongo 启用安全性时,这个选项是相关的。它用于提供要登录的用户的密码。 |
使用 Java 客户端连接到单个节点
这个教程是关于为 MongoDB 设置 Java 客户端的。在处理其他教程时,您将反复参考这个教程,所以请仔细阅读。
准备工作
以下是这个教程的先决条件:
-
建议使用 Java SDK 1.6 或更高版本。
-
使用最新版本的 Maven。在撰写本书时,版本 3.3.3 是最新版本。
-
在撰写本书时,MongoDB Java 驱动程序版本 3.0.1 是最新版本。
-
连接到互联网以访问在线 maven 存储库或本地存储库。或者,您可以选择一个适合您的计算机访问的本地存储库。
-
Mongo 服务器正在本地主机和端口
27017上运行。查看第一个教程,安装单节点 MongoDB,并启动服务器。
操作步骤如下:
-
如果您的机器上还没有安装最新版本的 JDK,请从
www.java.com/en/download/下载。我们不会在这个教程中介绍安装 JDK 的步骤,但在进行下一步之前,JDK 应该已经安装好了。 -
需要从
maven.apache.org/download.cgi下载 Maven。在下载页面上应该看到类似以下图片的内容。选择.tar.gz或.zip格式的二进制文件并下载。这个教程是在运行 Windows 平台的机器上执行的,因此这些安装步骤是针对 Windows 的。![操作步骤如下:]()
-
下载完档案后,我们需要解压它,并将提取的档案中的
bin文件夹的绝对路径放入操作系统的路径变量中。Maven 还需要将 JDK 的路径设置为JAVA_HOME环境变量。记得将你的 JDK 根目录设置为这个变量的值。 -
现在我们只需要在命令提示符上输入
mvn -version,如果看到以下开头的输出,我们就成功设置了 maven:
> mvn -version
- 在这个阶段,我们已经安装了 maven,现在准备创建我们的简单项目,在 Java 中编写我们的第一个 Mongo 客户端。我们首先创建一个
project文件夹。假设我们创建一个名为Mongo Java的文件夹。然后在这个project文件夹中创建一个文件夹结构src/main/java。project文件夹的根目录包含一个名为pom.xml的文件。一旦这个文件夹创建完成,文件夹结构应该如下所示:
Mongo Java
+--src
| +main
| +java
|--pom.xml
- 我们现在只有项目的框架。我们将在
pom.xml文件中添加一些内容。这并不需要太多。以下内容是我们在pom.xml文件中所需要的全部内容:
<project>
<modelVersion>4.0.0</modelVersion>
<name>Mongo Java</name>
<groupId>com.packtpub</groupId>
<artifactId>mongo-cookbook-java</artifactId>
<version>1.0</version> <packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>
</project>
- 最后,我们编写一个 Java 客户端,用于连接到 Mongo 服务器并执行一些非常基本的操作。以下是
com.packtpub.mongo.cookbook包中src/main/java位置中的 Java 类,类名为FirstMongoClient:
package com.packtpub.mongo.cookbook;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import java.net.UnknownHostException;
import java.util.List;
/**
* Simple Mongo Java client
*
*/
public class FirstMongoClient {
/**
* Main method for the First Mongo Client. Here we shall be connecting to a mongo
* instance running on localhost and port 27017.
*
* @param args
*/
public static final void main(String[] args)
throws UnknownHostException {
MongoClient client = new MongoClient("localhost", 27017);
DB testDB = client.getDB("test");
System.out.println("Dropping person collection in test database");
DBCollection collection = testDB.getCollection("person");
collection.drop();
System.out.println("Adding a person document in the person collection of test database");
DBObject person =
new BasicDBObject("name", "Fred").append("age", 30);
collection.insert(person);
System.out.println("Now finding a person using findOne");
person = collection.findOne();
if(person != null) {
System.out.printf("Person found, name is %s and age is %d\n", person.get("name"), person.get("age"));
}
List<String> databases = client.getDatabaseNames();
System.out.println("Database names are");
int i = 1;
for(String database : databases) {
System.out.println(i++ + ": " + database);
}
System.out.println("Closing client");
client.close();
}
}
- 现在是执行前面的 Java 代码的时候了。我们将使用 maven 从 shell 中执行它。您应该在项目的
pom.xml所在的同一目录中:
mvn compile exec:java -Dexec.mainClass=com.packtpub.mongo.cookbook.FirstMongoClient
它是如何工作的...
这些是相当多的步骤要遵循。让我们更详细地看一些步骤。直到第 6 步为止,都是直接的,不需要任何解释。让我们从第 7 步开始看起。
我们这里有的pom.xml文件非常简单。我们在 mongo 的 Java 驱动程序上定义了一个依赖关系。它依赖于在线存储库repo.maven.apache.org来解析这些构件。对于本地存储库,我们所需要做的就是在pom.xml中定义repositories和pluginRepositories标签。有关 maven 的更多信息,请参阅 maven 文档maven.apache.org/guides/index.html。
对于 Java 类,org.mongodb.MongoClient类是主干。我们首先使用其重载的构造函数实例化它,给出服务器的主机和端口。在这种情况下,主机名和端口实际上并不是必需的,因为提供的值已经是默认值,而且无参数的构造函数也可以很好地工作。以下代码片段实例化了这个客户端:
MongoClient client = new MongoClient("localhost", 27017);
下一步是获取数据库,在这种情况下,使用getDB方法来测试。这将作为com.mongodb.DB类型的对象返回。请注意,这个数据库可能不存在,但getDB不会抛出任何异常。相反,只有在我们向该数据库的集合中添加新文档时,数据库才会被创建。同样,DB 对象上的getCollection将返回一个代表数据库中集合的com.mongodb.DBCollection类型的对象。这个集合在数据库中也可能不存在,并且在插入第一个文档时会自动创建。
我们的类中以下两个代码片段向您展示了如何获取DB和DBCollection的实例:
DB testDB = client.getDB("test");
DBCollection collection = testDB.getCollection("person");
在插入文档之前,我们将删除集合,以便即使在程序的多次执行中,person 集合中也只有一个文档。使用DBCollection对象的drop()方法来删除集合。接下来,我们创建一个com.mongodb.DBObject的实例。这是一个表示要插入到集合中的文档的对象。这里使用的具体类是BasicDBObject,它是java.util.LinkedHashMap类型,其中键是 String,值是 Object。值也可以是另一个DBObject,在这种情况下,它是嵌套在另一个文档中的文档。在我们的例子中,我们有两个键,name 和 age,它们是要插入的文档中的字段名,值分别是 String 和 Integer 类型。BasicDBObject的append方法将一个新的键值对添加到BasicDBObject实例中,并返回相同的实例,这使我们可以链接append方法调用以添加多个键值对。然后使用 insert 方法将创建的DBObject插入到集合中。这就是我们为 person 集合实例化DBObject并将其插入到集合中的方式:
DBObject person = new BasicDBObject("name", "Fred").append("age", 30);
collection.insert(person);
DBCollection上的findOne方法很简单,它从集合中返回一个文档。这个版本的findOne不接受DBObject(否则会在选择和返回文档之前执行的查询)作为参数。这相当于在 shell 中执行db.person.findOne()。
最后,我们只需调用getDatabaseNames来获取服务器中数据库名称的列表。此时,我们应该至少在返回的结果中有test和local数据库。完成所有操作后,我们关闭客户端。MongoClient类是线程安全的,通常一个应用程序使用一个实例。要执行该程序,我们使用 maven 的 exec 插件。在执行第 9 步时,我们应该在控制台的最后看到以下行:
[INFO] [exec:java {execution: default-cli}]
--snip--
Dropping person collection in test database
Adding a person document in the person collection of test database
Now finding a person using findOne
Person found, name is Fred and age is 30
Database names are
1: local
2: test
INFO: Closed connection [connectionId{localValue:2, serverValue:2}] to localhost:27017 because the pool has been closed.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3 seconds
[INFO] Finished at: Tue May 12 07:33:00 UTC 2015
[INFO] Final Memory: 22M/53M
[INFO] ------------------------------------------------------------------------
使用 Python 客户端连接到单个节点
在这个配方中,我们将使用 Python MongoDB 驱动程序 PyMongo 连接到单个 MongoDB 实例。使用 Python 的简单语法和多功能性与 MongoDB 结合在一起,许多程序员发现这个堆栈可以实现更快的原型设计和减少的开发周期。
准备工作
以下是此配方的先决条件:
-
Python 2.7.x(尽管该代码与 Python 3.x兼容)。
-
PyMongo 3.0.1:Python MongoDB 驱动程序。
-
Python 软件包安装程序(pip)。
-
Mongo 服务器正在 localhost 和端口
27017上运行。查看第一个配方,安装单节点 MongoDB,并启动服务器。
如何做…
- 根据您的操作系统,在 Ubuntu/Debian 系统上安装 pip 实用程序。您可以使用以下命令安装 pip:
> apt-get install python-pip
- 使用 pip 安装最新的 PyMongo 驱动程序:
> pip install pymongo
- 最后,创建一个名为
my_client.py的新文件,并输入以下代码:
from __future__ import print_function
import pymongo
# Connect to server
client = pymongo.MongoClient('localhost', 27017)
# Select the database
testdb = client.test
# Drop collection
print('Dropping collection person')
testdb.person.drop()
# Add a person
print('Adding a person to collection person')
employee = dict(name='Fred', age=30)
testdb.person.insert(employee)
# Fetch the first entry from collection
person = testdb.person.find_one()
if person:
print('Name: %s, Age: %s' % (person['name'], person['age']))
# Fetch list of all databases
print('DB\'s present on the system:')
for db in client.database_names():
print(' %s' % db)
# Close connection
print('Closing client connection')
client.close()
- 使用以下命令运行脚本:
> python my_client.py
它是如何工作的…
我们首先通过使用 pip 软件包管理器在系统上安装 Python MongoDB 驱动程序 pymongo。在给定的 Python 代码中,我们首先从__future__模块中导入print_function,以兼容 Python 3.x。接下来,我们导入 pymongo,以便在脚本中使用它。
我们使用 localhost 和27017作为 mongo 服务器主机和端口来实例化pymongo.MongoClient()。在 pymongo 中,我们可以直接使用<client>.<database_name>.<collection_name>的约定来引用数据库及其集合。
在我们的配方中,我们使用客户端处理程序通过引用client.test来选择数据库 test。即使数据库不存在,这也会返回一个数据库对象。作为这个配方的一部分,我们通过调用testdb.person.drop()来删除集合,其中testdb是对client.test的引用,person是我们希望删除的集合。对于这个配方,我们有意地删除集合,以便重复运行将始终在集合中产生一条记录。
接下来,我们实例化一个名为employee的字典,其中包含一些值,如姓名和年龄。现在,我们将使用insert_one()方法将此条目添加到我们的person集合中。
现在我们知道 person 集合中有一个条目,我们将使用find_one()方法获取一个文档。该方法根据磁盘上存储的文档的顺序返回集合中的第一个文档。
随后,我们还尝试通过调用get_databases()方法来获取所有数据库的列表到客户端。该方法返回服务器上存在的数据库名称列表。当您尝试断言服务器上是否存在数据库时,此方法可能会派上用场。
最后,我们使用close()方法关闭客户端连接。
作为副本集的一部分启动多个实例
在这个配方中,我们将看看在同一主机上启动多个服务器作为集群。启动单个 mongo 服务器足以用于开发目的或非关键应用。对于关键的生产部署,我们需要高可用性,如果一个服务器实例失败,另一个实例接管并且数据仍然可用于查询、插入或更新。集群是一个高级概念,我们无法在一个配方中涵盖整个概念。在这里,我们将浅尝辄止,并在本书后面的管理部分的其他配方中进行更详细的讨论。在这个配方中,我们将在同一台机器上启动多个 mongo 服务器进程,用于测试目的。在生产环境中,它们将在同一数据中心甚至不同数据中心的不同机器(或虚拟机)上运行。
让我们简要看一下什么是副本集。顾名思义,它是一组服务器,它们在数据方面彼此是副本。查看它们如何保持彼此同步以及其他内部情况是我们将推迟到管理部分的一些后续配方中,但要记住的一件事是,写操作只会发生在一个节点上,即主节点。默认情况下,所有查询也都是从主节点进行的,尽管我们可能会明确允许在次要实例上进行读操作。要记住的一个重要事实是,副本集并不是为了通过在副本集的各个节点之间分发读操作来实现可伸缩性。它的唯一目标是确保高可用性。
准备就绪
虽然不是必需条件,但查看使用命令行选项启动单节点实例的配方将会让事情变得更容易,以防您不了解在启动 mongo 服务器时各种命令行选项及其重要性。此外,在继续进行此配方之前,必须完成单服务器设置中提到的必要二进制文件和设置。让我们总结一下我们需要做什么。
我们将在本地主机上启动三个 mongod 进程(mongo 服务器实例)。
我们将为Node1、Node2和Node3分别创建三个数据目录/data/n1、/data/n2和/data/n3。同样,我们将把日志重定向到/logs/n1.log、/logs/n2.log和/logs/n3.log。以下图片将让您对集群的外观有一个概念:

如何做…
让我们详细看一下步骤:
-
为三个节点的数据和日志创建
/data/n1、/data/n2、/data/n3和/logs目录。在 Windows 平台上,您可以选择c:\data\n1、c:\data\n2、c:\data\n3和c:\logs\目录,或者选择其他目录来分别存放数据和日志。确保这些目录对于 mongo 服务器来说具有适当的写权限。 -
按照以下方式启动三个服务器。在 Windows 平台上,用户需要跳过
--fork选项,因为它不受支持:
$ mongod --replSet repSetTest --dbpath /data/n1 --logpath /logs/n1.log --port 27000 --smallfiles --oplogSize 128 --fork
$ mongod --replSet repSetTest --dbpath /data/n2 --logpath /logs/n2.log --port 27001 --smallfiles --oplogSize 128 --fork
$ mongod --replSet repSetTest --dbpath /data/n3 --logpath /logs/n3.log --port 27002 --smallfiles --oplogSize 128 –fork
- 启动 mongo shell 并连接到正在运行的任何 mongo 服务器。在这种情况下,我们连接到第一个(监听端口
27000)。执行以下命令:
$ mongo localhost:27000
- 连接到 mongo shell 后,尝试执行一个插入操作:
> db.person.insert({name:'Fred', age:35})
这个操作应该失败,因为副本集尚未初始化。更多信息可以在它是如何工作的……部分找到。
- 下一步是开始配置副本集。我们首先在 shell 中准备一个 JSON 配置,如下所示:
cfg = {
'_id':'repSetTest', 'members':[ {'_id':0, 'host': 'localhost:27000'}, {'_id':1, 'host': 'localhost:27001'}, {'_id':2, 'host': 'localhost:27002'} ]
}
- 最后一步是使用上述配置初始化副本集。
> rs.initiate(cfg)
- 在 shell 上几秒钟后执行
rs.status(),查看状态。几秒钟后,其中一个应该成为主节点,其余两个应该成为次要节点。
它是如何工作的……
我们在安装单节点 MongoDB示例中描述了常见的选项,之前的命令行选项示例中也描述了所有这些命令行选项的详细信息。
由于我们启动了三个独立的 mongod 服务,因此在文件系统上有三个专用的数据库路径。同样,我们为每个进程有三个单独的日志文件位置。然后,我们使用指定的数据库和日志文件路径启动了三个 mongod 进程。由于这个设置是为了测试目的,并且在同一台机器上启动,我们使用了--smallfiles和--oplogSize选项。由于这些进程在同一主机上运行,我们还选择了显式端口,以避免端口冲突。我们选择的端口是27000、27001和27002。当我们在不同的主机上启动服务器时,我们可能会选择一个单独的端口,也可能不选择。在可能的情况下,我们可以选择使用默认端口。
--fork选项需要一些解释。通过选择此选项,我们可以从操作系统的 shell 中将服务器作为后台进程启动,并在 shell 中恢复控制,然后可以启动更多这样的 mongod 进程或执行其他操作。如果没有--fork选项,我们不能在一个 shell 中启动多个进程,需要在三个单独的 shell 中启动三个 mongod 进程。
如果我们查看日志目录中生成的日志,我们应该看到其中的以下行:
[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
尽管我们使用--replSet选项启动了三个 mongod 进程,但我们仍然没有将它们配置为副本集。这个命令行选项只是用来告诉服务器在启动时,这个进程将作为副本集的一部分运行。副本集的名称与传递给命令提示符的选项的值相同。这也解释了为什么在初始化副本集之前,在一个节点上执行的插入操作失败了。在 mongo 副本集中,只能有一个主节点,所有的插入和查询都在这里进行。在显示的图像中,N1节点显示为主节点,并监听端口27000以进行客户端连接。所有其他节点都是从节点,它们与主节点同步,因此默认情况下也禁用了查询。只有在主节点宕机时,其中一个从节点才会接管并成为主节点。但是,可以查询从节点的数据,就像我们在图像中所示的那样;我们将在下一个示例中看到如何从从节点实例查询。
现在剩下的就是通过将我们启动的三个进程分组来配置副本集。首先定义一个 JSON 对象如下:
cfg = {
'_id':'repSetTest', 'members':[ {'_id':0, 'host': 'localhost:27000'}, {'_id':1, 'host': 'localhost:27001'}, {'_id':2, 'host': 'localhost:27002'} ]
}
有两个字段,_id和members,分别用于副本集的唯一 ID 和该副本集中 mongod 服务器进程的主机名和端口号数组。在这种情况下,使用 localhost 来引用主机并不是一个很好的主意,通常是不鼓励的;然而,在这种情况下,因为我们在同一台机器上启动了所有进程,所以可以接受。最好是通过主机名来引用主机,即使它们在 localhost 上运行。请注意,您不能在同一配置中混合使用 localhost 和主机名来引用实例。要么是主机名,要么是 localhost。然后,我们连接到三个运行中的 mongod 进程中的任何一个来配置副本集;在这种情况下,我们连接到第一个,然后从 shell 中执行以下操作:
> rs.initiate(cfg)
传递的cfg对象中的_id字段的值与我们在启动服务器进程时给--replSet选项的值相同。如果不给出相同的值,将会抛出以下错误:
{
"ok" : 0,
"errmsg" : "couldn't initiate : set name does not match the set name host Amol-PC:27000 expects"
}
如果一切顺利,初始化调用成功,我们应该在 shell 上看到类似以下 JSON 响应:
{"ok" : 1}
几秒钟后,我们应该看到从我们执行此命令的 shell 的不同提示。它现在应该成为主服务器或辅助服务器。以下是连接到副本集的主成员的 shell 的示例:
repSetTest:PRIMARY>
执行rs.status()应该给我们一些关于副本集状态的统计信息,我们将在本书的管理部分的后面的教程中深入探讨。目前,stateStr字段很重要,包含PRIMARY、SECONDARY和其他文本。
还有更多…
查看在 shell 中连接到副本集以查询和插入数据教程,以在连接到副本集后从 shell 执行更多操作。复制并不像我们在这里看到的那么简单。请参阅管理部分,了解更多关于复制的高级教程。
另请参阅
如果您想要将独立实例转换为副本集,那么具有数据的实例首先需要成为主服务器,然后将空的辅助实例添加到其中,数据将被同步。请参考以下网址以了解如何执行此操作:
docs.mongodb.org/manual/tutorial/convert-standalone-to-replica-set/
在 shell 中连接到副本集以查询和插入数据
在上一个教程中,我们启动了三个 mongod 进程的副本集。在本教程中,我们将通过使用 mongo 客户端应用程序连接到它,执行查询,插入数据,并从客户端的角度查看副本集的一些有趣方面。
准备工作
此教程的先决条件是副本集应该已经设置并运行。有关如何启动副本集的详细信息,请参考上一个教程,作为副本集的一部分启动多个实例。
如何做…
- 我们将在这里启动两个 shell,一个用于
PRIMARY,一个用于SECONDARY。在命令提示符上执行以下命令:
> mongo localhost:27000
-
shell 的提示告诉我们我们连接的服务器是
PRIMARY还是SECONDARY。它应该显示副本集的名称,后跟:,后跟服务器状态。在这种情况下,如果副本集已初始化,正在运行,我们应该看到repSetTest:PRIMARY>或repSetTest:SECONDARY>。 -
假设我们连接到的第一个服务器是一个辅助服务器,我们需要找到主服务器。在 shell 中执行
rs.status()命令,并查找stateStr字段。这应该给我们主服务器。使用 mongo shell 连接到此服务器。 -
此时,我们应该有两个运行的 shell,一个连接到主服务器,另一个连接到辅助服务器。
-
在连接到主节点的 shell 中,执行以下插入:
repSetTest:PRIMARY> db.replTest.insert({_id:1, value:'abc'})
-
这没什么特别的。我们只是在一个我们将用于复制测试的集合中插入了一个小文档。
-
通过在主服务器上执行以下查询,我们应该得到以下结果:
repSetTest:PRIMARY> db.replTest.findOne()
{ "_id" : 1, "value" : "abc" }
- 到目前为止,一切顺利。现在,我们将转到连接到
SECONDARY节点的 shell,并执行以下操作:
repSetTest:SECONDARY> db.replTest.findOne()
这样做后,我们应该在控制台上看到以下错误:
{ "$err" : "not master and slaveOk=false", "code" : 13435 }
- 现在在控制台上执行以下操作:
repSetTest:SECONDARY> rs.slaveOk(true)
- 在 shell 上再次执行我们在步骤 7 中执行的查询。现在应该得到以下结果:
repSetTest:SECONDARY>db.replTest.findOne()
{ "_id" : 1, "value" : "abc" }
- 在辅助节点上执行以下插入;它不应该成功,并显示以下消息:
repSetTest:SECONDARY> db.replTest.insert({_id:1, value:'abc'})
not master
它是如何工作的…
在这个教程中,我们做了很多事情,并且将尝试对一些重要的概念进行一些解释。
我们基本上从 shell 连接到主节点和从节点,并执行(我会说,尝试执行)选择和插入操作。Mongo 副本集的架构由一个主节点(只有一个,不多不少)和多个从节点组成。所有写操作只发生在PRIMARY上。请注意,复制不是一种分发读请求负载以实现系统扩展的机制。它的主要目的是确保数据的高可用性。默认情况下,我们不被允许从从节点读取数据。在第 6 步中,我们只是从主节点插入数据,然后执行查询以获取我们插入的文档。这很简单,与集群无关。只需注意我们是从主节点插入文档,然后再查询它。
在下一步中,我们执行相同的查询,但这次是从辅助的 shell 中执行。默认情况下,SECONDARY上未启用查询。由于要复制的数据量大、网络延迟或硬件容量等原因,可能会出现数据复制的小延迟,因此,在辅助上进行查询可能无法反映在主服务器上进行的最新插入或更新。但是,如果我们可以接受并且可以容忍数据复制中的轻微延迟,我们只需要通过执行一个命令rs.slaveOk()或rs.slaveOk(true)来显式地在SECONDARY节点上启用查询。完成此操作后,我们可以自由地在辅助节点上执行查询。
最后,我们尝试将数据插入到从节点的集合中。无论我们是否执行了rs.slaveOk(),在任何情况下都不允许这样做。当调用rs.slaveOk()时,它只允许从SECONDARY节点查询数据。所有写操作仍然必须发送到主节点,然后流向从节点。复制的内部将在管理部分的不同示例中进行介绍。
另请参阅
下一个示例,连接到副本集以从 Java 客户端查询和插入数据,是关于从 Java 客户端连接到副本集。
连接到副本集以从 Java 客户端查询和插入数据
在这个示例中,我们将演示如何从 Java 客户端连接到副本集,以及客户端如何在主节点失败时自动切换到副本集中的另一个节点。
准备工作
我们需要查看使用 Java 客户端连接到单个节点示例,因为它包含了设置 maven 和其他依赖项的所有先决条件和步骤。由于我们正在处理副本集的 Java 客户端,因此副本集必须处于运行状态。有关如何启动副本集的详细信息,请参阅作为副本集的一部分启动多个实例示例。
如何操作...
- 编写/复制以下代码片段:(此 Java 类也可从 Packt 网站下载。)
package com.packtpub.mongo.cookbook;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import com.mongodb.ServerAddress;
import java.util.Arrays;
/**
*
*/
public class ReplicaSetMongoClient {
/**
* Main method for the test client connecting to the replica set.
* @param args
*/
public static final void main(String[] args) throws Exception {
MongoClient client = new MongoClient(
Arrays.asList(
new ServerAddress("localhost", 27000), new ServerAddress("localhost", 27001), new ServerAddress("localhost", 27002)
)
);
DB testDB = client.getDB("test");
System.out.println("Dropping replTest collection");
DBCollection collection = testDB.getCollection("replTest");
collection.drop();
DBObject object = new BasicDBObject("_id", 1).append("value", "abc");
System.out.println("Adding a test document to replica set");
collection.insert(object);
System.out.println("Retrieving document from the collection, this one comes from primary node");
DBObject doc = collection.findOne();
showDocumentDetails(doc);
System.out.println("Now Retrieving documents in a loop from the collection.");
System.out.println("Stop the primary instance after few iterations ");
for(int i = 0 ; i < 10; i++) {
try {
doc = collection.findOne();
showDocumentDetails(doc);
}
catch (Exception e) {
//Ignoring or log a message
}
Thread.sleep(5000);
}
}
/**
*
* @param obj
*/
private static void showDocumentDetails(DBObject obj) {
System.out.printf("_id: %d, value is %s\n", obj.get("_id"), obj.get("value"));
}
}
- 连接到副本集中的任何节点,比如
localhost:27000,并从 shell 中执行rs.status()。记录副本集中的主实例,并从 shell 连接到它,如果localhost:27000不是主实例。在这里,切换到管理员数据库如下:
repSetTest:PRIMARY>use admin
- 现在我们从操作系统 shell 中执行前面的程序:
$ mvn compile exec:java -Dexec.mainClass=com.packtpub.mongo.cookbook.ReplicaSetMongoClient
- 通过在连接到主实例的 mongo shell 上执行以下操作来关闭主实例:
repSetTest:PRIMARY> db.shutdownServer()
- 观察在使用 maven 执行
com.packtpub.mongo.cookbook.ReplicaSetMongoClient类时控制台上的输出。
它是如何工作的...
一个有趣的事情是观察我们如何实例化MongoClient实例。它是这样做的:
MongoClient client = new MongoClient(Arrays.asList(new ServerAddress("localhost", 27000), new ServerAddress("localhost", 27001), new ServerAddress("localhost", 27002)));
构造函数接受一个com.mongodb.ServerAddress列表。这个类有很多重载的构造函数,但我们选择使用一个接受主机名和端口的构造函数。我们所做的是将副本集中的所有服务器详细信息提供为一个列表。我们没有提到什么是PRIMARY节点,什么是SECONDARY节点。MongoClient足够智能,可以弄清楚这一点,并连接到适当的实例。提供的服务器列表称为种子列表。它不一定要包含副本集中的所有服务器,尽管目标是尽可能提供尽可能多的服务器。MongoClient将从提供的子集中找出所有服务器的详细信息。例如,如果副本集有五个节点,但我们只提供了三个服务器,它也可以正常工作。连接到提供的副本集服务器后,客户端将查询它们以获取副本集元数据,并找出副本集中提供的其他服务器的其余部分。在前面的情况下,我们用三个实例实例化了客户端。如果副本集有五个成员,那么用其中的三个实例化客户端仍然是足够的,剩下的两个实例将被自动发现。
接下来,我们使用 maven 从命令提示符启动客户端。一旦客户端在循环中运行,我们关闭主实例以找到一个文档。我们应该在控制台上看到以下输出:
_id: 1, value is abc
Now Retrieving documents in a loop from the collection.
Stop the primary instance manually after few iterations
_id: 1, value is abc
_id: 1, value is abc
Nov 03, 2013 5:21:57 PM com.mongodb.ConnectionStatus$UpdatableNode update
WARNING: Server seen down: Amol-PC/192.168.1.171:27002
java.net.SocketException: Software caused connection abort: recv failed
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:150)
…
WARNING: Primary switching from Amol-PC/192.168.1.171:27002 to Amol-PC/192.168.1.171:27001
_id: 1, value is abc
正如我们所看到的,在主节点宕机时,循环中的查询被中断。然而,客户端无缝地切换到了新的主节点。嗯,几乎是无缝的,因为客户端可能需要捕获异常,并在经过预定的时间间隔后重试操作。
使用 Python 客户端连接到副本集以查询和插入数据
在这个示例中,我们将演示如何使用 Python 客户端连接到副本集,以及客户端在主节点故障时如何自动切换到副本集中的另一个节点。
准备工作
请参考使用 Python 客户端连接到单个节点示例,因为它描述了如何设置和安装 PyMongo,MongoDB 的 Python 驱动程序。此外,副本集必须处于运行状态。请参考作为副本集的一部分启动多个实例示例,了解如何启动副本集的详细信息。
操作步骤…
- 将以下代码写入/复制到
replicaset_client.py中:(此脚本也可从 Packt 网站下载。)
from __future__ import print_function
import pymongo
import time
# Instantiate MongoClient with a list of server addresses
client = pymongo.MongoClient(['localhost:27002', 'localhost:27001', 'localhost:27000'], replicaSet='repSetTest')
# Select the collection and drop it before using
collection = client.test.repTest
collection.drop()
#insert a record in
collection.insert_one(dict(name='Foo', age='30'))
for x in range(5):
try:
print('Fetching record: %s' % collection.find_one())
except Exception as e:
print('Could not connect to primary')
time.sleep(3)
- 连接到副本集中的任何节点,比如
localhost:27000,并从 shell 中执行rs.status()。记下副本集中的主实例,并从 shell 中连接到它,如果localhost:27000不是主节点。在这里,切换到管理员数据库如下:
> repSetTest:PRIMARY>use admin
- 现在,我们从操作系统 shell 中执行上述脚本如下:
$ python replicaset_client.py
- 通过在连接到主节点的 mongo shell 上执行以下操作关闭主实例:
> repSetTest:PRIMARY> db.shutdownServer()
- 观察在执行 Python 脚本的控制台上的输出。
工作原理…
您会注意到,在这个脚本中,我们通过给出主机列表而不是单个主机来实例化 mongo 客户端。从版本 3.0 开始,pymongo 驱动程序的MongoClient()类在初始化时可以接受主机列表或单个主机,并弃用了MongoReplicaSetClient()。客户端将尝试连接列表中的第一个主机,如果成功,将能够确定副本集中的其他节点。我们还专门传递了replicaSet='repSetTest'参数,确保客户端检查连接的节点是否是这个副本集的一部分。
一旦连接,我们执行正常的数据库操作,比如选择测试数据库、删除repTest集合,并向集合中插入一个文档。
接下来,我们进入一个条件循环,循环五次。每次,我们获取记录,显示它,并休眠三秒。在脚本处于此循环时,我们关闭副本集中的主节点,如步骤 4 中所述。我们应该看到类似于以下的输出:
Fetching record: {u'age': u'30', u'_id': ObjectId('5558bfaa0640fd1923fce1a1'), u'name': u'Foo'}
Fetching record: {u'age': u'30', u'_id': ObjectId('5558bfaa0640fd1923fce1a1'), u'name': u'Foo'}
Fetching record: {u'age': u'30', u'_id': ObjectId('5558bfaa0640fd1923fce1a1'), u'name': u'Foo'}
Could not connect to primary
Fetching record: {u'age': u'30', u'_id': ObjectId('5558bfaa0640fd1923fce1a1'), u'name': u'Foo'}
在上述输出中,客户端在主节点中途断开连接。然而,很快,剩余节点选择了一个新的主节点,mongo 客户端能够恢复连接。
启动由两个分片组成的简单分片环境
在这个配方中,我们将建立一个由两个数据分片组成的简单分片设置。由于这是最基本的分片设置来演示概念,因此不会配置任何复制。我们不会深入研究分片的内部结构,这将在管理部分中更多地探讨。
在我们继续之前,这里有一点理论。可伸缩性和可用性是构建任何关键任务应用程序的两个重要基石。可用性是由我们在本章前面的配方中讨论的副本集来处理的。现在让我们来看看可伸缩性。简单地说,可伸缩性是系统应对不断增长的数据和请求负载的能力。考虑一个电子商务平台。在正常的日子里,对网站的点击次数和负载都相当适度,系统的响应时间和错误率都很低。(这是主观的。)现在,考虑系统负载变成平常日负载的两倍、三倍,甚至更多,比如感恩节、圣诞节等。如果平台能够在这些高负载日提供与任何其他日子相似的服务水平,系统就被认为已经很好地应对了请求数量的突然增加。
现在,考虑一个需要存储过去十年中击中特定网站的所有请求的详细信息的归档应用程序。对于击中网站的每个请求,我们在底层数据存储中创建一个新记录。假设每个记录的大小为 250 字节,平均每天有 300 万个请求,我们将在大约五年内超过 1 TB 的数据标记。这些数据将用于各种分析目的,并可能经常被查询。当数据量增加时,查询性能不应受到严重影响。如果系统能够应对不断增长的数据量,并且在数据量较低时仍能提供与低数据量时相当的性能,系统就被认为已经很好地扩展了。
现在我们简要地了解了可伸缩性是什么,让我告诉你,分片是一种机制,让系统能够满足不断增长的需求。关键在于整个数据被分成更小的段,并分布在称为分片的各个节点上。假设我们在 mongo 集合中有 1000 万个文档。如果我们将这个集合分片到 10 个分片上,那么理想情况下每个分片上将有10,000,000/10 = 1,000,000个文档。在任何给定的时间点,只有一个文档会驻留在一个分片上(这本身将是生产系统中的一个副本集)。然而,有一些魔法使这个概念隐藏在查询集合的开发人员之外,无论分片的数量如何,他们都会得到一个统一的集合视图。根据查询,mongo 决定查询哪个分片的数据并返回整个结果集。有了这个背景,让我们建立一个简单的分片并仔细研究它。
准备就绪
除了已经安装的 MongoDB 服务器,从软件角度来看,没有其他先决条件。我们将创建两个数据目录,一个用于每个分片。将有一个用于数据和一个用于日志的目录。
如何做到这一点...
-
我们首先创建日志和数据的目录。创建以下目录,
/data/s1/db,/data/s2/db和/logs。在 Windows 上,我们可以有c:\data\s1\db等等用于数据和日志目录。在分片环境中还有一个用于存储一些元数据的配置服务器。我们将使用/data/con1/db作为配置服务器的数据目录。 -
启动以下 mongod 进程,一个用于两个分片中的每一个,一个用于配置数据库,一个用于 mongos 进程。对于 Windows 平台,跳过
--fork参数,因为它不受支持。
$ mongod --shardsvr --dbpath /data/s1/db --port 27000 --logpath /logs/s1.log --smallfiles --oplogSize 128 --fork
$ mongod --shardsvr --dbpath /data/s2/db --port 27001 --logpath /logs/s2.log --smallfiles --oplogSize 128 --fork
$ mongod --configsvr --dbpath /data/con1/db --port 25000 --logpath /logs/config.log --fork
$ mongos --configdb localhost:25000 --logpath /logs/mongos.log --fork
- 从命令提示符中执行以下命令。这应该显示一个 mongos 提示,如下所示:
$ mongo
MongoDB shell version: 3.0.2
connecting to: test
mongos>
- 最后,我们设置分片。从 mongos shell 中,执行以下两个命令:
mongos> sh.addShard("localhost:27000")
mongos> sh.addShard("localhost:27001")
- 在每次添加分片时,我们应该收到一个 ok 回复。应该看到以下 JSON 消息,为每个添加的分片提供唯一 ID:
{ "shardAdded" : "shard0000", "ok" : 1 }
注意
我们在所有地方都使用 localhost 来引用本地运行的服务器。这不是一种推荐的方法,也是不鼓励的。更好的方法是使用主机名,即使它们是本地进程。
它是如何工作的…
让我们看看我们在这个过程中做了什么。我们为数据创建了三个目录(两个用于分片,一个用于配置数据库)和一个日志目录。我们也可以有一个 shell 脚本或批处理文件来创建这些目录。事实上,在大型生产部署中,手动设置分片不仅耗时,而且容易出错。
提示
下载示例代码
您可以从您在www.packtpub.com的帐户中购买的所有 Packt 图书下载示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接发送到您的邮箱。
让我们试着了解我们到底做了什么,以及我们试图实现什么。以下是我们刚刚设置的分片设置的图像:

如果我们看一下前面的图像和第 2 步中启动的服务器,我们有分片服务器,它们将在集合中存储实际数据。这是我们启动的四个进程中的前两个,它们监听端口27000和27001。接下来,我们启动了一个配置服务器,在这个图像的左侧可以看到。这是第 2 步中启动的四个服务器中的第三个服务器,它监听端口25000以进行传入连接。这个数据库的唯一目的是维护有关分片服务器的元数据。理想情况下,只有 mongos 进程或驱动程序连接到此服务器以获取有关分片的详细信息/元数据和分片键信息。我们将在下一个示例中看到分片键是什么,我们将在其中操作一个分片集合并查看我们创建的分片的操作。
最后,我们有一个 mongos 进程。这是一个轻量级的进程,不做任何数据持久化,只接受来自客户端的连接。这是一个作为网关的层,将客户端与分片的概念抽象出来。现在,我们可以将其视为基本上是一个路由器,它会查询配置服务器并决定将客户端的查询路由到适当的分片服务器以执行。然后,如果适用,它会聚合来自各个分片的结果并将结果返回给客户端。可以肯定地说,没有客户端直接连接到配置或分片服务器;事实上,除了一些管理操作外,理想情况下没有人应该直接连接到这些进程。客户端只需连接到 mongos 进程并执行他们的查询和插入或更新操作。
仅仅启动碎片服务器、配置服务器和 mongos 进程并不能创建一个分片化的环境。在启动 mongos 进程时,我们提供了配置服务器的详细信息。那么存储实际数据的两个碎片怎么办?然而,作为碎片服务器启动的两个 mongod 进程尚未在配置中声明为碎片服务器。这正是我们在最后一步中通过为两个碎片服务器调用 sh.addShard() 来完成的。在启动时,mongos 进程提供了配置服务器的详细信息。从 shell 中添加碎片将存储关于碎片的元数据在配置数据库中,并且 mongos 进程随后将查询此配置数据库以获取碎片的信息。执行示例的所有步骤后,我们将得到一个操作中的碎片,如下所示:

在我们结束之前,我们在这里设置的碎片远非理想,也不是在生产环境中的操作方式。前面的图片给了我们一个关于生产环境中典型碎片的想法。碎片的数量不会是两个,而是更多。此外,每个碎片将是一个副本集,以确保高可用性。将有三个配置服务器来确保配置服务器的可用性。同样,将创建任意数量的用于监听客户端连接的碎片的 mongos 进程。在某些情况下,甚至可以在客户端应用程序的服务器上启动。
还有更多…
除非我们将碎片投入使用并从 shell 中插入和查询数据,否则碎片有何用处?在下一个示例中,我们将利用这里的碎片设置,添加一些数据,并查看其运行情况。
在 shell 中连接到一个碎片并执行操作
在这个示例中,我们将从命令提示符连接到一个碎片,看看如何为一个集合分片,并观察一些测试数据的分割情况。
准备就绪
显然,我们需要一个运行中的分片化 mongo 服务器设置。有关如何设置简单碎片的更多详细信息,请参阅上一个示例,启动由两个碎片组成的简单分片环境。mongos 进程,如上一个示例中所述,应该监听端口号 27017。我们在一个名为 names.js 的 JavaScript 文件中得到了一些名称。这个文件需要从 Packt 网站下载并保存在本地文件系统上。该文件包含一个名为 names 的变量,其值是一个包含一些 JSON 文档的数组,每个文档代表一个人。内容如下:
names = [
{name:'James Smith', age:30},
{name:'Robert Johnson', age:22},
…
]
操作步骤…
- 启动 mongo shell 并连接到本地主机上的默认端口,如下所示。这将确保名称在当前 shell 中可用:
mongo --shell names.js
MongoDB shell version: 3.0.2
connecting to: test
mongos>
- 切换到将用于测试分片的数据库;我们称之为
shardDB:
mongos> use shardDB
- 在数据库级别启用分片如下:
mongos> sh.enableSharding("shardDB")
- 如下所示为一个名为
person的集合分片:
mongos>sh.shardCollection("shardDB.person", {name: "hashed"}, false)
- 将测试数据添加到分片集合中:
mongos> for(i = 1; i <= 300000 ; i++) {
... person = names[Math.round(Math.random() * 100) % 20]
... doc = {_id:i, name:person.name, age:person.age}
... db.person.insert(doc)
}
- 执行以下操作以获取查询计划和每个碎片上的文档数量:
mongos> db.person.getShardDistribution()
工作原理…
这个示例需要一些解释。我们下载了一个 JavaScript 文件,其中定义了一个包含 20 个人的数组。数组的每个元素都是一个具有 name 和 age 属性的 JSON 对象。我们启动 shell 连接到加载了这个 JavaScript 文件的 mongos 进程。然后切换到我们用于分片目的的 shardDB。
要使集合分片化,首先需要为将创建集合的数据库启用分片。我们使用 sh.enableSharding() 来实现这一点。
下一步是启用集合进行分片。默认情况下,所有数据将保存在一个分片上,而不会分散在不同的分片上。想想看;Mongo 如何能够有意义地分割数据?整个意图是有意义地分割数据,并尽可能均匀地分割,以便每当我们基于分片键进行查询时,Mongo 都能轻松地确定要查询哪个分片。如果查询不包含分片键,查询将在所有分片上执行,然后数据将由 mongos 进程汇总后返回给客户端。因此,选择正确的分片键非常关键。
现在让我们看看如何对集合进行分片。我们通过调用sh.shardCollection("shardDB.person", {name: "hashed"}, false)来实现这一点。这里有三个参数:
-
shardCollection方法的第一个参数是<db name>.<collection name>格式的集合的完全限定名称。 -
第二个参数是集合中用于分片的字段名称。这是用于在分片上拆分文档的字段。一个好的分片键的要求之一是它应该具有很高的基数(可能值的数量应该很高)。在我们的测试数据中,名称值的基数非常低,因此不是一个好的分片键选择。当使用此作为分片键时,我们对此键进行哈希。我们通过将键标记为
{name: "hashed"}来实现这一点。 -
最后一个参数指定用作分片键的值是否是唯一的。名称字段肯定不是唯一的,因此它将是 false。如果该字段是,比如说,人的社会安全号码,它可以被设置为 true。此外,社会安全号码是一个很好的分片键选择,因为它的基数很高。请记住,分片键必须存在才能使查询有效。
最后一步是查看查找所有数据的执行计划。此操作的目的是查看数据如何分布在两个分片上。对于 30 万个文档,我们期望每个分片大约有 15 万个文档。然而,从分布统计数据中,我们可以观察到shard0000有1,49,715个文档,而shard0001有150285个文档:
Shard shard0000 at localhost:27000
data : 15.99MiB docs : 149715 chunks : 2
estimated data per chunk : 7.99MiB
estimated docs per chunk : 74857
Shard shard0001 at localhost:27001
data : 16.05MiB docs : 150285 chunks : 2
estimated data per chunk : 8.02MiB
estimated docs per chunk : 75142
Totals
data : 32.04MiB docs : 300000 chunks : 4
Shard shard0000 contains 49.9% data, 49.9% docs in cluster, avg obj size on shard : 112B
Shard shard0001 contains 50.09% data, 50.09% docs in cluster, avg obj size on shard : 112B
我建议您做一些额外的建议。
从 mongo shell 连接到各个分片,并在 person 集合上执行查询。查看这些集合中的计数是否与前面的计划中看到的相似。此外,可以发现没有文档同时存在于两个分片上。
我们简要讨论了基数如何影响数据在分片上的分布方式。让我们做一个简单的练习。我们首先删除 person 集合,然后再次执行 shardCollection 操作,但这次使用{name: 1}分片键,而不是{name: "hashed"}。这确保分片键不被哈希并按原样存储。现在,使用我们在第 5 步中使用的 JavaScript 函数加载数据,然后在数据加载后对集合执行explain()命令。观察数据如何在分片上分割(或不分割)。
还有更多...
现在一定会有很多问题涌现出来,比如什么是最佳实践?有什么技巧和窍门?MongoDB 在幕后是如何实现分片的,以使其对最终用户透明呢?
这里的配方只解释了基础知识。在管理部分,所有这些问题都将得到解答。
第二章:命令行操作和索引
在本章中,我们将涵盖以下主题:
-
创建测试数据
-
从 Mongo shell 执行简单的查询、投影和分页
-
从 shell 中更新和删除数据
-
创建索引并查看查询计划
-
在 shell 中创建背景和前景索引
-
创建和理解稀疏索引
-
使用 TTL 索引在固定间隔后过期文档
-
使用 TTL 索引在给定时间过期文档
介绍
在本章中,我们将使用 mongo shell 执行简单的查询。在本章后面,我们将详细了解常用的 MongoDB 索引。
创建测试数据
这个配方是为本章的一些配方以及本书后面的章节创建测试数据。我们将演示如何使用 mongo 导入实用程序将 CSV 文件加载到 mongo 数据库中。这是一个基本的配方,如果读者了解数据导入实用程序,他们可以直接从 Packt 网站下载 CSV 文件(pincodes.csv),自己将其加载到集合中,并跳过其余的配方。我们将使用默认数据库test,集合将被命名为postalCodes。
准备工作
这里使用的数据是印度的邮政编码。从 Packt 网站下载pincodes.csv文件。该文件是一个包含 39,732 条记录的 CSV 文件;成功导入后应该创建 39,732 个文档。我们需要让 Mongo 服务器处于运行状态。参考第一章中的安装单节点 MongoDB配方,了解如何启动服务器的说明。服务器应该开始监听默认端口27017上的连接。
如何做…
- 从 shell 中使用以下命令执行要导入的文件:
$ mongoimport --type csv -d test -c postalCodes --headerline --drop pincodes.csv
-
通过在命令提示符上输入
mongo来启动 mongo shell。 -
在 shell 中,执行以下命令:
> db.postalCodes.count()
它是如何工作的…
假设服务器正在运行,CSV 文件已经下载并保存在本地目录中,我们在其中执行导入实用程序。让我们看看mongoimport实用程序中给出的选项及其含义:
| 命令行选项 | 描述 |
|---|---|
--type |
这指定输入文件的类型为 CSV。它默认为 JSON;另一个可能的值是 TSV。 |
-d |
这是要加载数据的目标数据库。 |
-c |
这是前面提到的数据库中要加载数据的集合。 |
--headerline |
这只在 TSV 或 CSV 文件的情况下相关。它指示文件的第一行是标题。相同的名称将用作文档中字段的名称。 |
--drop |
在导入数据之前删除集合。 |
在给出所有选项后,命令提示符上的最终值是文件名pincodes.csv。
如果导入成功,您应该在控制台上看到类似以下内容的输出:
2015-05-19T06:51:54.131+0000 connected to: localhost
2015-05-19T06:51:54.132+0000 dropping: test.postalCodes
2015-05-19T06:51:54.810+0000 imported 39732 documents
最后,我们启动 mongo shell 并查找集合中文档的计数;正如在前面的导入日志中所看到的,它应该确实是 39,732。
注意
邮政编码数据来自github.com/kishorek/India-Codes/。这些数据不是来自官方来源,可能不准确,因为它是手动编译的,供公众免费使用。
另请参阅
在 Mongo shell 中执行简单的查询、投影和分页配方是关于在导入的数据上执行一些基本查询。
从 Mongo shell 执行简单的查询、投影和分页
在这个配方中,我们将通过一些查询来选择我们在前一个配方创建测试数据中设置的测试数据中的文档。在这个配方中没有什么奢侈的东西,熟悉查询语言基础知识的人可以跳过这个配方。其他不太熟悉基本查询或想要进行小小复习的人可以继续阅读配方的下一部分。此外,这个配方旨在让您感受到前一个配方中设置的测试数据。
准备工作
要执行简单的查询,我们需要有一个正在运行的服务器。一个简单的单节点就是我们需要的。请参考第一章中的安装单节点 MongoDB配方,了解如何启动服务器的说明。我们将要操作的数据需要导入到数据库中。导入数据的步骤在前一个配方创建测试数据中给出。您还需要启动 mongo shell 并连接到在本地主机上运行的服务器。一旦这些先决条件完成,我们就可以开始了。
如何做…
- 让我们首先找到集合中文档的数量:
> db.postalCodes.count()
- 让我们从
postalCodes集合中找到一个文档:
> db.postalCodes.findOne()
- 现在,我们按如下方式在集合中找到多个文档:
> db.postalCodes.find().pretty()
- 前面的查询检索了前 20 个文档的所有键,并在 shell 上显示它们。在结果的末尾,您会注意到一行,上面写着
键入"it"以获取更多内容。通过键入"it",mongo shell 将遍历结果游标。现在让我们做一些事情;我们将只显示city、state和pincode字段。此外,我们想显示集合中编号为 91 到 100 的文档。让我们看看如何做到这一点:
> db.postalCodes.find({}, {_id:0, city:1, state:1, pincode:1}).skip(90).limit(10)
- 让我们再进一步,编写一个稍微复杂的查询,在其中按照城市名称找到古吉拉特邦的前 10 个城市,并且与上一个查询类似,我们只选择
city、state和pincode字段:
> db.postalCodes.find({state:'Gujarat'},{_id:0, city:1, state:1, pincode:1}).sort({city:1}).limit(10)
工作原理…
这个配方非常简单,让我们感受到了我们在前一个配方中设置的测试数据。尽管如此,和其他配方一样,我确实需要向大家解释一下我们在这里做了什么。
我们首先使用db.postalCodes.count()找到了集合中文档的数量,应该有 39,732 个文档。这应该与我们在导入邮政编码集合数据时看到的日志保持一致。接下来,我们使用findOne从集合中查询一个文档。这个方法返回查询结果集中的第一个文档。在没有查询或排序顺序的情况下,就像在这种情况下一样,它将是按其自然顺序排序的集合中的第一个文档。
接下来,我们执行find而不是findOne。它们之间的区别在于find操作返回结果集的迭代器,我们可以使用它来遍历find操作的结果,而findOne返回一个文档。对find操作添加一个 pretty 方法调用将以漂亮或格式化的方式打印结果。
提示
请注意,pretty方法只对find有效,而对findOne无效。这是因为findOne的返回值是一个文档,而返回的文档上没有pretty操作。
现在我们将在 mongo shell 上执行以下查询:
> db.postalCodes.find({}, {_id:0, city:1, state:1, pincode:1}).skip(90).limit(10)
在这里,我们向find方法传递了两个参数:
-
第一个是
{},这是选择文档的查询,在这种情况下,我们要求 mongo 选择所有文档。 -
第二个参数是我们想要在结果文档中的字段集,也被称为投影。请记住,
_id字段默认存在,除非我们明确指定_id:0。对于所有其他字段,我们需要说<field_name>:1或<field_name>:true。具有投影的查找部分与在关系世界中说select field1``, field2 from table是一样的,而不指定要选择的字段在查找中说select * from table在关系世界中。
接下来,我们只需要看一下skip和limit的作用:
-
skip函数从结果集中跳过给定数量的文档,直到最后一个文档 -
limit函数然后将结果限制为给定数量的文档
让我们通过一个例子来看看这意味着什么。通过执行.skip(90).limit(10),我们说我们要跳过结果集中的前 90 个文档,并从第 91 个文档开始返回。然而,limit 表示我们将只从第 91 个文档返回 10 个文档。
现在,这里有一些边界条件,我们需要知道。如果 skip 提供的值大于集合中的文档总数会怎么样?在这种情况下,将不会返回任何文档。此外,如果提供给 limit 函数的数字大于集合中剩余的实际文档数量,则返回的文档数量将与集合中剩余的文档数量相同,并且在任一情况下都不会抛出异常。
从 shell 更新和删除数据
这将是一个简单的示例,将在测试集合上执行删除和更新。我们不会处理导入的相同测试数据,因为我们不想更新/删除任何数据,而是我们将在仅为此示例创建的测试集合上工作。
准备工作
对于此示例,我们将创建一个名为updAndDelTest的集合。我们需要服务器运行。有关如何启动服务器的说明,请参阅第一章中的安装单节点 MongoDB示例,安装和启动服务器。使用加载了UpdAndDelTest.js脚本的 shell 启动。此脚本可在 Packt 网站上下载。要了解如何使用预加载的脚本启动 shell,请参阅第一章中的使用 JavaScript 连接 Mongo shell 中的单个节点示例,安装和启动服务器。
操作步骤…
- 启动 MongoDB shell 并预加载脚本:
$ mongo --shell updAndDelTest.js
- 使用启动的 shell 和加载的脚本,在 shell 中执行以下操作:
> prepareTestData()
-
如果一切顺利,您应该在控制台上看到
在 updAndDelTest 中插入了 20 个文档的打印: -
为了了解集合的情况,让我们查询如下:
> db.updAndDelTest.find({}, {_id:0})
-
我们应该看到对于
x的每个值为1和2,我们有y从 1 到 10 递增的值。 -
我们将首先更新一些文档并观察结果。执行以下更新:
> db.updAndDelTest.update({x:1}, {$set:{y:0}})
- 执行以下
find命令并观察结果;我们应该得到 10 个文档。对于每个文档,注意y的值。
> db.updAndDelTest.find({x:1}, {_id:0})
- 我们现在将执行以下更新:
> db.updAndDelTest.update({x:1}, {$set:{y:0}}, {multi:true})
-
再次执行步骤 6 中给出的查询以查看更新后的文档。它将显示我们之前看到的相同文档。再次注意
y的值,并将其与我们上次执行此查询之前执行步骤 7 中给出的更新时看到的结果进行比较。 -
我们现在将看看删除是如何工作的。我们将再次选择
x为1的文档进行删除测试。让我们从集合中删除所有x为1的文档:
> db.updAndDelTest.remove({x:1})
- 执行以下
find命令并观察结果。我们将不会得到任何结果。似乎remove操作已删除所有x为1的文档。
> db.updAndDelTest.find({x:1}, {_id:0})
注意
当您在 mongo shell 中,并且想要查看函数的源代码时,只需输入函数名称而不带括号。例如,在这个示例中,我们可以通过输入函数名称prepareTestData(不带括号)来查看我们自定义函数的代码,并按下Enter键。
它是如何工作的...
首先,我们设置将用于更新和删除test的数据。我们已经看到了数据并知道它是什么。一个有趣的观察是,当我们执行更新操作,比如db.updAndDelTest.update({x:1}, {$set:{y:0}}),它只会更新与作为第一个参数提供的查询匹配的第一个文档。这是我们在此更新后查询集合时将观察到的事情。更新函数的格式如下:db.<collection name>.update(query, update object, {upsert: <boolean>, multi:<boolean>})。
我们将在后面的示例中看到 upsert 是什么。multi 参数默认设置为false。这意味着update方法不会更新多个文档;只有第一个匹配的文档会被更新。然而,当我们使用db.updAndDelTest.update({x:1}, {$set:{y:0}}, {multi:true})并将 multi 设置为true时,集合中匹配给定查询的所有文档都会被更新。这是我们在查询集合后可以验证的事情。
另一方面,删除的行为不同。默认情况下,remove操作会删除所有与提供的查询匹配的文档。然而,如果我们只想删除一个文档,我们可以将第二个参数明确传递为true。
注意
更新和删除的默认行为是不同的。默认情况下,update调用只会更新第一个匹配的文档,而remove会删除与查询匹配的所有文档。
创建索引并查看查询计划
在这个示例中,我们将查看如何查询数据,通过解释查询计划来分析其性能,然后通过创建索引来优化它。
准备工作
对于索引的创建,我们需要运行一个服务器。一个简单的单节点就是我们需要的。请参考第一章中的安装单节点 MongoDB示例,了解如何启动服务器的说明。我们将要操作的数据需要导入到数据库中。导入数据的步骤在前一个示例创建测试数据中给出。一旦这个先决条件完成,我们就可以开始了。
如何做...
我们正在尝试编写一个查询,该查询将找到给定州中的所有邮政编码。
- 执行以下查询以查看此查询的计划:
> db.postalCodes.find({state:'Maharashtra'}).explain('executionStats')
在解释计划操作的结果中,注意以下字段:stage、nReturned、totalDocsExamined、docsExamined和executionTimeMillis。
- 让我们再次执行相同的查询,但这次,我们将结果限制为仅 100 个结果:
> db.postalCodes.find({state:'Maharashtra'}).limit(100).explain()
-
在结果中注意以下字段:
nReturned、totalDocsExamined、docsExamined和executionTimeMillis。 -
我们现在在
state和pincode字段上创建索引,如下所示:
> db.postalCodes.createIndex({state:1, pincode:1})
- 执行以下查询:
> db.postalCodes.find({state:'Maharashtra'}).explain()
注意以下字段:stage、nReturned、totalDocsExamined、docsExamined和executionTimeMillis。
- 因为我们只想要邮政编码,所以我们修改查询如下并查看其计划:
> db.postalCodes.find({state:'Maharashtra'}, {pincode:1, _id:0}).explain()
在结果中注意以下字段:stage、nReturned、totalDocsExamined、docsExamined和executionTimeMillis。
它是如何工作的...
这里有很多要解释的地方。我们将首先讨论我们刚刚做的事情以及如何分析统计数据。接下来,我们将讨论索引创建时需要注意的一些要点和一些注意事项。
分析计划
好的,让我们看看我们执行的第一步并分析输出:
db.postalCodes.find({state:'Maharashtra'}).explain()
在我的机器上的输出如下:(我现在跳过了不相关的字段。)
{
"stage" : "COLLSCAN",
...
"nReturned" : 6446,
"totalDocsExamined " : 39732,
…
"docsExamined" : 39732,
…
"executionTimeMillis" : 12,
…
}
结果中stage字段的值为COLLSCAN,这意味着为了在整个集合中搜索匹配的文档,进行了完整的集合扫描(所有文档一个接一个地扫描)。nReturned的值为6446,这是与查询匹配的结果数量。totalDocsExamined和docsExamined字段的值为39,732,这是扫描集合以检索结果的文档数量。这也是集合中存在的文档总数,所有文档都被扫描以获取结果。最后,executionTimeMillis是检索结果所用的毫秒数。
提高查询执行时间
到目前为止,就性能而言,查询看起来并不太好,有很大的改进空间。为了演示应用于查询的限制如何影响查询计划,我们可以再次找到没有索引但有限制子句的查询计划,如下所示:
> db.postalCodes.find({state:'Maharashtra'}).limit(100).explain()
{
"stage" : "COLLSCAN",…
"nReturned" : 100,
"totalDocsExamined" : 19951,
…
"docsExamined" : 19951,
…
"executionTimeMillis" : 8,
…
}
这次的查询计划很有趣。虽然我们仍然没有创建索引,但我们确实看到了查询执行所需的时间和检索结果所需的对象数量有所改善。这是因为一旦达到了limit函数中指定的文档数量,mongo 就会忽略剩余文档的扫描。因此,我们可以得出结论,建议您使用limit函数来限制结果的数量,其中已知要访问的文档数量是有限的。这可能会提高查询性能。可能这个词很重要,因为在没有索引的情况下,如果匹配的文档数量不足,集合仍然可能被完全扫描。
使用索引进行改进
接着,我们在 state 和 pincode 字段上创建了一个复合索引。在这种情况下,索引的顺序是升序(因为值为 1),除非我们计划执行多键排序,否则这并不重要。这是一个决定性因素,决定了结果是否可以仅使用索引进行排序,还是 mongo 需要在返回结果之前在内存中对其进行排序。就查询计划而言,我们可以看到有了显著的改进:
{
"executionStages" : {
"stage" : "FETCH",
…
"inputStage" : {
"stage" : "IXSCAN",
…
"nReturned" : 6446,
"totalDocsExamined" : 6446,
"docsExamined" : 6446,
…
"executionTimeMillis" : 4,
…
}
inputStage字段现在具有IXSCAN值,这表明现在确实使用了索引。结果的数量保持不变,仍为6446。在索引中扫描的对象数量和集合中扫描的文档数量现在已经减少到与结果中的文档数量相同。这是因为我们现在使用了一个索引,它给出了我们要扫描的起始文档,然后只扫描所需数量的文档。这类似于使用书的索引查找单词或扫描整本书以搜索单词。如预期的那样,executionTimeMillis中的时间也减少了。
使用覆盖索引进行改进
这留下了一个字段,executionStages,它是FETCH,我们将看看这意味着什么。要了解这个值是什么,我们需要简要了解索引是如何操作的。
索引存储了集合中原始文档的字段子集。索引中存在的字段与创建索引的字段相同。然而,这些字段按照在索引创建期间指定的顺序在索引中保持排序。除了字段之外,索引中还存储了一个额外的值,作为指向集合中原始文档的指针。因此,每当用户执行查询时,如果查询包含索引存在的字段,就会查询索引以获取一组匹配项。然后,与查询匹配的索引条目一起存储的指针被用于进行另一个 IO 操作,以从集合中获取完整的文档,然后返回给用户。
executionStages的值为FETCH,表示用户在查询中请求的数据并不完全存在于索引中,而是需要进行额外的 IO 操作,从索引指向的集合中检索整个文档。如果值本身存在于索引中,则不需要额外的操作来从集合中检索文档,而是会返回索引中的数据。这称为覆盖索引,在这种情况下,executionStages的值将是IXSCAN。
在我们的情况下,我们只需要邮政编码。那么,为什么不在我们的查询中使用投影来检索我们需要的内容呢?这也会使索引成为覆盖索引,因为索引条目只包含州名和邮政编码,所需的数据可以完全提供,而无需从集合中检索原始文档。在这种情况下,查询的计划也很有趣。
执行以下命令:
db.postalCodes.find({state:'Maharashtra'}, {pincode:1, _id:0}).explain()
这给我们带来了以下计划:
{
"executionStages" : {
"stage" : "PROJECTION",
…
"inputStage" : {
"stage" : "IXSCAN",
…
"nReturned" : 6446,
"totalDocsExamined" : 0,
"totalKeysExamined": 6446
"executionTimeMillis" : 4,
…
}
观察totalDocsExamined和executionStage: PROJECTION字段的值。如预期的那样,我们在投影中请求的数据可以仅从索引中提供。在这种情况下,我们扫描了索引中的 6446 个条目,因此totalKeysExamined的值为6446。
由于整个结果都是从索引中获取的,我们的查询没有从集合中获取任何文档。因此,totalDocsExamined的值为0。
由于这个集合很小,我们没有看到查询执行时间的显着差异。这在更大的集合上将更加明显。使用索引是很好的,可以给我们良好的性能。使用覆盖索引可以给我们更好的性能。
注意
MongoDB 版本 3.0 的解释结果功能进行了重大改进。我建议花几分钟阅读其文档:docs.mongodb.org/manual/reference/explain-results/。
还要记住的一件事是,如果您的文档有很多字段,请尝试使用投影仅检索我们需要的字段数量。默认情况下,_id字段会被检索。除非我们打算使用它,否则将_id:0设置为不检索它,如果它不是索引的一部分。执行覆盖查询是查询集合的最有效方式。
索引创建的一些注意事项
现在我们将看到索引创建中的一些陷阱以及在索引中使用数组字段时的一些事实。
一些不高效使用索引的运算符是$where,$nin和$exists运算符。每当在查询中使用这些运算符时,应该牢记,当数据量增加时可能会出现性能瓶颈。
同样,$in运算符必须优先于$or运算符,因为两者都可以用来实现或多或少相同的结果。作为练习,尝试在postalCodes集合中找到马哈拉施特拉邦和古吉拉特邦的邮政编码。编写两个查询:一个使用$or,一个使用$in运算符。解释这两个查询的计划。
当数组字段用于索引时会发生什么?
Mongo 为文档的数组字段中的每个元素创建一个索引条目。因此,如果文档的数组中有 10 个元素,将会有 10 个索引条目,每个数组中的元素都有一个。然而,在创建包含数组字段的索引时有一个约束。在使用多个字段创建索引时,不能有超过一个字段是数组类型,这是为了防止在数组中添加一个元素时可能导致索引数量激增。如果我们仔细考虑一下,我们会发现每个数组元素都会创建一个索引条目。如果允许多个数组字段成为索引的一部分,那么索引中的条目数量将会很大,这将是这些数组字段长度的乘积。例如,如果一个文档添加了两个长度为 10 的数组字段,如果允许使用这两个数组字段创建一个索引,将会向索引中添加 100 个条目。
这应该足够了,现在,来初步了解一个简单的、普通的索引。在接下来的几个配方中,我们将看到更多的选项和类型。
在 shell 中创建后台和前台索引
在我们之前的配方中,我们看到了如何分析查询,如何决定需要创建什么索引,以及如何创建索引。这本身是直接的,看起来相当简单。然而,对于大型集合,随着索引创建时间的增加,情况开始变得更糟。这个配方的目标是为这些概念扔一些光,避免在创建索引时遇到这些陷阱,特别是在大型集合上。
准备工作
为了创建索引,我们需要一个正在运行的服务器。一个简单的单节点就是我们需要的。请参考第一章中的安装单节点 MongoDB配方,了解如何启动服务器的说明。
通过在操作系统 shell 中键入mongo来连接两个 shell 到服务器。它们都将默认连接到test数据库。
我们的邮政编码测试数据太小,无法展示在大型集合上创建索引时遇到的问题。我们需要更多的数据,因此,我们将开始创建一些数据来模拟在创建索引过程中遇到的问题。这些数据没有实际意义,但足够测试概念。在其中一个已启动的 shell 中复制以下内容并执行:(这是一个相当容易输入的片段。)
for(i = 0; i < 5000000 ; i++) {
doc = {}
doc._id = i
doc.value = 'Some text with no meaning and number ' + i + ' in between'
db.indexTest.insert(doc)
}
这个集合中的文档看起来是这样的:
{ _id:0, value:"Some text with no meaning and number 0 in between" }
执行将需要相当长的时间,所以我们需要耐心等待。一旦执行完成,我们就可以开始操作了。
注意
如果你想知道集合中当前加载的文档数量,可以定期从第二个 shell 中评估以下内容:
db.indexTest.count()
如何做…
- 在文档的
value字段上创建索引,如下所示:
> db.indexTest.createIndex({value:1})
- 在索引创建过程中,这应该需要相当长的时间,切换到第二个控制台并执行以下操作:
> db.indexTest.findOne()
-
索引创建 shell 和我们执行
findOne的 shell 都将被阻塞,直到索引创建完成,两者都不会显示提示。 -
现在,这是默认的前台索引创建。我们想看看后台索引创建的行为。按照以下方式删除已创建的索引:
> db.indexTest.dropIndex({value:1})
- 再次创建索引,但这次是在后台进行,如下所示:
> db.indexTest.createIndex({value:1}, {background:true})
- 在第二个 mongo shell 中,执行以下
findOne:
> db.indexTest.findOne()
-
这应该返回一个文档,这与第一个实例不同,那里的操作会一直被阻塞,直到前台的索引创建完成。
-
在第二个 shell 中,反复执行以下解释操作,每次解释计划调用之间间隔四到五秒,直到索引创建过程完成:
> db.indexTest.find({value:"Some text with no meaning and number 0 in between"}).explain()
它是如何工作的…
让我们现在分析一下我们刚刚做的事情。我们创建了大约五百万个没有实际重要性的文档,但我们只是想获取一些数据,这将花费大量时间来构建索引。
索引可以通过两种方式构建,前台和后台。在任何一种情况下,shell 在createIndex操作完成之前都不会显示提示,并且会阻塞所有操作,直到索引创建完成。为了说明前台和后台索引创建之间的区别,我们执行了第二个 mongo shell。
我们首先在前台创建了索引,这是默认行为。这个索引构建不允许我们查询集合(从第二个 shell)直到索引构建完成。findOne操作在整个索引构建完成之前(从第一个 shell)都会被阻塞。另一方面,在后台构建的索引不会阻塞findOne操作。如果您想在索引构建过程中尝试向集合中插入新文档,这应该能很好地工作。随时删除索引并在后台重新创建它,同时向indexTest集合中插入一个文档,您会注意到它可以顺利进行。
那么,这两种方法之间有什么区别,为什么不总是在后台构建索引?除了作为第二个参数传递给createIndex调用的额外参数{background:true}(也可以是{background:1})之外,还有一些区别。后台索引创建过程将比前台创建的索引稍慢。此外,在内部——虽然与最终用户无关——在前台创建的索引将比在后台创建的索引更紧凑。
除此之外,没有其他显著的区别。实际上,如果系统正在运行并且需要在为最终用户提供服务时创建索引(虽然不建议,但有时可能需要在活动系统中进行索引创建),那么在后台创建索引是唯一的方法。有其他策略可以执行此类管理活动,我们将在管理部分的一些示例中看到。
对于前台索引创建来说,mongo 在索引创建期间获取的锁不是在集合级别,而是在数据库级别。为了解释这意味着什么,我们将不得不删除indexTest集合上的索引,并执行以下小练习:
- 首先通过从 shell 执行以下命令来在前台创建索引:
> db.indexTest.createIndex({value:1})
- 现在,将一个文档插入到 person 集合中,该集合此时可能存在,也可能不存在于测试数据库中,如下所示:
> db.person.insert({name:'Amol'})
我们将看到,person 集合中的此插入操作将在indexTest集合的索引创建过程中被阻塞。但是,如果此插入操作是在索引构建期间的不同数据库中的集合上执行的,它将正常执行而不会阻塞。(您也可以尝试一下。)这清楚地表明锁是在数据库级别而不是在集合或全局级别获取的。
注意
在 mongo 的 2.2 版本之前,锁是在全局级别,即在 mongod 进程级别,而不是在我们之前看到的数据库级别。当处理旧于 2.2 版本的 mongo 分布时,您需要记住这一点。
创建和理解稀疏索引
Mongo 的无模式设计是 Mongo 的基本特性之一。这允许集合中的文档具有不同的字段,一些文档中存在一些字段,而其他文档中不存在。换句话说,这些字段可能是稀疏的,这可能已经给你一个关于稀疏索引是什么的线索。在这个示例中,我们将创建一些随机测试数据,并查看稀疏索引在普通索引中的行为。我们将看到使用稀疏索引的优势和一个主要的缺陷。
准备工作
对于这个示例,我们需要创建一个名为sparseTest的集合。我们需要一个正在运行的服务器。有关如何启动服务器的说明,请参阅第一章中的安装单节点 MongoDB示例,安装和启动服务器。使用加载了SparseIndexData.js脚本的 shell 启动。此脚本可在 Packt 网站上下载。要了解如何使用预加载的脚本启动 shell,请参阅第一章中的使用 JavaScript 在 Mongo shell 中连接到单个节点示例,安装和启动服务器。
如何做…
- 通过调用以下方法加载集合中的数据。这应该会在
sparseTest集合中导入 100 个文档。
> createSparseIndexData()
- 现在,通过执行以下查询来查看数据,注意顶部几个结果中的
y字段:
> db.sparseTest.find({}, {_id:0})
- 我们可以看到
y字段不存在,或者如果存在的话是唯一的。然后执行以下查询:
> db.sparseTest.find({y:{$ne:2}}, {_id:0}).limit(15)
-
注意结果;它包含符合条件的文档以及不包含给定字段
y的字段。 -
由于
y的值似乎是唯一的,让我们按照以下方式在y字段上创建一个新的唯一索引:
> db.sparseTest.createIndex({y:1}, {unique:1})
这会抛出一个错误,抱怨值不是唯一的,冒犯的值是 null 值。
- 我们将通过以下方式将此索引设置为稀疏:
> db.sparseTest.createIndex({y:1}, {unique:1, sparse:1})
- 这应该解决我们的问题。为了确认索引已创建,请在 shell 上执行以下操作:
> db.sparseTest.getIndexes()
这应该显示两个索引,一个是默认的_id索引,另一个是我们刚刚在上一步中创建的索引。
-
现在,再次执行我们在步骤 3 中执行的查询,并查看结果。
-
查看结果并将其与创建索引之前看到的结果进行比较。重新执行查询,但使用以下提示强制进行完整集合扫描:
>db.sparseTest.find({y:{$ne:2}},{_id:0}).limit(15).hint({$natural:1})
- 观察结果。
工作原理…
这些是我们刚刚执行的许多步骤。我们现在将深入探讨并解释使用稀疏索引查询集合时看到的奇怪行为的内部和推理。
我们使用 JavaScript 方法创建的测试数据只是创建了一个名为x的键的文档,其值是从一开始的数字,一直到 100。只有当x是三的倍数时,才设置y的值-它的值也是一个从一开始的递增数字,当x是99时,它应该最多达到 33。
然后执行查询并查看以下结果:
> db.sparseTest.find({y:{$ne:2}}, {_id:0}).limit(15)
{ "x" : 1 }
{ "x" : 2 }
{ "x" : 3, "y" : 1 }
{ "x" : 4 }
{ "x" : 5 }
{ "x" : 7 }
{ "x" : 8 }
{ "x" : 9, "y" : 3 }
{ "x" : 10 }
{ "x" : 11 }
{ "x" : 12, "y" : 4 }
{ "x" : 13 }
{ "x" : 14 }
{ "x" : 15, "y" : 5 }
{ "x" : 16 }
结果中缺少y为2的值,这正是我们想要的。请注意,结果中仍然可以看到y不存在的文档。我们现在计划在y字段上创建一个索引。由于该字段要么不存在,要么具有唯一值,因此唯一索引应该能够正常工作。
在内部,索引默认情况下会在索引中添加一个条目,即使原始文档中的字段在集合中不存在。然而,进入索引的值将是 null。这意味着索引中将有与集合中文档数量相同的条目。对于唯一索引,值(包括 null 值)应该在整个集合中是唯一的,这解释了为什么在创建稀疏字段的索引时会出现异常(字段不在所有文档中都存在)。
解决这个问题的一个方法是使索引稀疏化,我们所做的就是在选项中添加sparse:1以及unique:1。如果文档中不存在该字段,则不会在索引中放入条目。因此,索引现在将包含更少的条目。它只包含那些字段存在于文档中的条目。这不仅使索引变小,易于放入内存,而且解决了添加唯一约束的问题。我们最不希望的是,拥有数百万文档的集合的索引有数百万条目,而只有少数几百条目有一些值定义。
虽然我们可以看到创建稀疏索引确实使索引更有效,但它引入了一个新问题,即一些查询结果不一致。我们之前执行的相同查询产生了不同的结果。请参见以下输出:
> db.sparseTest.find({y:{$ne:2}}, {_id:0}).hint({y:1}).limit(15)
{ "x" : 3, "y" : 1 }
{ "x" : 9, "y" : 3 }
{ "x" : 12, "y" : 4 }
{ "x" : 15, "y" : 5 }
{ "x" : 18, "y" : 6 }
{ "x" : 21, "y" : 7 }
{ "x" : 24, "y" : 8 }
{ "x" : 27, "y" : 9 }
{ "x" : 30, "y" : 10 }
{ "x" : 33, "y" : 11 }
{ "x" : 36, "y" : 12 }
{ "x" : 39, "y" : 13 }
{ "x" : 42, "y" : 14 }
{ "x" : 45, "y" : 15 }
{ "x" : 48, "y" : 16 }
为什么会发生这种情况?答案在于这个查询的查询计划。执行以下操作查看此查询的计划:
>db.sparseTest.find({y:{$ne:2}}, {_id:0}). hint({y:1}).limit(15).explain()
这个计划表明它使用索引来获取匹配的结果。由于这是一个稀疏索引,所有没有y字段的文档都不在其中,它们也没有出现在结果中,尽管它们应该出现。这是一个我们在查询使用稀疏索引的集合时需要小心的陷阱。它会产生意想不到的结果。一个解决方案是强制进行全集合扫描,我们可以使用hint函数为查询分析器提供提示。提示用于强制查询分析器使用用户指定的索引。尽管通常不建议这样做,因为你真的需要知道你在做什么,但这是真正需要的情况之一。那么,我们如何强制进行全表扫描呢?我们只需在hint函数中提供{$natural:1}。集合的自然排序是指它在磁盘上存储的特定集合的顺序。这个hint强制进行全表扫描,现在我们得到了之前的结果。然而,对于大集合,查询性能会下降,因为现在使用了全表扫描。
如果字段存在于许多文档中(对于什么是很多没有正式的标准;对于一些人来说可能是 50%,对于其他人来说可能是 75%),并且不是真正稀疏的,那么使索引稀疏化除了当我们想要使其唯一之外就没有太多意义了。
注意
如果两个文档对于相同字段具有空值,唯一索引创建将失败,并且将其创建为稀疏索引也不会有帮助。
使用 TTL 索引在固定间隔后过期文档
Mongo 中一个有趣的特性是在预定的时间后自动删除集合中的数据。当我们想要清除一些比特定时间段更旧的数据时,这是一个非常有用的工具。对于关系数据库来说,通常不会有人设置每晚运行的批处理作业来执行此操作。
有了 Mongo 的 TTL 功能,您不必担心这个问题,因为数据库会自动处理。让我们看看如何实现这一点。
准备就绪
让我们在 Mongo 中创建一些数据,以便使用 TTL 索引进行操作。我们将为此目的创建一个名为ttlTest的集合。我们需要一个服务器正在运行。有关如何启动服务器的说明,请参阅第一章中的安装单节点 MongoDB配方,安装和启动服务器。使用加载了TTLData.js脚本的 shell 启动。此脚本可在 Packt 网站上下载。要了解如何使用预加载的脚本启动 shell,请参阅第一章中的使用 JavaScript 在 Mongo shell 中连接到单节点配方,安装和启动服务器。
如何做…
- 假设服务器已启动,并且提供的脚本已加载到 shell 中,请从 mongo shell 中调用以下方法:
> addTTLTestData()
- 在
createDate字段上创建 TTL 索引如下:
> db.ttlTest.createIndex({createDate:1}, {expireAfterSeconds:300})
- 现在,按以下方式查询集合:
> db.ttlTest.find()
- 这应该给我们三个文件。重复这个过程,并且在大约 30-40 秒内执行
find查询,以便看到三个文件被删除,直到整个集合中没有文件为止。
它是如何工作的...
让我们从打开TTLData.js文件开始,看看里面发生了什么。代码非常简单,它只是使用 new Date()获取当前日期。然后在这个脚本中的addTTLTestData()方法的执行中,我们有三个文档在ttlTest集合中,每个文档的创建时间相差一分钟。
下一步是 TTL 功能的核心:创建 TTL 索引。它类似于使用createIndex方法创建任何其他索引,只是它还接受一个 JSON 对象作为第二个参数。这两个参数如下:
-
第一个参数是
{createDate:1};这将告诉 mongo 在createDate字段上创建一个索引,索引的顺序是升序的,因为值是1(-1将是降序的)。 -
第二个参数
{expireAfterSeconds:300}是使该索引成为 TTL 索引的关键,它告诉 Mongo 在 300 秒(五分钟)后自动使文档过期。
好吧,但是从什么时候开始的五分钟?它是它们被插入集合的时间还是其他时间戳?在这种情况下,它认为createTime字段是基础,因为这是我们创建索引的字段。
现在引发一个问题:如果一个字段被用作时间计算的基础,那么它的类型必须受到一定的限制。在一个char字段上创建 TTL 索引就没有意义,比如说,保存一个人名字的字段。
是的;正如我们猜测的那样,字段的类型可以是 BSON 类型的日期或日期数组。在数组中有多个日期的情况下会发生什么?在这种情况下会考虑什么?
结果是 Mongo 使用数组中可用的日期的最小值。尝试这种情况作为练习。
在一个文档中,对updateField字段放入两个相隔大约五分钟的日期,然后在这个字段上创建一个 TTL 索引,使文档在 10 分钟(600 秒)后过期。查询集合,看看文档何时从集合中删除。它应该在updateField数组中的最小时间值之后大约 10 分钟后被删除。
除了字段类型的约束外,还有一些其他约束:
-
如果一个字段已经有了索引,你就不能创建 TTL 索引。因为集合的
_id字段已经默认有了索引,这实际上意味着你不能在_id字段上创建 TTL 索引。 -
TTL 索引不能是涉及多个字段的复合索引。
-
如果字段不存在,它将永远不会过期。(我想这很合乎逻辑。)
-
它不能在封闭集合上创建。如果你不知道封闭集合,它们是 Mongo 中的特殊集合,它们有一个大小限制,按照 FIFO 插入顺序删除旧文档,以便为新文档腾出空间。
注意
TTL 索引仅支持 Mongo 版本 2.2 及以上。请注意,文档不会在字段中给定的确切时间被删除。周期将以一分钟的粒度进行,这将删除自上次运行周期以来符合删除条件的所有文档。
另请参阅
使用情况可能不要求在固定时间间隔后删除所有文档。如果我们想要自定义文档在集合中停留的时间,也可以实现,这将在下一个示例“使用 TTL 索引在特定时间到期的文档”中进行演示。
使用 TTL 索引在特定时间到期的文档
在上一个示例“使用 TTL 索引在固定时间间隔后到期的文档”中,我们已经看到文档在固定时间段后到期的情况。但是,可能存在一些情况,我们希望文档在不同时间到期。这与上一个示例中所看到的情况不同。在本示例中,我们将看到如何指定文档可以到期的时间,对于不同的文档可能是不同的。
准备就绪
对于本示例,我们将创建一个名为ttlTest2的集合。我们需要一个正在运行的服务器。有关如何启动服务器的说明,请参阅第一章中的安装单节点 MongoDB示例,安装和启动服务器。使用加载了TTLData.js脚本的 shell。此脚本可在 Packt 网站上下载。要了解如何使用预加载脚本启动 shell,请参阅第一章中的使用 JavaScript 连接 Mongo shell 中的单节点示例,安装和启动服务器。
如何操作…
- 使用
addTTLTestData2方法在集合中加载所需的数据。在 mongo shell 上执行以下操作:
> addTTLTestData2()
- 现在,按照以下步骤在
ttlTest2集合上创建 TTL 索引:
> db.ttlTest2.createIndex({expiryDate :1}, {expireAfterSeconds:0})
- 执行以下
find查询以查看集合中的三个文档:
> db.ttlTest2.find()
- 现在,大约四、五和七分钟后,查看 ID 为 2、1 和 3 的文档是否分别被删除。
工作原理…
让我们开始打开TTLData.js文件,看看里面发生了什么。我们本次示例感兴趣的方法是addTTLTestData2。该方法简单地在tllTest2集合中创建了三个文档,其_id分别为1、2和3,其exipryDate字段分别设置为当前时间之后的5、4和7分钟。请注意,与上一个示例中给出的创建日期不同,该字段具有未来日期。
接下来,我们创建一个索引:db.ttlTest2.createIndex({expiryDate :1}, {expireAfterSeconds:0})。这与我们在上一个示例中创建索引的方式不同,其中对象的expireAfterSeconds字段设置为非零值。这是expireAfterSeconds属性值的解释方式。如果值为非零,则这是文档将在 Mongo 中从集合中删除的基准时间之后经过的秒数。此基准时间是索引创建的字段中保存的值(如上一个示例中的createTime)。如果此值为零,则索引创建的日期值(在本例中为expiryDate)将是文档到期的时间。
总之,如果要在到期后删除文档,则 TTL 索引效果很好。有很多情况下,我们可能希望将文档移动到存档集合中,存档集合可能是基于年份和月份创建的。在任何这种情况下,TTL 索引都没有帮助,我们可能需要自己编写一个外部作业来完成这项工作。这样的作业还可以读取一系列文档,将它们添加到目标集合中,并从源集合中删除它们。MongoDB 的开发人员已经计划发布一个解决这个问题的功能。
另请参阅
在这个和前一个教程中,我们看了看 TTL 索引以及如何使用它们。然而,如果在创建了 TTL 索引之后,我们想要修改 TTL 值怎么办?这是可以通过使用collMod选项来实现的。在管理部分可以了解更多关于这个选项的信息。
第三章:编程语言驱动程序
在本章中,我们将涵盖以下配方:
-
使用 PyMongo 执行查询和插入操作
-
使用 PyMongo 执行更新和删除操作
-
使用 PyMongo 在 Mongo 中实现聚合
-
使用 PyMongo 在 Mongo 中执行 MapReduce
-
使用 Java 客户端执行查询和插入操作
-
使用 Java 客户端执行更新和删除操作
-
使用 Java 客户端在 Mongo 中实现聚合
-
使用 Java 客户端在 Mongo 中执行 MapReduce
介绍
到目前为止,我们已经在 shell 中使用 Mongo 执行了大部分操作。Mongo shell 是管理员执行管理任务和开发人员在编写应用程序逻辑之前通过查询数据快速测试事物的绝佳工具。然而,我们如何编写应用程序代码来允许我们在 MongoDB 中查询、插入、更新和删除(以及其他操作)数据?我们编写应用程序的编程语言应该有一个库。我们应该能够实例化一些东西或从程序中调用方法来执行一些操作在远程 Mongo 进程上。
除非有一个理解与远程服务器通信协议并能够传输我们需要的操作的桥梁,以便在 Mongo 服务器进程上执行并将结果传回客户端。简单地说,这个桥梁被称为驱动程序,也称为客户端库。驱动程序构成了 Mongo 的编程语言接口的支柱;如果没有它们,应用程序将负责使用服务器理解的低级协议与 Mongo 服务器通信。这不仅需要开发,还需要测试和维护。尽管通信协议是标准的,但不能有一个适用于所有语言的实现。各种编程语言需要有自己的实现,向所有语言公开类似的编程接口。我们将在本章中看到的客户端 API 的核心概念对所有语言都适用。
提示
Mongo 支持所有主要编程语言,并得到 MongoDB Inc 的支持。社区还支持大量的编程语言。您可以访问docs.mongodb.org/ecosystem/drivers/community-supported-drivers/查看 Mongo 支持的各种平台。
使用 PyMongo 执行查询和插入操作
这个配方是关于使用 PyMongo 执行基本查询和“插入”操作的。这与我们在本书前面使用 Mongo shell 所做的类似。
准备工作
要执行简单的查询,我们需要一个正在运行的服务器。我们需要一个简单的单节点。有关如何启动服务器的说明,请参阅第一章中的安装单节点 MongoDB配方,安装和启动服务器。我们将操作的数据需要导入数据库。有关导入数据的步骤,请参阅第二章中的创建测试数据配方,命令行操作和索引。主机操作系统上必须安装 Python 2.7 或更高版本,以及 MongoDB 的 Python 客户端 PyMongo。如何在主机操作系统上安装 PyMongo,请参阅第一章中的早期配方使用 Python 客户端连接到单节点,安装和启动服务器。此外,在这个配方中,我们将执行“插入”操作并提供写关注。
如何做…
让我们从 Python shell 中查询 Mongo 开始。这将与我们在 mongo shell 中所做的完全相同,只是这是用 Python 编程语言而不是我们在 mongo shell 中使用的 JavaScript。我们可以使用这里将看到的基础知识来编写在 Python 上运行并使用 mongo 作为数据存储的大型生产系统。
让我们从操作系统的命令提示符开始启动 Python shell。所有这些步骤都与主机操作系统无关。执行以下步骤:
- 在 shell 中输入以下内容,Python shell 应该启动:
$ python
Python 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>
- 然后,导入
pymongo包,并创建客户端如下:
>>> import pymongo
>>> client = pymongo.MongoClient('localhost', 27017)
The following is an alternative way to connect
>>> client = pymongo.MongoClient('mongodb://localhost:27017')
- 这样做效果很好,可以实现相同的结果。现在我们有了客户端,下一步是获取我们将执行操作的数据库。这与一些编程语言不同,在那里我们有一个
getDatabase()方法来获取数据库的实例。我们将获取一个对数据库对象的引用,我们将在其上执行操作,在这种情况下是test。我们将以以下方式执行此操作:
>>> db = client.test
Another alternative is
>>> db = client['test']
- 我们将查询
postalCodes集合。我们将将结果限制为 10 项。
>>> postCodes = db.postalCodes.find().limit(10)
- 遍历结果。注意
for语句后的缩进。以下片段应该打印出返回的 10 个文档:
>>> for postCode in postCodes:
print 'City: ', postCode['city'], ', State: ', postCode['state'], ', Pin Code: ', postCode['pincode']
- 要查找一个文档,请执行以下操作:
>>> postCode = db.postalCodes.find_one()
- 按以下方式打印返回结果的
state和city:
>>> print 'City: ', postCode['city'], ', State: ', postCode['state'], ', Pin Code: ', postCode['pincode']
- 让我们查询古吉拉特邦前 10 个城市,按城市名称排序,并且额外选择
city、state和pincode。在 Python shell 中执行以下查询:
>>> cursor = db.postalCodes.find({'state':'Gujarat'}, {'_id':0, 'city':1, 'state':1, 'pincode':1}).sort('city', pymongo.ASCENDING).limit(10)
前面游标的结果可以以与我们在第 5 步中打印结果相同的方式打印出来。
- 让我们对我们查询的数据进行排序。我们希望按州的降序和城市的升序进行排序。我们将编写以下查询:
>>> city = db.postalCodes.find().sort([('state', pymongo.DESCENDING),('city',pymongo.ASCENDING)]).limit(5)
-
通过迭代这个游标,应该在控制台上打印出五个结果。参考第 5 步,了解如何迭代返回的游标以打印结果。
-
因此,我们已经玩了一会儿,找到了文档,并涵盖了 Python 中与查询 MongoDB 有关的基本操作。现在,让我们稍微了解一下
insert操作。我们将使用一个测试集合来执行这些操作,而不会干扰我们的邮政编码测试数据。我们将使用pymongoTest集合来实现这个目的,并按以下方式向其中添加文档:
>>> for i in range(1, 21):
db.pymongoTest.insert_one({'i':i})
insert可以接受一个字典对象列表并执行批量插入。因此,现在类似以下的insert是完全有效的:
>>> db.pythonTest.insert_many([{'name':'John'}, {'name':'Mark'}])
对返回值有什么猜测吗?在单个文档插入的情况下,返回值是新创建文档的_id的值。在这种情况下,它是一个 ID 列表。
它是如何工作的...
在第 2 步,我们实例化客户端并获取对MongoClient对象的引用,该对象将用于访问数据库。有几种方法可以获取这个引用。第一个选项更方便,除非你的数据库名称中有一些特殊字符,比如连字符(-)。例如,如果名称是db-test,我们将不得不使用[]运算符来访问数据库。使用任何一种替代方案,我们现在都有了一个指向db变量中测试数据库的对象。在 Python 中获取client和db实例之后,我们查询以自然顺序从第 3 步的集合中找到前 10 个文档。语法与在 shell 中执行此查询的方式完全相同。第 4 步只是简单地打印出结果,这种情况下有 10 个结果。通常,如果您需要在 Python 解释器中使用类名或该类的实例获得特定类的即时帮助,只需执行dir(<class_name>)或dir(<object of a class>),这将给出模块中定义的属性和函数的列表。例如,dir('pymongo.MongoClient')或dir(client),其中client是持有pymongo.MongoClient实例引用的变量,可以用来获取所有支持的属性和函数的列表。help函数更具信息性,打印出模块的文档,并且在需要即时帮助时是一个很好的参考来源。尝试输入help('pymongo.MongoClient')或help(client)。
在第 3 和第 4 步,我们查询postalCodes集合,将结果限制为前 10 个结果,并将它们打印出来。返回的对象是pymongo.cursor.Cursor类的一个实例。下一步使用find_one()函数从集合中获取一个文档。这相当于在 shell 中调用集合的findOne()方法。这个函数返回的值是一个内置对象,dict。
在第 6 步,我们执行另一个find来查询数据。在第 8 步,我们传递了两个 Python 字典。第一个字典是查询,类似于我们在 mongo shell 中使用的查询参数。第二个字典用于提供要在结果中返回的字段。对于一个字段,值为 1 表示该值将被选择并返回在结果中。这与在关系数据库中使用select语句并显式提供要选择的几组列是相同的。_id字段默认被选择,除非在选择器dict对象中明确设置为零。这里提供的选择器是{'_id':0, 'city':1, 'state':1, 'pincode':1},它选择了城市、州和邮政编码,并抑制了_id字段。我们也有一个排序方法。这个方法有两种格式,如下所示:
sort(sort_field, sort_direction)
sort([(sort_field, sort_direction)…(sort_field, sort_direction)])
第一个用于只按一个字段排序。第二种表示接受一个排序字段和排序方向的对列表,并且用于按多个字段排序。我们在第 8 步的查询中使用了第一种形式,在第 9 步的查询中使用了第二种形式,因为我们首先按州名排序,然后按城市排序。
如果我们看一下我们调用sort的方式,它是在Cursor实例上调用的。同样,limit函数也是在Cursor类上的。评估是延迟的,直到进行迭代以从游标中检索结果为止。在这个时间点之前,Cursor对象不会在服务器上进行评估。
在第 11 步,我们在集合中插入一个文档 20 次。每次插入,正如我们在 Python shell 中看到的那样,都会返回一个生成的_id字段。在插入的语法方面,它与我们在 shell 中执行的操作完全相同。传递给插入的参数是一个dict类型的对象。
在第 12 步中,我们将一个文档列表传递给集合进行插入。这被称为批量插入操作,它可以在一次调用中向服务器插入多个文档。在这种情况下,返回值是一个 ID 列表,每个插入的文档都有一个 ID,并且顺序与输入列表中的顺序相同。然而,由于 MongoDB 不支持事务,每次插入都是相互独立的,一个插入的失败不会自动回滚整个操作。
添加插入多个文档的功能需要另一个参数来控制行为。在给定的列表中,如果其中一个插入失败,剩余的插入是否应该继续,还是在遇到第一个错误时停止插入?控制此行为的参数名称是continue_on_error,其默认值为False,即遇到第一个错误时停止插入。如果此值为True,并且在插入过程中发生多个错误,那么只有最新的错误将可用,因此默认选项为False是明智的。让我们看几个例子。在 Python shell 中,执行以下操作:
>>> db.contOnError.drop()
>>> db.contOnError.insert([{'_id':1}, {'_id':1}, {'_id':2}, {'_id':2}])
>>> db.contOnError.count()
我们将得到的计数是1,这是第一个具有_id字段为1的文档。一旦找到另一个具有相同_id字段值的文档,即在本例中为1,就会抛出错误并停止批量插入。现在执行以下insert操作:
>>> db.contOnError.drop()
>>> db.contOnError.insert([{'_id':1}, {'_id':1}, {'_id':2}, {'_id':2}], continue_on_error=True)
>>> db.contOnError.count()
在这里,我们传递了一个额外的参数continue_on_error,其值为True。这样做可以确保insert操作会在中间的insert操作失败时继续进行下一个文档的插入。第二个具有_id:1的插入失败,但在另一个具有_id:2的插入失败之前,下一个插入会继续进行(因为已经存在一个具有此_id的文档)。此外,报告的错误是最后一个失败的错误,即具有_id:2的错误。
另请参阅
下一个配方,使用 PyMongo 执行更新和删除操作,接着介绍了更新、删除和原子查找操作。
使用 PyMongo 执行更新和删除操作
在上一个配方中,我们看到了如何使用 PyMongo 在 MongoDB 中执行find和insert操作。在本配方中,我们将看到如何在 Python 中执行更新和删除操作。我们还将看到原子查找和更新/删除是什么,以及如何执行它们。最后,我们将重新审视查找操作,并了解cursor对象的一些有趣的功能。
准备工作
如果您已经看过并完成了上一个配方,那么您就可以开始了。如果没有,建议您在继续本配方之前先完成那个配方。此外,如果您不确定读取偏好和写入关注是什么,请参考本书的附录中的两个配方,用于查询的读取偏好和写入关注及其重要性。
在我们开始之前,让我们定义一个小函数,它通过游标迭代并在控制台上显示游标的结果。每当我们想要在pymongoTests集合上显示查询结果时,我们将使用这个函数。以下是函数体:
>>> def showResults(cursor):
if cursor.count() != 0:
for e in cursor:
print e
else:
print 'No documents found'
您可以参考上一个配方中的步骤 1 和步骤 2,了解如何连接到 MongoDB 服务器以及用于在该数据库上执行 CRUD 操作的db对象。此外,还可以参考上一个配方中的第 8 步,了解如何在pymongoTest集合中插入所需的测试数据。一旦数据存在,您可以在 Python shell 中执行以下操作来确认该集合中的数据:
>>> showResults(db.pymongoTest.find())
对于食谱的一部分,人们也应该知道如何启动一个副本集实例。有关副本集的更多细节以及如何启动副本集,请参阅第一章中的作为副本集的一部分启动多个实例和在 shell 中连接到副本集以查询和插入数据食谱。
操作步骤…
我们将从 Python shell 中运行以下命令开始:
- 如果
i字段的值大于 10,则我们将设置一个名为gtTen的字段,并指定一个布尔值True。让我们执行以下更新:
>>>result = db.pymongoTest.update_one({'i':{'$gt':10}}, {'$set':{'gtTen':True}})
>>> print result.raw_result
{u'n': 1, u'nModified': 0, u'ok': 1, 'updatedExisting': True}
- 查询集合,通过执行以下操作查看其数据,并检查已更新的数据:
>>> showResults(db.pymongoTest.find())
- 显示的结果证实只有一个文档被更新。现在我们将再次执行相同的更新,但这一次,我们将更新所有与提供的查询匹配的文档。在 Python shell 中执行以下更新。注意响应中的 n 的值,这次是
10。
>>> result = db.pymongoTest.update_many({'i':{'$gt':10}},{'$set':{'gtTen':True}})
print result.raw_result
{u'n': 10, u'nModified': 9, u'ok': 1, 'updatedExisting': True}
-
再次执行我们在步骤 2 中进行的操作,查看
pymongoTest集合中的内容,并验证已更新的文档。 -
让我们看看如何执行
upsert操作。Upserts 是更新加插入,如果文档存在则更新文档,就像更新操作一样,否则插入一个新文档。我们将看一个例子。考虑对集合中不存在的文档进行以下更新:
>>> db.pymongoTest.update_one({'i':21},{'$set':{'gtTen':True}})
- 这里的更新不会更新任何内容,并且返回更新的文档数为零。但是,如果我们想要更新一个文档(如果存在),否则插入一个新文档并原子性地应用更新,那么我们执行一个
upsert操作。在这种情况下,upsert操作执行如下。请注意返回结果中提到的upsert,新插入文档的ObjectId和updatedExisting值,这个值是False:
>>>result = db.pymongoTest.update_one({'i':21},{'$set':{'gtTen':True}}, upsert=True)
>>> print result.raw_result
{u'n': 1,
u'nModified': 0,
u'ok': 1,
'updatedExisting': False,
u'upserted': ObjectId('557bd3a618292418c38b046d')}
- 让我们看看如何使用
remove方法从集合中删除文档:
>>>result = db.pymongoTest.delete_one({'i':21})
>>> print result.raw_result
{u'n': 1, u'ok': 1}
-
如果我们查看前面响应中
n的值,我们可以看到它是1。这意味着已删除一个文档。 -
要从集合中删除多个文档,我们使用
delete_many方法:
>>>result = db.pymongoTest.delete_many({'i':{'$gt': 10}})
>>> print result.raw_result
{u'n': 10, u'ok': 1}
- 我们现在将看一下查找和修改操作。我们可以将这些操作视为查找文档并更新/删除它的一种方式,这两种操作都是原子性执行的。操作完成后,返回的文档要么是更新操作之前的文档,要么是更新操作之后的文档。(在
remove的情况下,操作后将没有文档。)在没有这种操作的情况下,我们无法保证在多个客户端连接可能对同一文档执行类似操作的情况下的原子性。以下是如何在 Python 中执行此查找和修改操作的示例:
>>> db.pymongoTest.find_one_and_update({'i':20}, {'$set':{'inWords':'Twenty'}})
{u'_id': ObjectId('557bdb070640fd0a0a935c22'), u'i': 20}
提示
前面的结果告诉我们,在应用更新之前返回的文档是之前的文档。
- 执行以下
find方法来查询并查看我们在上一步中更新的文档。结果文档将包含在Words字段中新增的内容:
>>> db.pymongoTest.find_one({'i':20})
{u'i': 20, u'_id': ObjectId('557bdb070640fd0a0a935c22'), u'inWords': u'Twenty'}
- 我们将再次执行
find和modify操作,但这一次,我们返回更新后的文档,而不是在步骤 9 中看到的更新前的文档。在 Python shell 中执行以下操作:
>>> db.pymongoTest.find_one_and_update({'i':19}, {'$set':{'inWords':'Nineteen'}}, new=True)
{u'_id': ObjectId('557bdb070640fd0a0a935c21'), u'i': 19, u'inWords': u'Nineteen'}
- 我们在上一个食谱中看到了如何使用 PyMongo 进行查询。在这里,我们将继续进行查询操作。我们看到
sort和limit函数是如何链接到 find 操作的。对postalCodes集合的调用原型如下:
db.postalCode.find(..).limit(..).sort(..)
- 有一种实现相同结果的替代方法。在 Python shell 中执行以下查询:
>>>cursor = db.postalCodes.find({'state':'Gujarat'}, {'_id':0, 'city':1, 'state':1, 'pincode':1}, limit=10, sort=[('city', pymongo.ASCENDING)])
- 使用已定义的
showResult函数打印前面的游标。
工作原理…
让我们看看在这个示例中我们做了什么;我们从在步骤 1 中更新集合中的文档开始。但是,默认情况下,更新操作只更新第一个匹配的文档,其余匹配的文档不会被更新。在步骤 2 中,我们添加了一个名为multi的参数,其值为True,以便在同一更新操作中更新多个文档。请注意,所有这些文档不会作为一个事务的一部分被原子更新。在 Python shell 中查看更新后,我们看到与在 Mongo shell 中所做的操作非常相似。如果我们想要为更新操作命名参数,那么提供为查询使用的文档的参数名称称为spec,而更新文档的参数名称称为document。例如,以下更新是有效的:
>>> db.pymongoTest.update_one(spec={'i':{'$gt':10}},document= {'$set':{'gtTen':True}})
在第 6 步,我们进行了upsert(更新加插入)操作。我们只有一个额外的参数upsert,其值为True。但是,在upsert的情况下到底发生了什么呢?Mongo 尝试更新匹配提供条件的文档,如果找到一个,则这将是一个常规更新。但是,在这种情况下(第 6 步的upsert),未找到文档。服务器将以原子方式将作为查询条件的文档(第一个参数)插入到集合中,然后对其进行更新。
在第 7 和第 9 步,我们看到了remove操作。第一个变体接受一个查询,并删除匹配的文档。第二个变体在第 9 步中删除了所有匹配的文档。
在第 10 到 12 步,我们执行了find和modify操作。这些操作的要点非常简单。我们没有提到的是find_one_and_replace()方法,顾名思义,可以用来搜索文档并完全替换它。
在这个示例中看到的所有操作都是针对连接到独立实例的客户端的。如果您连接到一个副本集,客户端将以不同的方式实例化。我们也知道,默认情况下不允许查询副本节点的数据。我们需要在连接到副本节点的 mongo shell 中显式执行rs.slaveOk()来查询它。在 Python 客户端中也是以类似的方式执行。如果我们连接到副本节点,不能默认查询它,但是我们指定我们可以查询副本节点的方式略有不同。从 PyMongo 3.0 开始,我们现在可以在初始化MongoClient时传递ReadPreference。这主要是因为,从 PyMongo 3.0 开始,pymongo.MongoClient()是连接到独立实例、副本集或分片集群的唯一方式。可用的读取偏好包括PRIMARY、SECONDARY、PRIMARY_PREFERRED、SECONDARY_PREFERRED和NEAREST。
>> client = pymongo.MongoClient('localhost', 27017, readPreference='secondaryPreferred')
>> print cl.read_preference
SecondaryPreferred(tag_sets=None)
除了客户端之外,PyMongo 还允许您在数据库或集合级别设置读取偏好。
默认情况下,未显式设置读取偏好的初始化客户端的read_preference为PRIMARY(值为零)。但是,如果我们现在从先前初始化的客户端获取数据库对象,则读取偏好将为NEAREST(值为4)。
>>> db = client.test
>>> db.read_preference
Primary()
>>>
设置读取偏好就像做以下操作一样简单:
>>>db = client.get_database('test', read_preference=ReadPreference.SECONDARY)
同样,由于读取偏好从客户端继承到数据库对象,它也从数据库对象继承到集合对象。这将作为针对该集合执行的所有查询的默认值,除非在find操作中显式指定读取偏好。
因此,db.pymongoTest.find_one()将使用读取偏好作为SECONDARY(因为我们刚刚在数据库对象级别将其设置为SECONDARY)。
现在,我们将通过尝试在 Python 驱动程序中完成基本操作来结束。这些操作与我们在 mongo shell 中进行的一些常见操作相似,例如获取所有数据库名称,获取数据库中集合的列表,并在集合上创建索引。
在 shell 中,我们显示dbs以显示连接的 mongo 实例中的所有数据库名称。从 Python 客户端,我们对客户端实例执行以下操作:
>>> client.database_names()
[u'local', u'test']
同样,要查看集合的列表,我们在 mongo shell 中执行show collections;在 Python 中,我们对数据库对象执行以下操作:
>>> db.collection_names()
[u'system.indexes', u'writeConcernTest', u'pymongoTest']
现在对于index操作;我们首先查看pymongoTest集合中存在的所有索引。在 Python shell 中执行以下操作以查看集合上的索引:
>>> db.pymongoTest.index_information()
{u'_id_': {u'key': [(u'_id', 1)], u'ns': u'test.pymongoTest', u'v': 1}}
现在,我们将按照以下方式在pymongoTest集合上对键x创建一个升序排序的索引:
>>>from pymongo import IndexModel, ASCENDING
>>> myindex = IndexModel([("x", ASCENDING)], name='Index_on_X')
>>>db.pymongoTest.create_indexes([myindex])
['Index_on_X']
我们可以再次列出索引以确认索引的创建:
>>> db.pymongoTest.index_information()
{u'Index_on_X': {u'key': [(u'x', 1)], u'ns': u'test.pymongoTest', u'v': 1},
u'_id_': {u'key': [(u'_id', 1)], u'ns': u'test.pymongoTest', u'v': 1}}
我们可以看到索引已经创建。删除索引也很简单,如下所示:
db.pymongoTest.drop_index('Index_on_X')
另一个参数叫做CursorType.TAILABLE,用于表示find返回的游标是一个可追加的游标。解释可追加游标并提供更多细节不在这个配方的范围内,将在第五章的创建和追加一个被截断的集合游标在 MongoDB 中配方中进行解释。
使用 PyMongo 在 Mongo 中实现聚合
我们已经在之前的配方中看到了 PyMongo 使用 Python 的客户端接口来连接 Mongo。在这个配方中,我们将使用邮政编码集合并使用 PyMongo 运行一个聚合示例。这个配方的目的不是解释聚合,而是展示如何使用 PyMongo 实现聚合。在这个配方中,我们将根据州名对数据进行聚合,并获取出现次数最多的五个州名。我们将使用$project、$group、$sort和$limit操作符进行这个过程。
准备工作
要执行聚合操作,我们需要运行一个服务器。一个简单的单节点就足够了。请参考第一章中的安装单节点 MongoDB配方,了解如何启动服务器的说明。我们将要操作的数据需要导入到数据库中。导入数据的步骤在第二章的创建测试数据配方中有提到。此外,请参考第一章中的使用 Python 客户端连接到单节点配方,了解如何为您的主机操作系统安装 PyMongo。由于这是在 Python 中实现聚合的一种方式,假设读者已经了解了 MongoDB 中的聚合框架。
操作步骤如下:
- 通过在命令提示符中输入以下内容来打开 Python 终端:
$ Python
- 一旦 Python shell 打开,按照以下方式导入
pymongo:
>>> import pymongo
- 创建
MongoClient的实例如下:
>>> client = pymongo.MongoClient('mongodb://localhost:27017')
- 按照以下方式获取测试数据库的对象:
>>> db = client.test
- 现在,我们按照以下步骤在
postalCodes集合上执行聚合操作:
result = db.postalCodes.aggregate(
[
{'$project':{'state':1, '_id':0}},
{'$group':{'_id':'$state', 'count':{'$sum':1}}},
{'$sort':{'count':-1}},
{'$limit':5}
]
)
- 输入以下内容以查看结果:
>>>for r in result:
print r
{u'count': 6446, u'_id': u'Maharashtra'}
{u'count': 4684, u'_id': u'Kerala'}
{u'count': 3784, u'_id': u'Tamil Nadu'}
{u'count': 3550, u'_id': u'Andhra Pradesh'}
{u'count': 3204, u'_id': u'Karnataka'}
工作原理如下:
这些步骤非常简单。我们已经连接到运行在本地主机上的数据库,并创建了一个数据库对象。我们在集合上调用的聚合操作使用了aggregate函数,这与我们在 shell 中调用聚合的方式非常相似。返回值中的对象result是一个游标,它在迭代时返回一个dict类型的对象。这个dict包含两个键,分别是州名和它们出现次数的计数。在第 6 步中,我们只是简单地迭代游标(result)以获取每个结果。
在 PyMongo 中执行 MapReduce
在我们之前的配方使用 PyMongo 在 Mongo 中实现聚合中,我们看到了如何使用 PyMongo 在 Mongo 中执行聚合操作。在这个配方中,我们将处理与聚合操作相同的用例,但是我们将使用 MapReduce。我们的目的是根据州名对数据进行聚合,并获取出现次数最多的五个州名。
编程语言驱动程序为我们提供了一个接口,用于在服务器上调用用 JavaScript 编写的 map reduce 作业。
准备工作
要执行 map reduce 操作,我们需要启动并运行一个服务器。一个简单的单节点就是我们需要的。请参考第一章中的安装单节点 MongoDB配方,了解如何启动服务器的说明。我们将要操作的数据需要导入到数据库中。导入数据的步骤在第二章的创建测试数据配方中有提到。另外,请参考第一章中的使用 Python 客户端连接到单节点配方,了解如何为您的主机操作系统安装 PyMongo。
操作步骤如下:
- 通过在命令提示符上输入以下内容打开 Python 终端:
>>>python
- 一旦 Python shell 打开,导入
bson包,如下所示:
>>> import bson
- 导入
pymongo包,如下所示:
>>> import pymongo
- 创建一个
MongoClient对象,如下所示:
>>> client = pymongo.MongoClient('mongodb://localhost:27017')
- 获取测试数据库的对象,如下所示:
>>> db = client.test
- 编写以下
mapper函数:
>>> mapper = bson.Code('''function() {emit(this.state, 1)}''')
- 编写以下
reducer函数:
>>> reducer = bson.Code('''function(key, values){return Array.sum(values)}''')
- 调用 map reduce;结果将被发送到
pymr_out集合:
>>> db.postalCodes.map_reduce(map=mapper, reduce=reducer, out='pymr_out')
- 验证结果如下:
>>> c = db.pymr_out.find(sort=[('value', pymongo.DESCENDING)], limit=5)
>>> for elem in c:
... print elem
...
{u'_id': u'Maharashtra', u'value': 6446.0}
{u'_id': u'Kerala', u'value': 4684.0}
{u'_id': u'Tamil Nadu', u'value': 3784.0}
{u'_id': u'Andhra Pradesh', u'value': 3550.0}
{u'_id': u'Karnataka', u'value': 3204.0}
>>>
工作原理
除了常规的pymongo导入外,这里我们还导入了bson包。这就是我们有Code类的地方;它是我们用于 JavaScript map和reduce函数的Python对象。通过将 JavaScript 函数体作为构造函数参数来实例化它。
一旦实例化了两个Code类的实例,一个用于map,另一个用于reduce,我们所做的就是在集合上调用map_reduce函数。在这种情况下,我们传递了三个参数:两个map和reduce函数的Code实例,参数名称分别为map和reduce,以及一个字符串值,用于提供结果写入的输出集合的名称。
我们不会在这里解释 map reduce JavaScript 函数,但它非常简单,它所做的就是将键作为州名,并将值作为特定州名出现的次数。使用的结果文档,将州名作为_id字段,另一个名为 value 的字段,它是给定在_id字段中出现的特定州名的次数的总和,添加到输出集合pymr_out中。例如,在整个集合中,州马哈拉施特拉邦出现了6446次,因此马哈拉施特拉邦的文档是{u'_id': u'马哈拉施特拉邦', u'value': 6446.0}。要验证结果是否正确,您可以在 mongo shell 中执行以下查询,并查看结果是否确实为6446:
> db.postalCodes.count({state:'Maharashtra'})
6446
我们还没有完成,因为要求是找到集合中出现次数最多的五个州;我们只有州和它们的出现次数,所以最后一步是按值字段对文档进行排序,即州名出现的次数,按降序排序,并将结果限制为五个文档。
另请参阅
请参考第八章中的与 Hadoop 集成,了解在 MongoDB 中使用 Hadoop 连接器执行 map reduce 作业的不同示例。这允许我们在诸如 Java、Python 等语言中编写map和reduce函数。
使用 Java 客户端执行查询和插入操作
在这个示例中,我们将使用 Java 客户端执行 MongoDB 的查询和insert操作。与 Python 编程语言不同,Java 代码片段无法从交互式解释器中执行,因此我们将已经实现了一些单元测试用例,其相关代码片段将被展示和解释。
准备工作
对于这个示例,我们将启动一个独立的实例。请参考第一章中的安装单节点 MongoDB示例,了解如何启动服务器的说明。
下一步是从 Packt 网站下载 Java 项目mongo-cookbook-javadriver。这个示例使用一个 JUnit 测试用例来测试 Java 客户端的各种功能。在整个过程中,我们将使用一些最常见的 API 调用,因此学会如何使用它们。
如何做…
执行测试用例,可以在类似 Eclipse 的 IDE 中导入项目并执行测试用例,也可以使用 Maven 从命令提示符中执行测试用例。
我们将执行此示例的测试用例是com.packtpub.mongo.cookbook.MongoDriverQueryAndInsertTest。
-
如果您正在使用 IDE,请打开此测试类并将其执行为 JUnit 测试用例。
-
如果您计划使用 Maven 来执行此测试用例,请转到命令提示符,更改项目根目录下的目录,并执行以下命令来执行此单个测试用例:
$ mvn -Dtest=com.packtpub.mongo.cookbook.MongoDriverQueryAndInsertTest test
如果 Java SDK 和 Maven 已经正确设置,并且 MongoDB 服务器已经启动并监听端口27017以接收连接,那么一切都应该正常执行,测试用例应该成功。
工作原理…
我们现在将打开我们执行的测试类,并查看test方法中的一些重要的 API 调用。我们的test类的超类是com.packtpub.mongo.cookbook.AbstractMongoTest。
我们首先看一下这个类中的getClient方法。已创建的client实例是com.mongodb.MongoClient类型的实例。对于这个类有几个重载的构造函数;然而,我们使用以下方法来实例化客户端:
MongoClient client = new MongoClient("localhost:27017");
另一个要看的方法是在同一个抽象类中的getJavaDriverTestDatabase,它可以获取数据库实例。这个实例类似于 shell 中的隐式变量db。在 Java 中,这个类是com.mongodb.DB类型的实例。我们通过在客户端实例上调用getDB()方法来获取这个DB类的实例。在我们的情况下,我们想要javaDriverTest数据库的DB实例,我们可以通过以下方式获取:
getClient().getDB("javaDriverTest");
一旦我们获得了com.mongodb.DB的实例,我们就可以使用它来获得com.mongodb.DBCollection的实例,这将用于执行各种操作——在集合上进行find和insert操作。抽象测试类中的getJavaTestCollection方法返回DBCollection的一个实例。我们通过在com.mongodb.DB上调用getCollection()方法来获取javaTest集合的这个类的一个实例,如下所示:
getJavaDriverTestDatabase().getCollection("javaTest")
一旦我们获得了DBCollection的实例,我们现在可以对其执行操作。在这个示例的范围内,它仅限于find和insert操作。
现在,我们打开主测试用例类com.packtpub.mongo.cookbook.MongoDriverQueryAndInsertTest。在 IDE 或文本编辑器中打开这个类。我们将查看这个类中的方法。我们将首先查看的方法是findOneDocument。在这里,我们感兴趣的行是查询_id值为3的文档的行:collection.findOne(new BasicDBObject("_id", 3))。
该方法返回一个com.mongodb.DBObject的实例,它是一个键值映射,返回文档的字段作为键和相应键的值。例如,要从返回的DBObject实例中获取_id的值,我们在返回的结果上调用result.get("_id")。
我们要检查的下一个方法是getDocumentsFromTestCollection。这个测试用例在集合上执行了一个find操作,并获取其中的所有文档。collection.find()调用在DBCollection的实例上执行find操作。find操作的返回值是com.mongodb.DBCursor。值得注意的一点是,调用find操作本身并不执行查询,而只是返回DBCursor的实例。这是一个不消耗服务器端资源的廉价操作。实际查询只有在DBCursor实例上调用hasNext或next方法时才在服务器端执行。hasNext()方法用于检查是否有更多的结果,next()方法用于导航到结果中的下一个DBObject实例。对DBCursor实例返回的示例用法是遍历结果:
while(cursor.hasNext()) {
DBObject object = cursor.next();
//Some operation on the returned object to get the fields and
//values in the document
}
现在,我们来看一下withLimitAndSkip和withQueryProjectionAndSort两个方法。这些方法向我们展示了如何对结果进行排序、限制数量和跳过初始结果的数量。正如我们所看到的,排序、限制和跳过方法是链接在一起的:
DBCursor cursor = collection
.find(null)
.sort(new BasicDBObject("_id", -1))
.limit(2)
.skip(1);
所有这些方法都返回DBCursor的实例本身,允许我们链接调用。这些方法在DBCursor类中定义,根据它们在实例中执行的操作更改某些状态,并在方法的末尾返回相同的实例。
请记住,实际操作只有在DBCursor上调用hasNext或next方法时才在服务器上执行。在服务器上执行查询后调用任何方法,如sort、limit和skip,都会抛出java.lang.IllegalStateException。
我们使用了find方法的两种变体。一个接受一个参数用于执行查询,另一个接受两个参数——第一个用于查询,第二个是另一个DBObject,用于投影,将仅返回结果中文档的一组选定字段。
例如,测试用例的withQueryProjectionAndSort方法中的以下查询选择所有文档作为第一个参数为null,返回的DBCursor将包含只包含一个名为value的字段的文档:
DBCursor cursor = collection
.find(null, new BasicDBObject("value", 1).append("_id", 0))
.sort(new BasicDBObject("_id", 1));
_id字段必须显式设置为0,否则默认情况下将返回它。
最后,我们来看一下测试用例中的另外两个方法,insertDataTest和insertTestDataWithWriteConcern。在这两种方法中,我们使用了insert方法的几种变体。所有insert方法都在DBCollection实例上调用,并返回一个com.mongodb.WriteResult的实例。结果可以用于通过调用getLastError()方法获取写操作期间发生的错误,使用getN()方法获取插入的文档数量,以及操作的写关注。有关方法的更多详细信息,请参阅 MongoDB API 的 Javadoc。我们执行的两个插入操作如下:
collection.insert(new BasicDBObject("value", "Hello World"));
collection.insert(new BasicDBObject("value", "Hello World"), WriteConcern.JOURNALED);
这两个方法都接受一个DBObject实例作为要插入的文档的第一个参数。第二个方法允许我们提供用于write操作的写关注。DBCollection类中还有允许批量插入的insert方法。有关insert方法的各种重载版本的更多细节,请参考 Javadocs。
另请参阅…
MongoDB 驱动当前版本的 Javadocs 可以在api.mongodb.org/java/current/找到。
使用 Java 客户端执行更新和删除操作
在上一个配方中,我们看到了如何使用 Java 客户端在 MongoDB 中执行find和insert操作;在这个配方中,我们将看到 Java 客户端中更新和删除的工作原理。
准备工作
对于这个配方,我们将启动一个独立的实例。请参考第一章中的安装单节点 MongoDB配方,了解如何启动服务器的说明。
下一步是从 Packt 网站下载 Java 项目mongo-cookbook-javadriver。这个配方使用一个 JUnit 测试用例来测试 Java 客户端的各种功能。在整个过程中,我们将使用一些最常见的 API 调用,并学会使用它们。
如何操作…
要执行测试用例,可以在类似 Eclipse 的 IDE 中导入项目并执行测试用例,或者使用 Maven 从命令提示符中执行测试用例。
我们将为这个配方执行的测试用例是com.packtpub.mongo.cookbook.MongoDriverUpdateAndDeleteTest。
-
如果您使用的是 IDE,请打开这个测试类并将其作为 JUnit 测试用例执行。
-
如果您计划使用 Maven 执行此测试用例,请转到命令提示符,更改到项目的根目录,并执行以下命令来执行这个单个测试用例:
$ mvn -Dtest=com.packtpub.mongo.cookbook.MongoDriverUpdateAndDeleteTest test
如果 Java SDK 和 Maven 已经正确设置,并且 MongoDB 服务器正在运行并监听端口27017以接收连接,则一切都应该正常执行。
工作原理…
我们使用setupUpdateTestData()方法为配方创建了测试数据。在这里,我们只是将文档放在javaDriverTest数据库的javaTest集合中。我们向这个集合添加了 20 个文档,i的值范围从 1 到 20。这些测试数据在不同的测试用例方法中用于创建测试数据。
现在让我们看看这个类中的方法。我们首先看一下basicUpdateTest()。在这个方法中,我们首先创建测试数据,然后执行以下更新:
collection.update(
new BasicDBObject("i", new BasicDBObject("$gt", 10)),
new BasicDBObject("$set", new BasicDBObject("gtTen", true)));
这里的update方法接受两个参数。第一个是用于选择更新的符合条件的文档的查询,第二个参数是实际的更新。第一个参数由于嵌套的BasicDBObject实例看起来很混乱;然而,它是{'i' : {'$gt' : 10}}条件,第二个参数是更新,{'$set' : {'gtTen' : true}}。更新的结果是com.mongodb.WriteResult的一个实例。WriteResult的实例告诉我们更新的文档数量,执行write操作时发生的错误以及用于更新的写关注。更多细节请参考WriteConcern类的 Javadocs。默认情况下,此更新仅更新第一个匹配的文档,如果有多个文档匹配查询,则只更新第一个匹配的文档。
我们将要看的下一个方法是multiUpdateTest,它将更新给定查询的所有匹配文档,而不是第一个匹配的文档。我们使用的方法是在集合实例上使用updateMulti。updateMulti方法是更新多个文档的便捷方式。以下是我们调用的更新多个文档的方法:
collection.updateMulti(new BasicDBObject("i",
new BasicDBObject("$gt", 10)),
new BasicDBObject("$set", new BasicDBObject("gtTen", true)));
我们接下来做的操作是删除文档。删除文档的测试用例方法是deleteTest()。文档被删除如下:
collection.remove(new BasicDBObject(
"i", new BasicDBObject("$gt", 10)),
WriteConcern.JOURNALED);
我们有两个参数。第一个是查询,匹配的文档将从集合中删除。请注意,默认情况下,所有匹配的文档都将被删除,不像更新,更新默认情况下只会删除第一个匹配的文档。第二个参数是用于remove操作的写关注。
请注意,当服务器在 32 位机器上启动时,默认情况下禁用了日志记录。在这些机器上使用写关注可能会导致操作失败,并出现以下异常:
com.mongodb.CommandFailureException: { "serverUsed" : "localhost/127.0.0.1:27017" , "connectionId" : 5 , "n" : 0 , "badGLE" : { "getlasterror" : 1 , "j" : true} , "ok" : 0.0 , "errmsg" : "cannot use 'j' option when a host does not have journaling enabled" , "code" : 2}
这将需要服务器使用--journal选项启动。在 64 位机器上,默认情况下启用了日志记录,因此这是不必要的。
接下来我们将看看findAndModify操作。执行此操作的测试用例方法是findAndModifyTest。以下代码行用于执行此操作:
DBObject old = collection.findAndModify(
new BasicDBObject("i", 10),
new BasicDBObject("i", 100));
该操作是查询将找到匹配的文档,然后更新它们。该操作的返回类型是在应用更新之前的DBObject实例。findAndModify操作的一个重要特性是find和update操作是原子执行的。
前面的方法是findAndModify操作的简单版本。有一个重载版本的方法,签名如下:
DBObject findAndModify(DBObject query, DBObject fields, DBObject sort,boolean remove, DBObject update, boolean returnNew, boolean upsert)
让我们看看以下表中这些参数是什么:
| 参数 | 描述 |
|---|---|
query |
这是用于查询文档的查询。这是将被更新/删除的文档。 |
fields |
find方法支持选择结果文档中需要选择的字段的投影。此处的参数执行相同的操作,从结果文档中选择固定的字段集。 |
sort |
如果你还没有注意到,让我告诉你,该方法只能对一个文档执行这个原子操作,并且只返回一个文档。sort函数可以用于查询选择多个文档并且只选择第一个文档进行操作的情况。在选择要更新的第一个文档之前,sort函数被应用于结果。 |
remove |
这是一个布尔标志,指示是删除还是更新文档。如果该值为true,则将删除文档。 |
update |
与前面的属性不同,这不是一个布尔值,而是一个DBObject实例,它将告诉更新需要什么。请注意,remove布尔标志优先于此参数;如果remove属性为true,即使提供了更新,更新也不会发生。 |
returnNew |
查找操作返回一个文档,但是哪一个?在执行更新之前的文档还是在执行更新之后的文档?当给定为true时,这个布尔标志在更新执行后返回文档。 |
upsert |
这又是一个布尔标志,当为true时执行upsert。这仅在预期操作为更新时才相关。 |
此操作还有更多的重载方法。请参考com.mongodb.DBCollection的 Javadocs 以获取更多方法。我们使用的findAndModify方法最终调用了我们讨论的方法,其中字段和排序参数为空,其余参数remove,returnNew和upsert都为false。
最后,我们来看看 MongoDB 的 Java API 中的查询构建器支持。
mongo 中的所有查询都是DBObject实例,其中可能包含更多嵌套的DBObject实例。对于小查询来说很简单,但对于更复杂的查询来说会变得很丑陋。考虑一个相对简单的查询,我们想查询i > 10和i < 15的文档。这个 mongo 查询是{$and:[{i:{$gt:10}},{i:{$lt:15}}]}。在 Java 中编写这个查询意味着使用BasicDBObject实例,这甚至更加痛苦,如下所示:
DBObject query = new BasicDBObject("$and",
new BasicDBObject[] {
new BasicDBObject("i", new BasicDBObject("$gt", 10)),
new BasicDBObject("i", new BasicDBObject("$lt", 15))
});
然而,值得庆幸的是,有一个名为com.mongodb.QueryBuilder的类,这是一个用于构建复杂查询的实用类。前面的查询是使用查询构建器构建的,如下所示:
DBObject query = QueryBuilder.start("i").greaterThan(10).and("i").lessThan(15).get();
在编写查询时,这样做错误的可能性较小,而且阅读起来也很容易。com.mongodb.QueryBuilder类中有很多方法,我鼓励您阅读该类的 Javadocs。基本思想是使用start方法和键开始构建,然后链接方法调用以添加不同的条件,当添加各种条件完成时,使用get()方法构建查询,该方法返回DBObject。请参考测试类中的queryBuilderSample方法,了解 MongoDB Java API 的查询构建器支持的示例用法。
另请参阅
还有一些使用 GridFS 和地理空间索引的操作。我们将在高级查询章节中看到如何在 Java 应用程序中使用它们的小样本。请参考第五章中的高级操作了解这样的配方。
当前版本的 MongoDB 驱动程序的 Javadocs 可以在api.mongodb.org/java/current/找到。
使用 Java 客户端在 Mongo 中实现聚合
这个配方的目的不是解释聚合,而是向您展示如何使用 Java 客户端从 Java 程序实现聚合。在这个配方中,我们将根据州名对数据进行聚合,并获取出现在其中的文档数量最多的五个州名。我们将使用$project、$group、$sort和$limit操作符进行处理。
准备工作
用于此配方的测试类是com.packtpub.mongo.cookbook.MongoAggregationTest。要执行聚合操作,我们需要一个正在运行的服务器。一个简单的单节点就是我们需要的。请参考第一章中的安装单节点 MongoDB配方,了解如何启动服务器的说明。我们将操作的数据需要导入到数据库中。有关如何导入数据的步骤,请参阅第二章中的创建测试数据配方,命令行操作和索引。下一步是从 Packt 网站下载 Java 项目mongo-cookbook-javadriver。虽然可以使用 Maven 执行测试用例,但将项目导入 IDE 并执行测试用例类更加方便。假设您熟悉 Java 编程语言,并且习惯使用要导入的项目的 IDE。
操作步骤
要执行测试用例,可以将项目导入类似 Eclipse 的 IDE 中并执行测试用例,或者使用 Maven 从命令提示符中执行测试用例。
-
如果您使用的是 IDE,请打开测试类并将其执行为 JUnit 测试用例。
-
如果您计划使用 Maven 执行此测试用例,请转到命令提示符,更改项目根目录下的目录,并执行以下命令以执行此单个测试用例:
$ mvn -Dtest=com.packtpub.mongo.cookbook.MongoAggregationTesttest
如果 Java SDK 和 Maven 设置正确,并且 MongoDB 服务器正在运行并监听端口27017以进行传入连接,那么一切都应该正常执行。
工作原理
我们测试类中用于聚合功能的方法是aggregationTest()。聚合操作是在 Java 客户端上使用DBCollection类中定义的aggregate()方法对 MongoDB 执行的。该方法具有以下签名:
AggregationOutput aggregate(firstOp, additionalOps)
只有第一个参数是必需的,它形成管道中的第一个操作。第二个参数是varagrs参数(零个或多个值的可变数量的参数),允许更多的管道操作符。所有这些参数都是com.mongodb.DBObject类型。如果在执行聚合命令时发生任何异常,聚合操作将抛出com.mongodb.MongoException,并带有异常的原因。
返回类型com.mongodb.AggregationOutput用于获取聚合操作的结果。从开发人员的角度来看,我们更感兴趣的是这个实例的results字段,可以使用返回对象的results()方法来访问。results()方法返回一个Iterable<DBObject>类型的对象,可以迭代以获取聚合的结果。
让我们看看我们如何在测试类中实现了聚合管道:
AggregationOutput output = collection.aggregate(
//{'$project':{'state':1, '_id':0}},
new BasicDBObject("$project", new BasicDBObject("state", 1).append("_id", 0)),
//{'$group':{'_id':'$state', 'count':{'$sum':1}}}
new BasicDBObject("$group", new BasicDBObject("_id", "$state")
.append("count", new BasicDBObject("$sum", 1))),
//{'$sort':{'count':-1}}
new BasicDBObject("$sort", new BasicDBObject("count", -1)),
//{'$limit':5}
new BasicDBObject("$limit", 5)
);
在以下顺序中,管道中有四个步骤:$project操作,然后是$group,$sort,然后是$limit。
最后两个操作看起来效率低下,我们首先对所有元素进行排序,然后只取前五个元素。在这种情况下,MongoDB 服务器足够智能,可以在排序时考虑限制操作,只需维护前五个结果而不是对所有结果进行排序。
对于 MongoDB 2.6 版本,聚合结果可以返回一个游标。尽管前面的代码仍然有效,但AggregationResult对象不再是获取操作结果的唯一方式。我们可以使用com.mongodb.Cursor来迭代结果。此外,前面的格式现在已被弃用,而是采用了接受管道操作符列表而不是操作符varargs的格式。请参考com.mongodb.DBCollection类的 Javadocs,并查看各种重载的aggregate()方法。
在 Java 客户端中执行 Mongo 中的 MapReduce
在我们之前的食谱中,使用 Java 客户端在 Mongo 中实现聚合,我们看到了如何使用 Java 客户端在 Mongo 中执行聚合操作。在这个食谱中,我们将处理与聚合操作相同的用例,但我们将使用 MapReduce。目的是根据州名对数据进行聚合,并获取出现在其中的文档数量最多的五个州名。
如果有人不知道如何从编程语言客户端为 Mongo 编写 MapReduce 代码,并且是第一次看到它,你可能会惊讶地看到它实际上是如何完成的。你可能想象过,你会在编写代码的编程语言中(在这种情况下是 Java)编写map和reduce函数,然后使用它来执行 map reduce。然而,我们需要记住,MapReduce 作业在 mongo 服务器上运行,并执行 JavaScript 函数。因此,无论编程语言驱动程序如何,map reduce 函数都是用 JavaScript 编写的。编程语言驱动程序只是让我们调用和执行服务器上用 JavaScript 编写的 map reduce 函数的手段。
准备工作
用于此示例的测试类是com.packtpub.mongo.cookbook.MongoMapReduceTest。要执行 map reduce 操作,我们需要运行一个服务器。我们只需要一个简单的单节点。有关如何启动服务器的说明,请参阅第一章中的安装单节点 MongoDB配方,安装和启动服务器。我们将操作的数据需要导入数据库。有关导入数据的步骤,请参阅第二章中的创建测试数据配方,命令行操作和索引。下一步是从 Packt 网站下载 Java 项目mongo-cookbook-javadriver。虽然可以使用 Maven 执行测试用例,但将项目导入 IDE 并执行测试用例类更加方便。假设您熟悉 Java 编程语言,并且习惯使用将要导入项目的 IDE。
如何做…
要执行测试用例,可以在类似 Eclipse 的 IDE 中导入项目并执行测试用例,也可以使用 Maven 从命令提示符中执行测试用例。
-
如果使用 IDE,打开测试类并将其作为 JUnit 测试用例执行。
-
如果您计划使用 Maven 执行此测试用例,请转到命令提示符,将目录更改为项目的根目录,并执行以下命令以执行此单个测试用例:
$ mvn -Dtest=com.packtpub.mongo.cookbook.MongoMapReduceTesttest
如果 Java SDK 和 Maven 设置正确,并且 MongoDB 服务器正常运行并监听端口27017以接收连接,则一切都应该能够正常执行。
工作原理…
我们 map reduce 测试的测试用例方法是mapReduceTest()。
可以使用DBCollection类中定义的mapReduce()方法在 Java 客户端中对 Mongo 进行 map reduce 操作。有很多重载版本,您可以参考com.mongodb.DBCollection类的 Javadocs,了解此方法的各种用法。我们使用的是collection.mapReduce(mapper, reducer, output collection, query)。
该方法接受以下四个参数:
-
mapper函数的类型为 String,是在 mongo 数据库服务器上执行的 JavaScript 代码 -
reducer函数的类型为 String,是在 mongo 数据库服务器上执行的 JavaScript 代码 -
map reduce 执行的输出将写入的集合的名称
-
服务器将执行的查询,此查询的结果将作为 map reduce 作业执行的输入
假设读者对 shell 中的 map reduce 操作很熟悉,我们不会解释在测试用例方法中使用的 map reduce JavaScript 函数。它所做的就是将键作为州的名称和值进行发射,这些值是特定州名出现的次数。此结果将添加到输出集合javaMROutput中。例如,在整个集合中,州Maharashtra出现了6446次;因此,Maharashtra州的文档是{'_id': 'Maharashtra', 'value': 6446}。要确认这是否是真实值,可以在 mongo shell 中执行以下查询,并查看结果是否确实为6446:
> db.postalCodes.count({state:'Maharashtra'})
6446
我们还没有完成,因为要找到集合中出现次数最多的五个州;我们只有州和它们的出现次数,所以最后一步是按照value字段对文档进行排序,这个字段是州名出现的次数,按降序排列,并将结果限制为五个文档。
另请参阅
参考第八章,“与 Hadoop 集成”中有关在 MongoDB 中使用 Hadoop 连接器执行 Map Reduce 作业的不同方法。这使我们能够使用 Java、Python 等语言编写Map和Reduce函数。
第四章:管理
在本章中,我们将看到以下与 MongoDB 管理相关的配方:
-
重命名集合
-
查看集合统计信息
-
查看数据库统计信息
-
手动填充文档
-
mongostat 和 mongotop 实用程序
-
获取当前正在执行的操作并终止它们
-
使用分析器对操作进行分析
-
在 Mongo 中设置用户
-
Mongo 中的进程间安全性
-
使用 collMod 命令修改集合行为
-
将 MongoDB 设置为 Windows 服务
-
副本集配置
-
从副本集中降级为主要
-
探索副本集的本地数据库
-
理解和分析 oplogs
-
构建带标签的副本集
-
为非分片集合配置默认分片
-
手动拆分和迁移块
-
使用标签进行领域驱动分片
-
在分片设置中探索配置数据库
介绍
在本章中,我们将介绍一些用于管理 MongoDB 的工具和实践。以下配方将帮助您从数据库中收集统计信息,管理用户访问权限,分析 oplogs,并了解与副本集工作的一些方面。
重命名集合
您是否曾经遇到过这样的情况:在关系数据库中命名了一个表,后来觉得名字可能会更好?或者也许您所在的组织迟迟没有意识到表名真的很混乱,并对名称强制执行一些标准?关系数据库确实有一些专有的方法来重命名表,数据库管理员会为您做这件事。
不过,这也带来了一个问题。在 Mongo 世界中,集合等同于表,创建后是否有办法将集合重命名为其他名称?在这个配方中,我们将探索 Mongo 的这个特性,即在集合中有数据的情况下重命名现有集合。
准备工作
我们需要运行一个 MongoDB 实例来执行这个集合重命名实验。参考第一章中的安装单节点 MongoDB一节,了解如何启动服务器的信息。我们将要执行的操作将来自 mongo shell。
如何做…
- 一旦服务器启动,并假设它在默认端口
27017上监听客户端连接,从 shell 执行以下命令连接到它:
> mongo
- 连接后,使用默认的测试数据库。让我们创建一个带有一些测试数据的集合。我们将使用的集合名称是:
sloppyNamedCollection.
> for(i = 0 ; i < 10 ; i++) { db.sloppyNamedCollection.insert({'i':i}) };
-
现在将创建测试数据(我们可以通过查询集合
sloppyNamedCollection来验证数据)。 -
将集合
neatNamedCollection重命名为:
> db.sloppyNamedCollection.renameCollection('neatNamedCollection')
{ "ok" : 1 }
- 通过执行以下命令验证
slappyNamedCollection集合是否不再存在:
> show collections
- 最后,查询
neatNamedCollection集合,验证最初在sloppyNamedCollection中的数据确实存在其中。只需在 mongo shell 上执行以下命令:
> db.neatNamedCollection.find()
它是如何工作的…
重命名集合非常简单。它是通过renameCollection方法实现的,该方法接受两个参数。通常,函数签名如下:
> db.<collection to rename>.renameCollection('<target name of the collection>', <drop target if exists>)
第一个参数是要将集合重命名为的名称。
我们没有使用的第二个参数是一个布尔值,告诉命令是否删除目标集合(如果存在)。这个值默认为 false,这意味着不要删除目标,而是报错。这是一个明智的默认值,否则如果我们意外给出一个存在的集合名称并且不希望删除它,结果会很可怕。但是,如果你知道自己在做什么,并且希望在重命名集合时删除目标,将第二个参数传递为 true。这个参数的名称是dropTarget。在我们的情况下,调用应该是:
> db.sloppyNamedCollection.renameCollection('neatNamedCollection', true)
作为练习,尝试再次创建sloppyNamedCollection并将其重命名为没有第二个参数(或 false 作为值)。您应该看到 mongo 抱怨目标命名空间已存在。然后,再次使用第二个参数重命名为 true,现在重命名操作执行成功。
请注意,重命名操作将保留原始的和新重命名的集合在同一个数据库中。这个renameCollection方法不足以将集合移动/重命名到另一个数据库。在这种情况下,我们需要运行类似于以下命令的renameCollection命令:
> db.runCommand({ renameCollection: "<source_namespace>", to: "<target_namespace>", dropTarget: <true|false> });
假设我们想要将集合sloppyNamedCollection重命名为neatNamedCollection,并将其从test数据库移动到newDatabase,我们可以通过执行以下命令来执行此操作。请注意,使用的dropTarget: true开关旨在删除现有的目标集合(newDatabase.neatNamedCollection)(如果存在)。
> db.runCommand({ renameCollection: "test.sloppyNamedCollection ", to: " newDatabase.neatNamedCollection", dropTarget: true });
另外,重命名集合操作不适用于分片集合。
查看集合统计信息
也许在管理目的上,关于存储使用情况的一个有趣的统计数据是集合中文档的数量,可能可以根据数据的增长来估算未来的空间和内存需求,以获得集合的高级统计信息。
准备就绪
要查找集合的统计信息,我们需要运行一个服务器,并且一个单节点应该是可以的。有关如何启动服务器的信息,请参阅第一章中的安装单节点 MongoDB,安装和启动服务器。我们将要操作的数据需要导入到数据库中。导入数据的步骤在第二章的创建测试数据中给出。完成这些步骤后,我们就可以继续进行本教程了。
如何做…
-
我们将使用
postalCodes集合来查看统计信息。 -
打开 mongo shell 并连接到正在运行的 MongoDB 实例。如果您在默认端口上启动了 mongo,请执行以下操作:
$ mongo
- 导入数据后,如果
pincode字段上不存在索引,则在该字段上创建一个索引:
> db.postalCodes.ensureIndex({'pincode':1})
- 在 mongo 终端上执行以下操作:
> db.postalCodes.stats()
- 观察输出并在 shell 上执行以下操作:
> db.postalCodes.stats(1024)
- 再次观察输出。
接下来,我们将看看这些打印出的值对我们意味着什么。
它是如何工作的…
如果我们观察这两个命令的输出,我们会发现第二个命令中的所有数字都是以 KB 为单位,而第一个命令中的数字是以字节为单位。提供的参数称为比例,所有指示大小的数字都会除以这个比例。在这种情况下,由于我们给出的值是1024,我们得到的所有值都是以 KB 为单位,而如果将1024 * 1024作为比例的值(显示的大小将以 MB 为单位)。对于我们的分析,我们将使用以 KB 显示大小的值。
> db.postalCodes.stats(1024)
{
"ns" : "test.postalCodes",
"count" : 39732,
"size" : 9312,
"avgObjSize" : 240,
"numExtents" : 6,
"storageSize" : 10920,
"lastExtentSize" : 8192,
"paddingFactor" : 1,
"paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0\. It remains hard coded to 1.0 for compatibility only.",
"userFlags" : 1,
"capped" : false,
"nindexes" : 2,
"totalIndexSize" : 2243,
"indexSizes" : {
"_id_" : 1261,
"pincode_1" : 982
},
"ok" : 1
}
以下表格显示了重要字段的含义:
| Field | Description |
|---|---|
ns |
以<database>.<collection name>格式的集合的完全限定名称。 |
count |
集合中的文档数量。 |
size |
集合中文档占用的实际存储空间大小。对集合中文档的添加、删除和更新可能会改变此数字。比例参数会影响此字段的值,在我们的情况下,此值以 KB 为单位,因为1024是比例。此数字包括填充(如果有)。 |
avgObjSize |
这是集合中文档的平均大小。它只是大小字段除以集合中文档的计数(前两个字段)。比例参数会影响此字段的值,在我们的情况下,此值以 KB 为单位,因为1024是比例。 |
storageSize |
Mongo 在磁盘上预先分配空间,以确保集合中的文档保持在连续的位置,以提供更好的磁盘访问性能。这种预分配会用零填充文件,然后开始为插入的文档分配空间。该字段告诉此集合使用的存储空间大小。这个数字通常会比集合的实际大小大得多。比例参数影响此字段的值,在我们的情况下,此值以 KB 为单位,因为比例为1024。 |
numExtents |
正如我们所看到的,Mongo 为了性能目的而预先分配了连续的磁盘空间给集合。然而,随着集合的增长,需要分配新的空间。该字段给出了这种连续块分配的数量。这个连续的块称为一个区段。 |
nindexes |
该字段给出了集合上存在的索引的数量。即使我们没有在集合上创建索引,该值也将为1,因为 Mongo 会在字段_id上隐式创建一个索引。 |
lastExtentSize |
分配的最后一个区段的大小。比例参数影响此字段的值,在我们的情况下,此值以 KB 为单位,因为比例为1024。 |
paddingFactor |
自 3.0.0 版本起,此参数已被弃用,并且由于向后兼容性原因已硬编码为1。 |
totalIndexSize |
索引也占用存储空间。该字段给出了磁盘上索引占用的总大小。比例参数影响此字段的值,在我们的情况下,此值以 KB 为单位,因为比例为1024。 |
indexSizes |
该字段是一个文档,其键是索引的名称,值是所讨论的索引的大小。在我们的情况下,我们在pincode字段上显式创建了一个索引;因此,我们看到索引的名称作为键,磁盘上索引的大小作为值。所有这些索引的值的总和与先前给出的值totalIndexSize相同。比例参数影响此字段的值,在我们的情况下,此值以 KB 为单位,因为比例为1024。 |
文档被放置在存储设备上的连续位置。如果文档被更新,导致大小增加,Mongo 将不得不重新定位这个文档。这个操作会变得昂贵,影响这样的更新操作的性能。从 Mongo 3.0.0 开始,使用了两种数据分配策略。一种是2 的幂,其中文档以 2 的幂分配空间(例如,32、64、128 等)。另一种是无填充,其中集合不希望文档大小被改变。
另请参阅
在这个配方中,我们讨论了查看集合的统计信息。查看下一个配方以在数据库级别查看统计信息。
查看数据库统计信息
在上一个配方中,我们看到了如何从管理角度查看集合的一些重要统计信息。在这个配方中,我们得到了一个更高的视角,获得了数据库级别的这些(或大部分)统计信息。
准备工作
要查找数据库的统计信息,我们需要运行一个服务器,一个单节点应该是可以的。有关如何启动服务器的信息,请参阅第一章中的配方安装单节点 MongoDB,安装和启动服务器。我们将要操作的数据需要导入到数据库中。有关如何导入数据的步骤,请参阅第二章中的配方创建测试数据,命令行操作和索引。完成这些步骤后,我们就可以继续进行这个配方了。如果需要查看如何在集合级别查看统计信息,请参阅上一个配方。
如何做…
-
我们将使用
test数据库来完成此配方的目的。它已经在其中有一个postalCodes集合。 -
使用 mongo shell 连接到服务器,通过在操作系统终端中输入以下命令。假设服务器正在监听端口
27017。
$ mongo
- 在 shell 上,执行以下命令并观察输出:
> db.stats()
- 在 shell 上,再次执行以下命令,但这次我们添加了 scale 参数。观察输出:
> db.stats(1024)
它是如何工作的…
scale参数是stats函数的一个参数,它将字节数除以给定的 scale 值。在这种情况下,它是1024,因此所有值将以 KB 为单位。我们分析以下输出:
> db.stats(1024)
{
"db" : "test",
"collections" : 3,
"objects" : 39738,
"avgObjSize" : 143.32699179626553,
"dataSize" : 5562,
"storageSize" : 16388,
"numExtents" : 8,
"indexes" : 2,
"indexSize" : 2243,
"fileSize" : 196608,
"nsSizeMB" : 16,
"extentFreeList" : {
"num" : 4,
"totalSize" : 2696
},
"dataFileVersion" : {
"major" : 4,
"minor" : 5
},
"ok" : 1
}
以下表格显示了重要字段的含义:
| 字段 | 描述 |
|---|---|
db |
这是正在查看统计信息的数据库的名称。 |
collections |
这是数据库中集合的总数。 |
objects |
这是数据库中所有集合中文档的计数。如果我们使用db.<collection>.stats()查找集合的统计信息,我们会得到集合中文档的计数。这个属性是数据库中所有集合计数的总和。 |
avgObjectSize |
这只是数据库中所有集合中所有对象的字节大小除以所有集合中文档的计数。这个值不受提供的 scale 影响,尽管这是一个size字段。 |
dataSize |
这是数据库中所有集合中保存的数据的总大小。这个值受提供的 scale 影响。 |
storageSize |
这是为存储文档而分配给该数据库中集合的总存储量。这个值受提供的 scale 影响。 |
numExtents |
这是数据库中所有集合的 extent 数量的总数。这基本上是该数据库中集合统计信息中 extent(逻辑容器)的数量。 |
indexes |
这是数据库中所有集合的索引数量的总和。 |
indexSize |
这是数据库中所有集合的所有索引的字节大小。这个值受提供的 scale 影响。 |
fileSize |
这是应该在文件系统中找到的该数据库的所有数据库文件的大小总和。文件的名称将是test.0,test.1等等。这个值受提供的 scale 影响。 |
nsSizeMB |
这是数据库的.ns文件的大小(以 MB 为单位)。 |
extentFreeList.num |
这是空闲列表中空闲 extent 的数量。你可以将 extent 看作是 MongoDB 的内部数据结构。 |
extentFreeList.totalSize |
空闲列表上 extent 的大小。 |
要了解更多信息,你可以参考《Instant MongoDB》这样的书籍,由Packt Publishing出版(www.packtpub.com/big-data-and-business-inteliigence/instant-mongodb-instant)。
它是如何工作的…
让我们从collections字段开始。如果你仔细观察数字,并在 mongo shell 上执行show collections命令,你会发现与执行命令时相比,统计信息中多了一个隐藏的集合。这个差异是因为有一个隐藏的集合,它的名称是system.namespaces。你可以执行db.system.namespaces.find()来查看它的内容。
回到数据库上的统计操作的输出,结果中的对象字段也有一个有趣的值。如果我们在postalCodes集合中找到文档的数量,我们会发现它是39732。这里显示的数量是39738,这意味着还有六个文档。这六个文档来自system.namespaces和system.indexes集合。在这两个集合上执行计数查询将予以确认。请注意,test数据库除了postalCodes之外不包含任何其他集合。如果数据库包含更多包含文档的集合,这些数字将会改变。
还要注意avgObjectSize的值,这个值有点奇怪。与集合统计信息中的这个字段不同,该字段受所提供的比例值的影响,在数据库统计信息中,该值始终以字节为单位。这很令人困惑,我不太确定为什么这个值不根据提供的比例进行缩放。
手动填充文档
在不深入存储的内部细节的情况下,MongoDB 使用内存映射文件,这意味着数据存储在文件中,就像存储在内存中一样,并且它会使用低级别的操作系统服务将这些页面映射到内存中。文档存储在 mongo 数据文件中的连续位置,当文档增长并且不再适合空间时会出现问题。在这种情况下,mongo 会将文档重写到集合的末尾,并清理原来放置的空间(请注意,这个空间不会作为空闲空间释放给操作系统)。
对于不希望文档增长的应用程序来说,这不是一个大问题。然而,对于那些预期文档在一段时间内增长并且可能有很多这样的文档移动的人来说,这是一个很大的性能损失。随着 MongoDB 3.0 的发布,Power of 2方法成为了默认的大小分配策略。顾名思义,这种方法将文档存储在以 2 的幂分配的空间中。这不仅为文档提供了额外的填充,还更好地重用了由于文档的重定位或删除而导致的空闲空间。
也就是说,如果你仍然希望在你的策略中引入手动填充,继续阅读。
准备工作
这个食谱不需要任何东西,除非你打算尝试这个简单的技术,如果是这样,你需要一个正在运行的单个实例。有关如何启动服务器的信息,请参阅第一章中的食谱安装单节点 MongoDB,安装和启动服务器。
如何做到这一点...
这种技术的想法是向要插入的文档添加一些虚拟数据。这些虚拟数据的大小加上文档中的其他数据大致等于文档的预期大小。
例如,如果文件的平均大小在一段时间内估计为 1200 字节,而在插入文件时存在 300 字节的数据,我们将添加一个大小约为 900 字节的虚拟字段,以使总文件大小达到 1200 字节。
一旦插入文档,我们取消这个虚拟字段,这样在两个连续文档之间留下一个空隙。当文档随着时间的推移增长时,这个空白空间将被使用,最大限度地减少文档的移动。这个空白空间也可能被另一个文档使用。更加可靠的方法是只有在使用空间时才删除填充。然而,任何超出预期平均增长的文档都将被服务器复制到集合的末尾。不用说,没有达到预期大小的文档将倾向于浪费磁盘空间。
应用程序可以提出一些智能策略,也许根据文档的某个特定字段调整填充字段的大小,以解决这些缺陷,但这取决于应用程序开发人员。
现在让我们看一下这种方法的示例:
- 我们定义一个小函数,它将向文档添加一个名为
padField的字段,并将字符串值的数组添加到文档中。其代码如下:
function padDocument(doc) {
doc.padField = []
for(i = 0 ; i < 20 ; i++) {
doc.padField[i] = 'Dummy'
}
}
它将添加一个名为padField的数组,并添加 20 次名为Dummy的字符串。对于您添加到文档中的类型和添加的次数没有限制,只要它占用您所需的空间。上述代码只是一个示例。
- 下一步是插入一个文档。我们将定义另一个名为
insert的函数来执行:
function insert(collection, doc) {
//1\. Pad the document with the padField
padDocument(doc);
//2\. Create or store the _id field that would be used later
if(typeof(doc._id) == 'undefined') {
_id = ObjectId()
doc._id = _id
}
else {
_id = doc._id
}
//3\. Insert the document with the padded field
collection.insert(doc)
//4\. Remove the padded field, use the saved _id to find the document to be updated.
collection.update({'_id':_id}, {$unset:{'padField':1}})
}
- 现在我们将通过在集合
testCol中插入一个文档来将其付诸实践:
insert(db.testCol, {i:1})
- 您可以使用以下查询查询
testCol,并检查插入的文档是否存在:
> db.testCol.findOne({i:1})
请注意,在查询时,您将在其中找不到padField。但是,即使未设置该字段,数组占用的空间仍将保留在随后插入的文档之间。
它是如何工作的…
insert函数是不言自明的,并且其中有注释来说明它的作用。一个明显的问题是,我们如何相信这确实是我们打算做的事情。为此,我们将进行一个小活动如下。我们将在manualPadTest集合上进行这个目的。从 mongo shell 执行以下操作:
> db.manualPadTest.drop()
> db.manualPadTest.insert({i:1})
> db.manualPadTest.insert({i:2})
> db.manualPadTest.stats()
在统计信息中注意avgObjSize字段。接下来,从 mongo shell 执行以下操作:
> db.manualPadTest.drop()
> insert(db.manualPadTest , {i:1})
> insert(db.manualPadTest , {i:2})
> db.manualPadTest.stats()
在统计信息中注意avgObjSize字段。这个数字比我们之前看到的普通插入的数字要大得多。paddingFactor在这两种情况下仍然是 1,但后一种情况为文档提供了更多的缓冲区。
在这个示例中我们使用的insert函数中,插入到集合和更新文档操作不是原子的。
mongostat 和 mongotop 实用程序
大多数人可能会发现这些名称与两个流行的 Unix 命令iostat和top相似。对于 MongoDB,mongostat和mongotop是两个实用程序,它们的工作与这两个 Unix 命令几乎相同,毫无疑问,它们用于监视 mongo 实例。
准备工作
在这个示例中,我们将通过运行一个脚本来模拟独立 mongo 实例上的一些操作,该脚本将尝试使您的服务器保持繁忙,然后在另一个终端中,我们将运行这些实用程序来监视db实例。
您需要启动一个独立的服务器来监听任何端口以进行客户端连接;在这种情况下,我们将坚持使用默认的27017端口。如果您不知道如何启动独立服务器,请参阅第一章中的安装单节点 MongoDB,安装和启动服务器。我们还需要从 Packt 网站下载脚本KeepServerBusy.js并将其放在本地驱动器上以备执行。还假定您的 mongo 安装的bin目录存在于操作系统的路径变量中。如果没有,那么这些命令需要在 shell 中使用可执行文件的绝对路径来执行。这两个实用程序mongostat和mongotop是与 mongo 安装一起提供的标准工具。
如何做…
-
启动 MongoDB 服务器,并让它监听默认端口以进行连接。
-
在另一个终端中,执行提供的 JavaScript
KeepServerBusy.js如下:
$ mongo KeepServerBusy.js –quiet
- 打开一个新的操作系统终端并执行以下命令:
$ mongostat
-
捕获一段时间的输出内容,然后按下Ctrl + C停止命令捕获更多的统计信息。保持终端打开或将统计信息复制到另一个文件中。
-
现在,从终端执行以下命令:
$ mongotop
-
捕获输出内容一段时间,然后按Ctrl + C停止命令捕获更多统计信息。保持终端打开或将统计信息复制到另一个文件中。
-
在执行提供的 JavaScript
KeepServerBusy.js的 shell 中按Ctrl + C停止使服务器保持繁忙的操作。
工作原理…
让我们看看我们从这两个实用程序中捕获到了什么。
我们首先分析mongostat。在我的笔记本电脑上,使用mongostat进行捕获如下:
mongostat
connected to: 127.0.0.1
insert query update delete getmore command flushes mapped vsize res faults idx miss % qr|qw ar|aw netIn netOut conn time
1000 1 950 1000 1 1|0 0 624.0M 1.4G 50.0M 0 0 0|0 0|1 431k 238k 2 08:59:21
1000 1 1159 1000 1 1|0 0 624.0M 1.4G 50.0M 0 0 0|0 0|0 468k 252k 2 08:59:22
1000 1 984 1000 1 1|0 0 624.0M 1.4G 50.0M 0 0 0|0 0|1 437k 240k 2 08:59:23
1000 1 1066 1000 1 1|0 0 624.0M 1.4G 50.0M 0 0 0|0 0|1 452k 246k 2 08:59:24
1000 1 944 1000 1 2|0 0 624.0M 1.4G 50.0M 0 0 0|0 0|1 431k 237k 2 08:59:25
1000 1 1149 1000 1 1|0 0 624.0M 1.4G 50.0M 0 0 0|0 0|1 466k 252k 2 08:59:26
1000 2 1015 1053 2 1|0 0 624.0M 1.4G 50.0M 0 0 0|0 0|0 450k 293k 2 08:59:27
您可以选择查看脚本KeepServerBusy.js是如何使服务器保持繁忙的。它所做的就是在monitoringTest集合中插入 1000 个文档,然后逐个更新它们以设置一个新的键,执行查找并遍历所有文档,最后逐个删除它们,基本上是一个写入密集型操作。
输出看起来很丑陋,但让我们逐个分析字段,看看需要关注的字段。
| 列 | 描述 |
|---|---|
insert,query,update,delete |
前四列是每秒insert,query,update和delete操作的次数。这是每秒的,因为捕获这些数字的时间间隔相隔一秒,这由最后一列表示。 |
getmore |
当游标对查询的数据用尽时,它会在服务器上执行getmore操作,以获取之前执行的查询的更多结果。此列显示在此给定的 1 秒时间范围内执行的getmore操作的次数。在我们的情况下,并没有执行太多getmore操作。 |
commands |
这是在给定的 1 秒时间范围内在服务器上执行的命令数量。在我们的情况下,并不多,只有一个。在我们的情况下,|后面的数字是0,因为这是独立模式。尝试连接到副本集主服务器和次服务器执行mongostat。你应该在那里看到稍微不同的数字。 |
flushes |
这是在 1 秒间隔内将数据刷新到磁盘的次数。(在MMAPv1存储引擎的情况下是fsync,在WiredTiger存储引擎的情况下是在轮询间隔之间触发的检查点) |
mapped,virtual和resident memory |
映射内存是 Mongo 进程映射到数据库的内存量。这通常与数据库的大小相同。另一方面,虚拟内存是分配给整个mongod进程的内存。当启用日志记录时,这将是映射内存大小的两倍以上。最后,常驻内存是 Mongo 实际使用的物理内存。所有这些数字以 MB 为单位给出。物理内存的总量可能比 Mongo 使用的内存多得多,但除非发生大量页面错误(在先前提到的输出中确实会发生),否则这并不是一个问题。 |
faults |
这些是每秒发生的页面错误次数。这些数字应尽可能少。它表示 Mongo 需要多少次去磁盘获取在主内存中缺失的文档/索引。当使用 SSD 作为持久存储时,这个问题不像使用旋转磁盘驱动器时那么严重。 |
locked |
自 2.2 版本以来,对集合的所有写操作都会锁定包含该集合的数据库,并且不会获取全局级别的锁。此字段显示在给定的时间间隔内大部分时间被锁定的数据库。在我们的情况下,test数据库大部分时间被锁定。 |
idx miss % |
此字段给出了需要特定索引但在内存中不存在的次数。这会导致页面错误,并且需要访问磁盘以获取索引。可能还需要另一次磁盘访问以获取文档。这个数字也应该很低。高百分比的索引缺失是需要关注的问题。 |
qr | qw |
这些是等待执行的读取和写入的排队数。如果这个数字增加,表明数据库受到了读取和写入量的压倒。如果值太高,要密切关注页面错误和数据库锁定百分比,以便更深入地了解排队计数的增加。如果数据集太大,分片集合可以显著提高性能。 |
ar | aw |
这是活动读者和写者(客户端)的数量。只要其他我们之前看到的统计数据都在控制之下,即使数量很大,也不用担心。 |
netIn和netOut |
在给定时间范围内,mongo 服务器的网络流量进出。数字以位为单位。例如,271k 表示 271 千位。 |
conn |
这表示打开的连接数。要密切关注,看看是否会不断增加。 |
time |
这是捕获此样本时的时间间隔。 |
如果mongostat连接到副本集的主服务器或从服务器,会看到一些更多的字段。作为一个任务,一旦收集到统计数据或独立实例,启动一个副本集服务器并执行相同的脚本以使服务器保持繁忙。使用mongostat连接到主服务器和从服务器实例,并查看不同的统计数据。
除了mongostat,我们还使用了mongotop实用程序来捕获统计数据。让我们看看它的输出并理解一些:
$>mongotop
connected to: 127.0.0.1
ns total read write
2014-01-15T17:55:13
test.monitoringTest 899ms 1ms 898ms
test.system.users 0ms 0ms 0ms
test.system.namespaces 0ms 0ms 0ms
test.system.js 0ms 0ms 0ms
test.system.indexes 0ms 0ms 0ms
ns total read write
2014-01-15T17:55:14
test.monitoringTest 959ms 0ms 959ms
test.system.users 0ms 0ms 0ms
test.system.namespaces 0ms 0ms 0ms
test.system.js 0ms 0ms 0ms
test.system.indexes 0ms 0ms 0ms
ns total read write
2014-01-15T17:55:15
test.monitoringTest 954ms 1ms 953ms
test.system.users 0ms 0ms 0ms
test.system.namespaces 0ms 0ms 0ms
test.system.js 0ms 0ms 0ms
test.system.indexes 0ms 0ms 0ms
在这个统计数据中没有太多可看的。我们看到数据库在给定的 1 秒时间片段内忙于读取或写入的总时间。总时间中给定的值将是读取和写入时间的总和。如果我们实际上比较相同时间片段的mongotop和mongostat,那么写入正在进行的时间所占的百分比将非常接近mongostat输出中数据库被锁定的百分比。
mongotop命令接受命令行上的参数,如下所示:
$ mongotop 5
在这种情况下,打印统计数据的时间间隔将是 5 秒,而不是默认值 1 秒。
注意
从 MongoDB 3.0 开始,mongotop和mongostat实用程序都允许使用--json选项以 JSON 格式输出。如果您要使用自定义监视或度量收集脚本,这可能非常有用,这些脚本将依赖于这些实用程序。
另请参阅
-
在获取当前执行操作并终止它们的示例中,我们将看到如何从 shell 获取当前执行的操作,并在需要时终止它们。
-
在使用分析器来分析操作的示例中,我们将看到如何使用 Mongo 的内置分析功能来记录操作执行时间。
获取当前执行操作并终止它们
在这个示例中,我们将看到如何查看当前运行的操作,并终止一些长时间运行的操作。
准备工作
我们将在独立的 mongo 实例上模拟一些操作。我们需要启动一个独立服务器,以便监听任何端口以进行客户端连接;在这种情况下,我们将使用默认的27017端口。如果您不知道如何启动独立服务器,请参阅第一章中的安装单节点 MongoDB,安装和启动服务器。我们还需要启动两个连接到已启动服务器的 shell。一个 shell 将用于后台索引创建,另一个将用于监视当前操作,然后终止它。
如何做…
-
我们无法在测试环境中模拟实际长时间运行的操作。我们将尝试创建一个索引,并希望它需要很长时间来创建。根据您的目标硬件配置,该操作可能需要一些时间。
-
要开始这个测试,让我们在 mongo shell 上执行以下操作:
> db.currentOpTest.drop()
> for(i = 1 ; i < 10000000 ; i++) { db.currentOpTest.insert({'i':i})}
前面的插入可能需要一些时间来插入 1000 万个文档。
-
一旦文档被插入,我们将执行一个操作,该操作将在后台创建索引。如果您想了解更多关于索引创建的信息,请参考第二章中的在 shell 中创建后台和前台索引,但这不是本教程的先决条件。
-
在文档中的字段
i上创建一个后台索引。这个索引创建操作是我们将从currentOp操作中查看的,也是我们将尝试使用终止操作来终止的操作。在一个 shell 中执行以下操作来启动后台索引创建操作。这需要相当长的时间,在我的笔记本电脑上花了 100 多秒。
> db.currentOpTest.ensureIndex({i:1}, {background:1})
- 在第二个 shell 中,执行以下命令以获取当前正在执行的操作:
> db.currentOp().inprog
-
注意操作的进度,并找到必要的索引创建操作。在我们的情况下,这是测试机器上唯一正在进行的操作。它将是一个在
system.indexes上的操作,操作将是插入。在输出文档中要注意的关键是ns和op。我们需要注意这个操作的第一个字段,opid。在这种情况下,它是11587458。命令的示例输出在下一节中给出。 -
使用以下命令从 shell 中终止操作,使用我们之前得到的
opid(操作 ID):
> db.killOp(11587458)
工作原理...
我们将把我们的解释分成两部分,第一部分是关于当前操作的详细信息,第二部分是关于终止操作。
在我们的情况下,索引创建过程是我们打算终止的长时间运行的操作。我们创建了一个大约有 1000 万个文档的大集合,并启动了一个后台索引创建过程。
在执行db.currentOp()操作时,我们会得到一个文档作为结果,其中包含一个inprog字段,其值是另一个文档的数组,每个文档代表一个当前正在运行的操作。在繁忙的系统上通常会得到一个大型文档列表。这是一个用于索引创建操作的文档:
{
"desc" : "conn12",
"threadId" : "0x3be96c0",
"connectionId" : 12,
"opid" : 3212789,
"active" : true,
"secs_running" : 1,
"microsecs_running" : NumberLong(729029),
"op" : "query",
"ns" : "test.$cmd",
"query" : {
"createIndexes" : "currentOpTest",
"indexes" : [
{
"key" : {
"i" : 1
},
"name" : "i_1",
"background" : 1
}
]
},
"client" : "127.0.0.1:36542",
"msg" : "Index Build (background) Index Build (background): 384120/1000000 38%",
"progress" : {
"done" : 384120,
"total" : 1000000
},
"numYields" : 3003,
"locks" : {
"Global" : "w",
"MMAPV1Journal" : "w",
"Database" : "w",
"Collection" : "W"
"waitingForLock" : true,
"lockStats" : {
"Global" : {
"acquireCount" : {
"w" : NumberLong(3004)
}
},
"MMAPV1Journal" : {
"acquireCount" : {
"w" : NumberLong(387127)
},
"acquireWaitCount" : {
"w" : NumberLong(9)
},
"timeAcquiringMicros" : {
"w" : NumberLong(60025)
}
},
"Database" : {
"acquireCount" : {
"w" : NumberLong(3004),
"W" : NumberLong(1)
}
},
"Collection" : {
"acquireCount" : {
"W" : NumberLong(3004)
},
"acquireWaitCount" : {
"W" : NumberLong(1)
},
"timeAcquiringMicros" : {
"W" : NumberLong(66)
}
},
"Metadata" : {
"acquireCount" : {
"W" : NumberLong(4)
}
}
}
}
我们将在下表中看到这些字段的含义:
| 字段 | 描述 |
|---|---|
| opid | 这是一个唯一的操作 ID,用于标识操作。这是要用来终止操作的 ID。 |
active |
布尔值,指示操作是否已经开始,如果它正在等待获取锁来执行操作,则为 false。一旦它开始,即使在某个时刻它已经释放了锁并且没有在执行,值也将为 true。 |
secs_running |
给出操作执行的时间,单位为秒。 |
op |
这是操作的类型。在索引创建的情况下,它被插入到索引的系统集合中。可能的值包括insert,query,getmore,update,remove和command。 |
ns |
这是目标的完全限定命名空间。它将以<数据库名称>.<集合名称>的形式出现。 |
insert |
这是将插入到集合中的文档。 |
| 查询 | 这是一个字段,除了insert,getmore和command之外的其他操作中都会出现。 |
client |
启动操作的客户端的 IP 地址/主机名和端口。 |
desc |
这是客户端的描述,主要是客户端连接名称。 |
connectionId |
这是请求来源的客户端连接的标识符。 |
locks |
这是一个包含为此操作持有的锁的文档。该文档显示了用于分析的操作所持有的锁的类型和模式。可能的模式如下:R表示共享(S)锁。W表示排他(X)锁。r表示意向共享(IS)锁。w表示意向排他(IX)锁。 |
waitingForLock |
此字段指示操作是否正在等待获取锁。例如,如果前面的索引创建不是后台进程,那么此数据库上的其他操作将排队等待获取锁。那些操作的标志将为 true。 |
msg |
这是操作的人类可读消息。在这种情况下,我们可以看到操作完成的百分比,因为这是一个索引创建操作。 |
progress |
操作的状态,total 给出了集合中文档的总数,done 给出了到目前为止已索引的数量。在这种情况下,集合已经有超过 1000 万个文档。完成百分比是从这些数字计算出来的。 |
numYields |
这是进程放弃锁的次数,以允许其他操作执行。由于这是后台索引创建过程,这个数字会不断增加,因为服务器经常放弃它,以便让其他操作执行。如果是前台进程,锁将一直保持到操作完成。 |
lockStats |
这个文档有更多的嵌套文档,给出了此操作持有读取或写入锁的总时间,以及等待获取锁的时间。 |
注意
如果您有一个副本集,主服务器上的 oplog 将有更多的 getmore 操作,来自从服务器。
- 要查看正在执行的系统操作,我们需要将 true 值作为参数传递给
currentOp函数调用,如下所示:
> db.currentOp(true)
- 接下来,我们将看到如何使用
killOp函数终止用户发起的操作。操作可以简单地如下所示:
> db.killOp(<operation id>)
在我们的情况下,索引创建过程的进程 ID 为 11587458,因此将如下终止它:
> db.killOp(11587458)
无论给定的操作 ID 是否存在,终止任何操作,我们都会在控制台上看到以下消息:
{ "info" : "attempting to kill op" }
因此,看到这条消息并不意味着操作已被终止。这只是意味着如果存在该操作,将尝试终止该操作。
- 如果某些操作无法立即终止,并且为其发出了
killOp命令,则currentOp中的killPending字段将开始出现给定操作。例如,在 shell 上执行以下查询:
> db.currentOpTest.find({$where:'sleep(100000)'})
这不会返回,并且执行查询的线程将休眠 100 秒。这是一个无法使用killOp终止的操作。尝试从另一个 shell 执行currentOp命令(不要按Tab进行自动完成,否则您的 shell 可能会挂起),获取操作 ID,然后使用killOp终止它。如果执行currentOp命令,您应该看到该进程仍在运行,但是进程详细信息的文档现在将包含一个新的killPending键,指出该操作的终止已被请求但是挂起。
使用分析器来分析操作
在这个教程中,我们将看一下 mongo 内置的分析器,用于分析在 mongo 服务器上执行的操作。这是一个用于记录所有或慢操作的实用程序,可用于分析服务器的性能。
准备工作
在这个教程中,我们将在独立的 mongo 实例上执行一些操作并对其进行分析。我们需要启动一个独立的服务器,以便监听任何端口以进行客户端连接;在这种情况下,我们将使用默认的27017端口。如果您不知道如何启动独立服务器,请参阅第一章中的安装单节点 MongoDB,安装和启动服务器。我们还需要启动一个 shell,用于执行查询,启用分析和查看分析操作。
如何做…
- 一旦服务器启动并且 shell 连接到它,执行以下内容以获取当前的分析级别:
> db.getProfilingLevel()
-
如果我们之前没有设置默认级别,那么默认级别应该是
0(不进行分析)。 -
让我们将分析级别设置为
1(仅记录慢操作),并记录所有慢于50毫秒的操作。在 shell 上执行以下操作:
> db.setProfilingLevel(1, 50)
- 现在,让我们执行一个插入操作到一个收集中,然后执行一些查询:
> db.profilingTest.insert({i:1})
> db.profilingTest.find()
> db.profilingTest.find({$where:'sleep(70)'})
- 现在,在以下收集上执行查询:
> db.system.profile.find().pretty()
它是如何工作的...
分析通常不会默认启用。如果您对数据库的性能感到满意,没有理由启用分析器。只有当有改进的空间并且想要针对一些昂贵的操作时才会启用。一个重要的问题是什么样的操作被分类为慢操作?答案是,这取决于应用程序。在 mongo 中,慢操作指的是任何超过 100 毫秒的操作。然而,在设置分析级别时,您可以选择阈值。
有三种可能的分析级别:
-
0:禁用分析 -
1:启用慢操作的分析,调用时提供操作被分类为慢操作的阈值 -
2:分析所有操作
尽管分析所有操作可能不是一个很好的主意,也可能不常用,但将值设置为1并提供一个阈值是监视慢操作的好方法。
如果我们看一下我们执行的步骤,我们可以通过执行操作db.getProfilingLevel()来获取当前的分析级别。要获取更多信息,例如慢操作的阈值是多少,可以使用db.getProfilingStatus()。这将返回一个包含分析级别和慢操作阈值的文档。
要设置分析级别,我们调用db.setProfilingLevel()方法。在我们的情况下,我们设置为记录所有操作花费超过50毫秒的时间为db.setProfilingLevel(1, 50)。
要禁用分析,只需执行db.setProfilingLevel(0)。
接下来我们执行了三个操作,一个是插入文档,一个是查找所有文档,最后一个是调用sleep并设置值为70毫秒以减慢速度的查找。
最后一步是查看记录在system.profile收集中的这些被分析的操作。我们执行一个查找以查看记录的操作。对于我的执行,插入和最终的find操作与sleep一起被记录。
显然,这种分析会带来一些开销,但可以忽略不计。因此,我们不会默认启用它,只有在我们想要分析慢操作时才会启用。另一个问题是,“这种分析收集会随时间增加吗?”答案是“不会”,因为这是一个有上限的收集。有上限的收集是固定大小的收集,保留插入顺序,并充当循环队列,在新文档填满时丢弃最旧的文档。对system.namespaces的查询应该显示统计信息。对system.profile收集的查询执行将显示以下内容:
{"name":"test.system.profile", "options":{"capped":true, "size":1048576 }}
正如我们所看到的,这个收集的大小是 1MB,非常小。因此,将分析级别设置为2会很容易覆盖繁忙系统上的数据。如果希望保留更多操作,也可以选择显式创建一个名为system.profile的有上限的收集,并设置任何所需的大小。要显式创建一个有上限的收集,可以执行以下操作:
db.createCollection('system.profile', {capped:1, size: 1048576})
显然,所选择的大小是任意的,您可以根据数据填充的频率和希望在覆盖之前保留多少分析数据来分配任何大小给这个收集。
由于这是一个有上限的收集,并且保留了插入顺序,使用sort order {$natural:-1}的查询将非常适用且非常有效,可以按照执行时间的相反顺序找到操作。
我们最终将查看插入到system.profile集合中的文档,并查看它记录了哪些操作:
{
"op" : "query",
"ns" : "test.profilingTest",
"query" : {
"$where" : "sleep(70)"
},
"ntoreturn" : 0,
"ntoskip" : 0,
"nscanned" : 1,
"keyUpdates" : 0,
"numYield" : 0,
"lockStats" : {
…<<<<snip>>>
},
"nreturned" : 0,
"responseLength" : 20,
"millis" : 188,
"ts" : ISODate("2014-01-27T17:37:02.482Z"),
"client" : "127.0.0.1",
"allUsers" : [ ],
"user" : ""
}
正如我们在文档中所看到的,确实有一些有趣的统计数据。让我们在下表中看一些。其中一些字段与我们从 shell 执行db.currentOp()操作时看到的字段相同,并且我们在上一个示例中已经讨论过。
| 字段 | 描述 |
|---|---|
op |
执行的操作;在这种情况下,是一个查找操作,因此在这种情况下是查询。 |
ns |
这是操作执行的集合的完全限定名称。它的格式将是<数据库>.<集合名称>。 |
query |
显示在服务器上执行的查询。 |
nscanned |
这与解释计划有相似的含义。它是扫描的文档和索引条目的总数。 |
numYields |
操作执行时锁被放弃的次数。更高的放弃次数可能表明查询需要大量的磁盘访问。这可能是重新查看索引或优化查询本身的良好指标。 |
lockStats |
获取锁所花费的时间和持有锁的时间的一些有趣的统计数据。 |
nreturned |
返回的文档数量。 |
responseLength |
响应的长度(以字节为单位)。 |
millis |
最重要的是,执行操作所花费的毫秒数。这可以是捕捉慢查询的良好起点。 |
ts |
这是操作执行的时间。 |
client |
执行操作的客户端的主机名/IP 地址。 |
在 Mongo 中设置用户
安全是任何企业级系统的基石之一。并非总是可以在完全安全的环境中找到系统,以允许未经身份验证的用户访问它。除了测试环境外,几乎每个生产环境都需要适当的访问权限,也许还需要对系统访问进行审计。Mongo 安全有多个方面:
-
最终用户访问系统的访问权限。将会有多个角色,如管理员、只读用户和读写非管理员用户。
-
副本集中添加的节点的身份验证。在副本集中,只允许添加经过身份验证的系统。如果向副本集添加未经身份验证的节点,系统的完整性将受到损害。
-
加密在副本集的节点之间或甚至客户端和服务器(或分片设置中的 mongos 进程)之间传输的数据。
在这个和下一个示例中,我们将看看如何解决这里给出的第一和第二点。默认情况下,社区版的 mongo 不支持在传输的数据上加密,需要使用ssl选项重新构建 mongo 数据库。
准备工作
在这个示例中,我们将为独立的 mongo 实例设置用户。我们需要启动一个独立服务器,监听任何端口以进行客户端连接;在这种情况下,我们将使用默认的27017端口。如果您不知道如何启动独立服务器,请参阅第一章中的安装单节点 MongoDB,安装和启动服务器。我们还需要启动一个用于此管理操作的 shell。对于副本集,我们只会连接到主服务器并执行这些操作。
如何做…
在这个示例中,我们将为测试数据库添加一个管理员用户、一个只读用户和一个读写用户。
在这一点上,假设:
-
服务器正在运行,并且我们从 shell 连接到它。
-
服务器在没有特殊命令行参数的情况下启动,除了第一章中提到的那些,安装和启动服务器的使用命令行选项启动单节点实例配方。因此,我们对任何用户都有对服务器的完全访问权限。
执行以下步骤:
-
我们将要做的第一步是创建一个管理员用户。所有命令都假定您正在使用 MongoDB 3.0 及以上版本。
-
首先,我们从 admin 数据库开始创建管理员用户如下:
> use admin
> db.createUser({
user:'admin', pwd:'admin',
customData:{desc:'The admin user for admin db'},
roles:['readWrite', 'dbAdmin', 'clusterAdmin']
})
- 我们将添加
read_user和write_user到测试数据库。要添加用户,请从 mongo shell 执行以下操作:
> use test
> db.createUser({
user:'read_user', pwd:'read_user',
customData:{desc:'The read only user for test database'},
roles:['read']
}
)
> db.createUser({
user:'write_user', pwd:'write_user',
customData:{desc:'The read write user for test database'},
roles:['readWrite']
}
)
- 现在关闭 mongo 服务器并关闭 shell。在命令行上重新启动 mongo 服务器,但使用
--auth选项:
$ mongod .. <other options as provided earlier> --auth
如果您的 mongod 实例使用/etc/mongod.conf,则在配置文件中添加auth = true一行,并重新启动 mongod 服务。
- 现在从新打开的 mongo shell 连接到服务器并执行以下操作:
> db.testAuth.find()
-
testAuth集合不需要存在,但是您应该会看到一个错误,即我们未被授权查询该集合。 -
我们现在将使用
read_user从 shell 登录如下:
> db.auth('read_user', 'read_user')
- 我们现在将执行相同的
find操作如下。它不应该出现错误,根据集合是否存在,可能不会返回任何结果:
> db.testAuth.find()
- 现在,我们将尝试插入一个文档如下。我们应该会收到一个错误,表示您未被授权在此集合中插入数据。
> db.testAuth.insert({i:1})
- 我们现在将注销并再次登录,但是使用 write 用户如下。请注意,这次我们登录的方式与以前不同。我们为
auth函数提供了一个文档作为参数,而在以前的情况下,我们为用户名和密码传递了两个参数:
> db.logout()
> db.auth({user:'write_user', pwd:'write_user'})
Now to execute the insert again as follows, this time around it should work
> db.testAuth.insert({i:1})
- 现在,在 shell 上执行以下操作。您应该会收到未经授权的错误:
> db.serverStatus()
- 我们现在将切换到
admin数据库。我们当前使用具有test数据库上读写权限的write_user连接到服务器。从 mongo shell 尝试执行以下操作:
> use admin
> show collections
- 关闭 mongo shell 或从操作系统控制台打开一个新的 shell 如下。这应该会直接将我们带到 admin 数据库:
$ mongo -u admin -p admin admin
- 现在在 shell 上执行以下操作。它应该会显示我们在 admin 数据库中的集合:
> show collections
- 尝试并执行以下操作:
> db.serverStatus()
它是如何工作的...
我们执行了很多步骤,现在我们将仔细研究它们。
最初,服务器在没有--auth选项的情况下启动,因此默认情况下不会强制执行任何安全性。我们使用db.createUser方法创建了一个具有db.createUser方法的管理员用户。创建用户的方法签名是createUser(user, writeConcern)。第一个参数是用户,实际上是一个 JSON 文档,第二个是用于用户创建的写关注。用户的 JSON 文档具有以下格式:
{
'user' : <user name>,
'pwd' : <password>,
'customData': {<JSON document providing any user specific data>}
'roles':[<roles of the user>]
}
这里提供的角色可以按如下方式提供,假设在创建用户时的当前数据库是 shell 上的测试:
[{'role' : 'read', 'db':'reports'}, 'readWrite']
这将创建的用户对报告db具有读取访问权限,并对test数据库具有readWrite访问权限。让我们看看test用户的完整用户创建调用:
> use test
> db.createUser({
user:'test', pwd:'test',
customData:{desc:'read access on reports and readWrite access on test'},
roles:[
{role:'read', db : 'reports'},
'readWrite'
]
}
)
写关注,这是一个可选参数,可以作为 JSON 文档提供。一些示例值是{w:1},{w:'majority'}。
回到管理员用户创建,我们在第 2 步中使用createUser方法创建了用户,并在admin数据库中为该用户提供了三个内置角色。
在第 3 步中,我们使用相同的createUser方法在test数据库中创建了read和read-write用户。
在admin,read和read-write用户创建后关闭 MongoDB 服务器,并使用--auth选项重新启动它。
重新启动服务器后,我们将在第 8 步中从 shell 连接到它,但未经身份验证。在这里,我们尝试在 test 数据库中的集合上执行find查询,但由于我们未经身份验证,操作失败。这表明服务器现在需要适当的凭据才能执行操作。在第 8 和 9 步中,我们使用read_user登录,首先执行find操作(成功),然后执行一个插入操作(失败),因为用户只有读取权限。通过从 shell 调用db.auth(<user name>, <password>)和db.logout()来验证用户的方式,这将注销当前登录的用户。
在步骤 10 到 12 中,我们演示了我们可以使用write_user执行insert操作,但是像db.serverStatus()这样的管理员操作无法执行。这是因为这些操作在服务器上执行admin command,非管理员用户不允许调用这些操作。同样,当我们将数据库更改为 admin 时,来自test数据库的write_user不被允许执行任何操作,比如获取集合列表或查询admin数据库中的集合。
在第 14 步中,我们使用admin用户登录到admin数据库的 shell 中。之前,我们使用auth方法登录到数据库;在这种情况下,我们使用-u和-p选项来提供用户名和密码。我们还提供要连接的数据库的名称,在这种情况下是 admin。在这里,我们能够查看 admin 数据库中的集合,并执行像获取服务器状态这样的管理员操作。执行db.serverStatus调用是可能的,因为用户被赋予了clusterAdmin角色。
最后要注意的一点是,除了向集合写入数据之外,具有写入权限的用户还可以在具有写入访问权限的集合上创建索引。
还有更多...
在这个示例中,我们看到了如何创建不同的用户以及他们具有的权限,限制了一些操作。在接下来的示例中,我们将看到如何在进程级别进行身份验证。也就是说,一个 mongo 实例如何对自己进行身份验证,以便被添加到副本集中。
另请参阅
-
MongoDB 带有许多内置用户角色,每个角色都有各种权限。请参考以下网址以获取各种内置角色的详细信息:
docs.mongodb.org/manual/reference/built-in-roles/。 -
MongoDB 还支持自定义用户角色。请参考以下网址了解如何定义自定义用户角色的更多信息:
docs.mongodb.org/manual/core/authorization/#user-defined-roles。
Mongo 中的进程间安全性
在上一个示例中,我们看到了如何强制用户在允许对 Mongo 进行任何操作之前登录进行身份验证。在这个示例中,我们将研究进程间安全性。通过进程间安全性,我们并不是指加密通信,而是确保在将节点添加到副本集之前对其进行身份验证。
准备工作
在这个示例中,我们将作为副本集的一部分启动多个 mongo 实例,因此您可能需要参考第一章中的作为副本集的一部分启动多个实例这个示例,如果您不知道如何启动副本集。除此之外,在这个示例中,我们将看到如何生成用于使用的密钥文件以及在未经身份验证的节点被添加到副本集时的行为。
如何做...
为了奠定基础,我们将启动三个实例,分别监听端口27000、27001和27002。前两个将通过提供密钥文件的路径来启动,第三个则不会。稍后,我们将尝试将这三个实例添加到同一个副本集中。
- 让我们首先生成密钥文件。生成密钥文件并没有什么特别之处。这就像有一个包含来自
base64字符集的 6 到 1024 个字符的文件一样简单。在 Linux 文件系统上,您可以选择使用openssl生成伪随机字节,并将其编码为base64。以下命令将生成 500 个随机字节,然后将这些字节编码为base64并写入文件keyfile:
$ openssl rand –base64 500 > keyfile
- 在 Unix 文件系统上,密钥文件不应该对世界和组有权限。因此,在创建后,我们应该执行以下操作:
$ chmod 400 keyfile
-
不给创建者写权限可以确保我们不会意外地覆盖内容。然而,在 Windows 平台上,
openssl并不是开箱即用的,因此您需要下载它,解压缩存档,并将bin文件夹添加到操作系统的路径变量中。对于 Windows,我们可以从以下网址下载:gnuwin32.sourceforge.net/packages/openssl.htm。 -
您甚至可以选择不使用这里提到的方法(使用
openssl)生成密钥文件,并且可以通过在任何文本编辑器或您选择的地方输入纯文本来简化。但是,请注意,mongo 会剥离字符\r、\n和空格,并将剩余文本视为密钥。例如,我们可以创建一个文件,其中包含以下内容添加到密钥文件。同样,文件将被命名为keyfile,内容如下:
somecontentaddedtothekeyfilefromtheeditorwithoutspaces
-
使用这里提到的任何方法,我们都不应该有一个
keyfile,它将用于后续的步骤。 -
现在我们将通过以下方式启动 mongo 进程来保护 mongo 进程。我将在 Windows 上启动以下内容,我的密钥文件 ID 命名为
keyfile,放在c:\MongoDB上。数据路径分别为c:\MongoDB\data\c1、c:\MongoDB\data\c2和c:\MongoDB\data\c3。 -
启动第一个实例,监听端口
27000如下:
C:\>mongod --dbpath c:\MongoDB\data\c1 --port 27000 --auth --keyFile c:\MongoDB\keyfile --replSet secureSet --smallfiles --oplogSize 100
- 同样,启动第二个服务器,监听端口
27001如下:
C:\>mongod --dbpath c:\MongoDB\data\c2 --port 27001 --auth --keyFile c:\MongoDB\keyfile --replSet secureSet --smallfiles --oplogSize 100
- 第三个实例将启动,但不带
--auth和--keyFile选项,监听端口27002如下:
C:\>mongod --dbpath c:\MongoDB\data\c3 --port 27002 --replSet secureSet --smallfiles --oplogSize 100
- 然后我们启动一个 mongo shell,并连接到端口
27000,这是第一个启动的实例。从 mongo shell 中,我们输入:
> rs.initiate()
- 几秒钟后,副本集将被初始化,只有一个实例在其中。现在我们将尝试向这个副本集添加两个新实例。首先,按照以下方式添加监听端口
27001的实例(您需要添加适当的主机名,Amol-PC是我的主机名):
> rs.add({_id:1, host:'Amol-PC:27001'})
-
我们将执行
rs.status()命令来查看我们副本集的状态。在命令的输出中,我们应该看到我们新添加的实例。 -
现在我们将尝试最终添加一个实例,该实例是在没有
--auth和--keyFile选项的情况下启动的,如下所示:
> rs.add({_id:2, host:'Amol-PC:27002'})
这应该将实例添加到副本集中,但使用rs.status()将显示实例状态为 UNKNOWN。运行在27002上的服务器日志也应该显示一些身份验证错误。
- 最后,我们必须重新启动这个实例;然而,这一次我们提供
--auth和--keyFile选项如下:
C:\>mongod --dbpath c:\MongoDB\data\c3 --port 27002 --replSet secureSet --smallfiles --oplogSize 100 --auth --keyFile c:\MongoDB\keyfile
- 一旦服务器启动,再次从 shell 连接到它,并在几分钟内输入
rs.status(),它应该会显示为一个辅助实例。
还有更多...
在这个配方中,我们看到了用于防止未经身份验证的节点被添加到 mongo 副本集的进程间安全性。我们仍然没有通过加密在传输过程中发送的数据来保护传输。在附录中,我们将展示如何从源代码构建 mongo 服务器以及如何启用传输内容的加密。
使用collMod命令修改集合行为
这是一个用于更改 mongo 中集合行为的命令。它可以被认为是一个集合修改操作(尽管官方没有明确提到)。
对于这个配方的一部分,需要了解 TTL 索引的知识。
准备工作
在这个配方中,我们将在一个集合上执行collMod操作。我们需要启动一个独立的服务器来监听任何端口以进行客户端连接;在这种情况下,我们将坚持使用默认的27017端口。如果您不知道如何启动独立服务器,请参考第一章中的安装单节点 MongoDB,安装和启动服务器。我们还需要启动一个用于此管理的 shell。如果您不知道它们,强烈建议您查看第二章中的在固定间隔后使文档过期使用 TTL 索引和使用 TTL 索引在给定时间使文档过期这两个配方。
工作原理…
这个操作可以用来做一些事情:
- 假设我们有一个带有 TTL 索引的集合,就像我们在第二章中看到的那样,让我们通过执行以下操作来查看索引列表:
> db.ttlTest.getIndexes()
- 要将到期时间从
300毫秒更改为800毫秒,请执行以下操作:
> db.runCommand({collMod: 'ttlTest', index: {keyPattern: {createDate:1}, expireAfterSeconds:800}})
工作原理…
collMod命令始终具有以下格式:{collMod:<集合名称>,<collmod 操作>}。
我们使用collMod进行索引操作来修改 TTL 索引。如果 TTL 索引已经创建,并且需要在创建后更改生存时间,我们使用collMod命令。该命令的操作特定字段如下:
{index: {keyPattern: <the field on which the index was originally created>, expireAfterSeconds:<new time to be used for TTL of the index>}}
keyPattern是创建 TTL 索引的集合上的字段,expireAfterSeconds将包含要更改的新时间。成功执行后,我们应该在 shell 中看到以下内容:
{ "expireAfterSeconds_old" : 300, "expireAfterSeconds_new" : 800, "ok" : 1 }
将 MongoDB 设置为 Windows 服务
Windows 服务是在后台运行的长时间运行的应用程序,类似于守护线程。数据库是这种类型服务的良好候选者,它们会在主机启动和停止时启动和停止(但您也可以选择手动启动/停止服务)。许多数据库供应商在服务器上安装时提供了将数据库作为服务启动的功能。MongoDB 也可以做到这一点,这就是我们将在这个配方中看到的。
准备工作
参考第一章中的配方使用配置文件从配置文件安装单节点 MongoDB,获取有关如何使用外部配置文件启动 MongoDB 服务器的信息。由于在这种情况下 mongo 作为服务运行,因此无法提供类似命令的参数,并且从配置文件配置是唯一的选择。参考第一章中安装单节点 MongoDB配方的先决条件,这是我们这个配方所需要的一切。
如何操作…
- 我们首先将创建一个带有三个配置值
port、dbpath和logpath文件的配置文件。我们将文件命名为mongo.conf,并将其保存在位置c:\conf\mongo.conf,其中包含以下三个条目(您可以选择任何路径作为配置文件位置、数据库和日志):
port = 27000
dbpath = c:\data\mongo\db
logpath = c:\logs\mongo.log
-
从 Windows 终端执行以下操作,可能需要以管理员身份执行。在 Windows 7 中,执行了以下步骤:
-
在键盘上按 Windows 键。
-
在“搜索程序和文件”空间中,键入
cmd。 -
在程序中,将看到命令提示符程序;右键单击它并选择以管理员身份运行。
-
在 shell 中执行以下操作:
C:\>mongod --config c:\conf\mongo.conf –install
-
在控制台上打印的日志应该确认服务已正确安装。
-
可以通过以下方式从控制台启动服务:
C:\>net start MongoDB
- 可以通过以下方式停止服务:
C:\>net stop MongoDB
-
在运行窗口中键入
services.msc(Windows 键+R)。在管理控制台中,搜索 MongoDB 服务。我们应该看到它如下所示:![如何操作…]()
-
该服务是自动的,也就是说,当操作系统启动时会启动它。可以通过右键单击它并选择属性来更改为手动。
-
要删除服务,需要从命令提示符执行以下操作:
C:\>mongod --remove
- 还有更多可用的选项,可用于配置服务的名称、显示名称、描述以及运行服务的用户帐户。这些可以作为命令行参数提供。执行以下操作以查看可能的选项,并查看Windows 服务控制管理器选项:
C:\> mongod --help
副本集配置
我们在第一章中对副本集进行了深入讨论,安装和启动服务器中的配方作为副本集的一部分启动多个实例,我们看到了如何启动一个简单的副本集。在本章的Mongo 中的进程间安全性中,我们看到了如何启动具有进程间身份验证的副本集。老实说,这基本上就是我们在设置标准副本集时所做的。有一些配置是必须了解的,并且应该了解它们如何影响副本集的行为。请注意,我们在本配方中仍未讨论标签感知复制,并且它将在本章的另一个配方构建带标签的副本集中单独讨论。
准备工作
参考第一章中的安装和启动服务器中的配方作为副本集的一部分启动多个实例,了解先决条件并了解副本集的基础知识。按照配方中的说明,在计算机上设置一个简单的三节点副本集。
在进行配置之前,我们将了解副本集中的选举是什么,以及它们在高层次上是如何工作的。了解选举是很有必要的,因为一些配置选项会影响选举中的投票过程。
副本集中的选举
Mongo 副本集有一个主要实例和多个辅助实例。所有数据库写操作只发生在主要实例上,并且会被复制到辅助实例上。读操作可以根据读取偏好从辅助实例中进行。请参考附录中的了解查询的读取偏好,了解读取偏好是什么。然而,如果主要实例宕机或由于某种原因无法访问,副本集将无法进行写操作。MongoDB 副本集具有自动故障转移到辅助实例的功能,将其提升为主要实例,并使集合对客户端可用进行读写操作。在这一瞬间,副本集将暂时不可用,直到新的主要实例出现。
这一切听起来都很好,但问题是,谁决定新的主要实例是谁?选择新主要实例的过程是通过选举来进行的。每当任何辅助节点检测到无法联系主节点时,它会要求实例中的所有副本集节点选举自己为新的主节点。
复制集中的所有其他节点在接收主节点选举请求之前将执行某些检查,然后才会对请求重新选举的次要节点投票赞成:
-
首先,他们会检查现有的主节点是否可访问。这是必要的,因为请求重新选举的次要节点可能无法访问主节点,可能是因为网络分区,如果是这种情况,它不应该被允许成为主节点。在这种情况下,接收请求的实例将投票否定。
-
其次,实例将检查自身的复制状态与请求选举的次要节点的复制状态。如果发现请求的次要节点在复制数据方面落后于自己,它将投票否定。
-
最后,主节点无法访问,但具有比请求重新选举的次要节点更高优先级的实例可以访问它。如果请求重新选举的次要节点无法访问具有更高优先级的次要节点,可能是由于网络分区,此时接收选举请求的实例将投票否定。
前面的检查基本上是在重新选举期间会发生的事情(不一定按照之前提到的顺序),如果这些检查通过,实例就会投票赞成。
即使只有一个实例投票否定,选举也将无效。但是,如果没有一个实例投票否定,那么请求选举的次要节点将成为新的主节点,如果它从大多数实例那里得到了赞成。如果选举无效,将会进行重新选举,直到选出新的主节点为止,这个过程将与之前相同的次要节点或任何其他请求选举的实例进行。
现在我们对复制集中的选举和术语有了一些了解,让我们来看一些复制集配置。其中一些选项与投票有关,我们首先来看看这些选项。
复制集的基本配置
从我们设置复制集的第一章开始,我们的配置与以下类似。三个成员集的基本复制集配置如下:
{
"_id" : "replSet",
"members" : [
{
"_id" : 0,
"host" : "Amol-PC:27000"
},
{
"_id" : 1,
"host" : "Amol-PC:27001"
},
{
"_id" : 2,
"host" : "Amol-PC:27002"
}
]
}
我们不会在以下步骤中重复整个配置。我们将提到的所有标志都将添加到成员数组中特定成员的文档中。在上面的例子中,如果具有_id为2的节点要成为仲裁者,我们将在先前显示的配置文档中为其添加以下配置:
{
"_id" : 2,
"host" : "Amol-PC:27002"
"arbiterOnly" : true
}
通常,重新配置现有复制集的步骤如下:
- 将配置文档分配给一个变量。如果复制集已经配置,可以使用 shell 中的
rs.conf()调用来获取它。
> var conf = rs.conf()
- 文档中的成员字段是复制集中每个成员的文档数组。要为特定成员添加新属性,我们要做以下操作。例如,如果我们想要为复制集的第三个成员(数组中的索引 2)添加
votes键并将其值设置为2,我们将执行以下操作:
> conf.members[2].votes = 2
- 仅仅改变 JSON 文档不会改变复制集。如果复制集已经存在,我们需要重新配置它,如下所示:
> rs.reconfig(conf)
- 如果是首次进行配置,我们将调用以下命令:
> rs.initiate (conf)
在接下来的所有步骤中,除非明确提到其他步骤,否则您需要遵循前面的步骤来重新配置或启动复制集。
如何做…
在本教程中,我们将看一些可能在复制集中进行的配置。解释将是最小的,所有解释都将在下一节中进行,与往常一样。
- 第一个配置是
arbiterOnly选项。它用于将复制集成员配置为不持有数据,只具有投票权的成员。需要将以下键添加到将成为仲裁者的成员的配置中:
{_id: ... , 'arbiterOnly': true }
- 关于此配置的一点需要记住的是,一旦初始化了副本集,就无法将现有成员从非仲裁节点更改为仲裁节点,反之亦然。但是,我们可以使用助手函数
rs.addArb(<hostname>:<port>)向现有副本集添加仲裁者。例如,向现有副本集添加一个侦听端口27004的仲裁者。在我的机器上执行以下操作以添加仲裁者:
> rs.addArb('Amol-PC:27004')
-
当服务器启动以侦听端口
27004并从 mongo shell 执行rs.status()时,我们应该看到该成员的state和strState分别为7和ARBITER。 -
下一个选项
votes影响成员在选举中的投票数。默认情况下,所有成员每人有一票,此选项可用于更改特定成员的投票数。可以设置如下:
{_id: ... , 'votes': <0 or 1>}
-
可以使用
rs.reconfig()助手更改副本集现有成员的选票,并重新配置副本集。 -
尽管
votes选项可用,可以潜在地改变形成多数的选票数,但通常并不增加太多价值,也不建议在生产中使用。 -
下一个副本集配置选项称为
priority。它确定副本集成员成为主服务器的资格(或不成为主服务器)。该选项设置如下:
{_id: ... , 'priority': <priority number>}
-
更高的数字表示更有可能成为主要成员,主要成员始终是副本集中活跃成员中优先级最高的成员。在已配置的副本集中设置此选项将触发选举。
-
将优先级设置为
0将确保成员永远不会成为主服务器。 -
接下来我们将看到的选项是
hidden。将此选项的值设置为 true 可确保副本集成员处于隐藏状态。该选项设置如下:
{_id: ... , 'hidden': <true/false>}
-
需要记住的一点是,当副本集成员处于隐藏状态时,其优先级也应设置为
0,以确保它不会成为主服务器。尽管这似乎多余;但截至目前的版本,值或优先级需要明确设置。 -
当编程语言客户端连接到副本集时,无法发现隐藏成员。但是,在从 shell 中使用
rs.status()后,成员的状态将可见。 -
现在让我们看看
slaveDelay选项。此选项用于设置从副本集的主服务器到从服务器的时间延迟。该选项设置如下:
{_id: ... , 'slaveDelay': <number of seconds to lag>}
-
与隐藏成员一样,延迟的从服务器成员也应将优先级设置为
0,以确保它们永远不会成为主服务器。这需要明确设置。 -
让我们看看最终的配置选项:
buildIndexes。如果未指定,默认情况下为 true,这表示在主服务器上创建索引时,需要在从服务器上复制该索引。该选项设置如下:
{_id: ... , 'buildIndexes': <true/false>}
- 当将此选项与设置为 false 的值一起使用时,优先级设置为
0,以确保它们永远不会成为主服务器。这需要明确设置。此外,在初始化副本集后无法设置此选项。就像仲裁节点一样,这需要在创建副本集或向副本集添加新成员节点时设置。
它是如何工作的...
在本节中,我们将解释和理解不同类型成员的重要性以及在上一节中看到的配置选项。
副本集成员作为仲裁者
仲裁者这个词的英文含义是解决争端的法官。在副本集的情况下,仲裁者节点只是在选举时投票,而不复制任何数据。事实上,这是一个非常常见的情况,因为 Mongo 副本集至少需要三个实例(最好是奇数个实例,3 个或更多)。许多应用程序不需要维护三份数据,只需要两个实例,一个主服务器和一个带有数据的辅助服务器。
考虑只有两个实例存在于副本集的情况。当主服务器宕机时,辅助实例无法形成适当的多数,因为它只有 50%的选票(自己的选票),因此无法成为主服务器。如果大多数辅助实例宕机,那么主服务器实例将从主服务器退下,并成为辅助服务器,从而使副本集无法进行写入。因此,两节点副本集是无用的,因为即使其中任何一个实例宕机,它也无法保持可用。这违背了设置副本集的目的,因此副本集至少需要三个实例。
在这种情况下,仲裁者非常有用。我们设置了一个包含三个实例的副本集实例,其中只有两个实例具有数据,另一个充当仲裁者。我们无需同时维护三份数据,通过设置一个两实例副本集来消除我们设置两实例副本集时遇到的问题。
副本集成员的优先级
优先级标志可以单独使用,也可以与hidden、slaveDelay和buildIndexes等其他选项一起使用,其中我们不希望具有这三个选项之一的成员被选为主服务器。我们将很快看到这些选项。
还有一些可能的用例,我们永远不希望副本集成为主服务器,如下所示:
-
当成员的硬件配置无法处理写入和读取请求时,如果成为主服务器,那么将其放在那里的唯一原因就是复制数据。
-
我们有一个多数据中心的设置,其中一个副本集实例存在于另一个数据中心,为了地理分布数据以用于灾难恢复目的。理想情况下,应用程序服务器和数据库之间的网络延迟应该最小,以实现最佳性能。如果两台服务器(应用程序服务器和数据库服务器)在同一个数据中心,就可以实现这一点。不改变另一个数据中心副本集实例的优先级,使其同样有资格被选为主服务器,从而在其他数据中心的服务器被选为主服务器时会影响应用程序的性能。在这种情况下,我们可以将第二个数据中心的服务器的优先级设置为
0,并且需要管理员手动切换到另一个数据中心,以应对紧急情况。
在这两种情况下,我们还可以将相应的成员隐藏起来,以便应用客户端首先看不到这些成员。
与将优先级设置为0以防止某个成员成为主服务器类似,我们也可以通过将其优先级设置为大于 1 的值来偏向于某个成员在可用时成为主服务器,因为优先级的默认值是1。
假设由于预算原因,我们有一个成员将数据存储在固态硬盘上,其余成员存储在机械硬盘上。我们理想情况下希望固态硬盘的成员在运行时成为主服务器。只有在它不可用时,我们才希望另一个成员成为主服务器,在这种情况下,我们可以将运行在固态硬盘上的成员的优先级设置为大于 1 的值。该值实际上并不重要,只要它大于其他成员的值,也就是说,将其设置为1.5或2都没有关系,只要其他成员的优先级较低。
隐藏、从属延迟和构建索引配置
副本集节点的隐藏术语是指连接到副本集的应用程序客户端,而不是管理员。对于管理员来说,隐藏成员同样重要,因此它们的状态在rs.status()响应中可见。隐藏成员也像所有其他成员一样参与选举。
对于slaveDelay选项,最常见的用例是确保成员在特定时间点的数据落后于主要成员提供的秒数,并且可以在发生某些意外错误时进行恢复,例如错误地更新了一些数据。请记住,延迟时间越长,我们就能够获得更多的恢复时间,但可能会导致数据过时。
buildIndexes选项在以下情况下很有用:我们有一个副本集成员,其硬件不符合生产标准,维护索引的成本不值得。您可以选择为不执行任何查询的成员设置此选项。显然,如果设置了此选项,它永远不会成为主要成员,因此优先级选项被强制设置为0。
还有更多…
您可以使用副本集中的标签实现一些有趣的事情。这将在稍后的食谱中讨论,在我们学习有关标签的食谱构建带标签的副本集之后。
从副本集中下台为主要成员
有时,在工作时间进行某些维护活动时,我们希望将服务器从副本集中取出,执行维护活动,然后将其放回副本集。如果要处理的服务器是主服务器,我们需要从主成员位置下台,执行重新选举,并确保在一定的时间范围内不会再次被选中。一旦下台操作成功,服务器成为辅助服务器后,我们可以将其从副本集中取出,执行维护活动,然后将其放回副本集。
准备工作
有关先决条件和副本集基础知识,请参考第一章中的食谱作为副本集的一部分启动多个实例,安装和启动服务器。按照食谱中提到的方法,在计算机上设置一个简单的三节点副本集。
如何做…
假设此时我们已经设置并运行了一个副本集,请执行以下操作:
- 从连接到副本集成员之一的 shell 中执行以下操作,并查看当前是主要实例的哪个实例:
> rs.status()
- 从 mongo shell 连接到主实例,并在 shell 上执行以下操作:
> rs.stepDown()
- shell 应该重新连接,您应该看到连接到并最初是主要实例的实例现在变为辅助实例。从 shell 执行以下操作,以便现在重新选举一个新的主要实例:
> rs.status()
- 现在您可以连接到主服务器,修改副本集配置,并继续对服务器进行管理。
它是如何工作的…
前面提到的步骤非常简单,但我们将看到一些有趣的事情。
我们之前看到的rs.stepDown()方法没有任何参数。实际上,该函数可以接受一个数字值,即实例下台不参与选举并且不会成为主要实例的秒数,默认值为60秒。
另一个有趣的尝试是,如果被要求下台的实例的优先级高于其他实例会发生什么。事实证明,当您下台时,优先级并不重要。被下台的实例无论如何都不会在提供的秒数内成为主要实例。但是,如果为被下台的实例设置了优先级,并且优先级高于其他实例,则在给定的stepDown时间过去后,将发生选举,并且优先级较高的实例将再次成为主要实例。
探索副本集的本地数据库
在这个食谱中,我们将从副本集的角度探索本地数据库。本地数据库可能包含不特定于副本集的集合,但我们将只关注副本集特定的集合,并尝试查看其中的内容和含义。
准备工作
有关先决条件和副本集基础知识,请参阅第一章中的食谱作为副本集的一部分启动多个实例,安装和启动服务器。继续在计算机上设置一个简单的三节点副本集,如食谱中所述。
如何做...
-
设置并运行副本集后,我们需要打开一个连接到主节点的 shell。您可以随机连接到任何一个成员;使用
rs.status()然后确定主节点。 -
打开 shell 后,首先切换到
local数据库,然后按以下方式查看local数据库中的集合:
> use local
switched to db local
> show collections
- 您应该找到一个名为
me的集合。查询此集合应该显示一个文档,其中包含我们当前连接到的服务器的主机名:
>db.me.findOne()
-
将会有两个字段,主机名和
_id字段。记下_id字段-这很重要。 -
查看
replset.minvalid集合。您将需要从 shell 连接到次要成员才能执行以下查询。首先切换到local数据库:
> use local
switched to db local
> db.replset.minvalid.find()
-
此集合只包含一个带有键
ts和值的单个文档,该值是我们连接到的次要实例同步的时间戳。记下这个时间。 -
从主要的 shell 中,在任何集合中插入一个文档。我们将使用数据库作为测试。从主要成员的 shell 中执行以下操作:
> use test
switched to db test
> db.replTest.insert({i:1})
- 再次查询次要,如下所示:
> db.replset.minvalid.find()
-
我们应该看到
ts字段的时间现在已经增加,对应于此复制从主要到次要的时间。对于延迟的从属节点,只有在延迟期过去后,才会看到此时间得到更新。 -
最后,我们将看到
system.replset集合。这个集合是存储副本集配置的地方。执行以下操作:
> db.system.replset.find().pretty()
- 实际上,当我们执行
rs.conf()时,将执行以下查询:
db.getSisterDB("local").system.replset.findOne()
工作原理...
本地数据库是一个特殊的(非复制)数据库,用于保存其中的复制和实例特定的详细信息。尝试在本地数据库中创建自己的集合,并向其中插入一些数据;这些数据不会被复制到辅助节点。
这个数据库为我们提供了一些关于 mongo 存储的内部数据的视图。然而,作为管理员,了解这些集合和其中的数据类型是很重要的。
大多数集合都很简单。从辅助节点的 shell 中在本地数据库中执行查询db.me.findOne(),我们应该看到那里的_id应该与从属集合中的文档中的_id字段匹配。
我们看到的配置文档给出了我们所指的辅助实例的主机名。请注意,副本集成员的端口和其他配置选项在此文档中不存在。最后,syncedTo时间告诉我们次要实例与主要实例同步的时间。我们在次要实例上看到了replset.minvalid集合,它告诉我们它与主要实例同步的时间。主要实例上的syncedTo时间的值与相应次要实例上的replset.minvalid中的值相同。
还有更多...
我们还没有看到 oplog,这是一个有趣的地方。我们将在单独的食谱中查看这个特殊集合,理解和分析 oplog。
理解和分析 oplog
Oplog 是一个特殊的集合,是 MongoDB 复制的支柱。当在副本集的主服务器上执行任何写操作或配置更改时,它们都会被写入主服务器的 oplog 中。然后,所有次要成员都会追踪此集合以获取要复制的更改。追踪类似于 Unix 中的 tail 命令,只能在一种特殊类型的集合上进行,称为受限集合。受限集合是固定大小的集合,它们保持插入顺序,就像队列一样。当集合的分配空间变满时,最旧的数据将被覆盖。如果您不了解受限集合和可追踪游标是什么,请参考第五章中的在 MongoDB 中创建和追踪受限集合游标,了解更多详情。
Oplog 是一个受限集合,存在于名为local的非复制数据库中。在我们之前的配方中,我们看到了local数据库是什么,以及其中存在哪些集合。Oplog 是我们在上一篇配方中没有讨论的内容,因为它需要更多的解释,需要一个专门的配方来做出解释。
准备工作
请参考第一章中的配方作为副本集的一部分启动多个实例,了解先决条件并了解副本集的基础知识。按照配方中提到的步骤,在计算机上设置一个简单的三节点副本集。打开一个 shell 并连接到副本集的主成员。您需要启动 mongo shell 并连接到主实例。
如何操作…
- 连接到 shell 后,执行以下步骤以获取 oplog 中存在的最后一个操作的时间戳。我们对此时间之后的操作感兴趣。
> use test
> local = db.getSisterDB('local')
> var cutoff = local.oplog.rs.find().sort({ts:-1}).limit(1).next().ts
- 从 shell 中执行以下操作。保留 shell 中的输出或将其复制到其他地方。我们稍后会对其进行分析:
> local.system.namespaces.findOne({name:'local.oplog.rs'})
- 按以下方式插入 10 个文档:
> for(i = 0; i < 10; i++) db.oplogTest.insert({'i':i})
- 执行以下更新,为所有值大于
5的文档设置一个字符串值,即我们的情况下的 6、7、8 和 9。这将是一个多更新操作:
> db.oplogTest.update({i:{$gt:5}}, {$set:{val:'str'}}, false, true)
- 现在,按照以下步骤创建索引:
> db.oplogTest.ensureIndex({i:1}, {background:1})
- 在 oplog 上执行以下查询:
> local.oplog.rs.find({ts:{$gt:cutoff}}).pretty()
它是如何工作的…
对于了解消息传递及其术语的人来说,Oplog 可以被看作是消息传递世界中的一个主题,其中有一个生产者,即主实例,和多个消费者,即次要实例。主实例将所有需要复制的内容写入 oplog。因此,任何创建、更新和删除操作以及副本集上的任何重新配置都将被写入 oplog,次要实例将追踪(连续读取被添加到其中的 oplog 内容,类似于 Unix 中带有-f选项的 tail 命令)集合以获取主要写入的文档。如果次要实例配置了slaveDelay,它将不会从 oplog 中读取超过最大时间减去slaveDelay时间的文档。
我们首先将 local 数据库的一个实例保存在名为local的变量中,并确定一个截止时间,我们将使用它来查询我们将在本配方中执行的所有操作。
在本地数据库的system.namespaces集合上执行查询,可以看到该集合是一个具有固定大小的封顶集合。出于性能原因,封顶集合在文件系统上分配连续的空间,并且是预分配的。服务器分配的大小取决于操作系统和 CPU 架构。在启动服务器时,可以提供oplogSize选项来指定 oplog 的大小。默认值通常对大多数情况都足够好。但是,出于开发目的,您可以选择覆盖此值为较小的值。oplog 是需要在磁盘上预分配空间的封顶集合。这种预分配不仅在副本集首次初始化时需要时间,而且占用了固定数量的磁盘空间。出于开发目的,我们通常在同一台机器上作为同一副本集的一部分启动多个 MongoDB 进程,并希望它们尽快运行,并且占用最少的资源。此外,如果 oplog 大小较小,则可以将整个 oplog 放入内存中。出于所有这些原因,建议出于开发目的以较小的 oplog 大小启动本地实例。
我们执行了一些操作,比如插入 10 个文档,使用多次更新更新了四个文档,并创建了一个索引。如果我们查询截止日期后的 oplog 条目,我们计算出 10 个文档,每个插入一个。文档看起来像这样:
{
"ts" : Timestamp(1392402144, 1),
"h" : NumberLong("-4661965417977826137"),
"v" : 2, "op" : "i",
"ns" : "test.oplogTest",
"o" : {
"_id" : ObjectId("52fe5edfd473d2f623718f51"),
"i" : 0
}
}
正如我们所看到的,我们首先看三个字段:op,ns和o。这些代表操作,被插入数据的集合的完全限定名称,以及要插入的实际对象。操作i代表插入操作。请注意,o的值,即要插入的文档,包含在主键上生成的_id字段。我们应该看到 10 个这样的文档,每个插入一个。有趣的是在多次更新操作中发生了什么。主键为每个受影响的文档放入了四个文档。在这种情况下,值op是u,表示更新,用于匹配文档的查询与我们在更新函数中给出的查询不同,但是是一个基于_id字段唯一找到文档的查询。由于_id字段已经存在索引(每个集合自动创建),因此查找要更新的文档的操作并不昂贵。字段o的值是我们从 shell 中传递给更新函数的相同文档。更新的 oplog 中的示例文档如下:
{
"ts" : Timestamp(1392402620, 1),
"h" : NumberLong("-7543933489976433166"),
"v" : 2,
"op" : "u",
"ns" : "test.oplogTest",
"o2" : {
"_id" : ObjectId("52fe5edfd473d2f623718f57")
},
"o" : {
"$set" : {
"val" : "str"
}
}
}
oplog 中的更新与我们提供的更新相同。这是因为$set操作是幂等的,这意味着可以安全地多次应用操作。
但是,使用$inc运算符的更新不是幂等的。让我们执行以下更新:
> db.oplogTest.update({i:9}, {$inc:{i:1}})
在这种情况下,oplog 将具有以下值作为o的值。
"o" : {
"$set" : {
"i" : 10
}
}
这个非幂等操作被 Mongo 智能地放入 oplog 中,作为一个幂等操作,其值为 i 设置为一次增量操作后预期的值。因此,可以安全地重放 oplog 任意次数,而不会损坏数据。
最后,我们可以看到索引创建过程被放入 oplog 作为system.indexes集合中的插入操作。对于大型集合,索引创建可能需要几个小时,因此 oplog 的大小非常重要,以便让从未复制的次要服务器从索引创建开始复制。然而,自 2.6 版本以来,在主服务器上后台启动的索引创建也将在次要实例上后台构建。
有关副本集上索引创建的更多详细信息,请访问以下网址:docs.mongodb.org/master/tutorial/build-indexes-on-replica-sets/。
构建标记的副本集
在第一章中,安装和启动服务器,我们看到了如何设置一个简单的副本集,作为副本集的一部分启动多个实例,并了解了副本集的目的。我们还在本书的附录中对WriteConcern有了很好的解释,以及为什么要使用它。我们所看到的关于写入关注的内容是,它为某个写入操作提供了最低级别的保证。然而,通过标签和写入关注的概念,我们可以定义各种规则和条件,在写入操作被视为成功并向用户发送响应之前,这些规则和条件必须得到满足。
考虑一些常见的用例,比如以下情况:
-
应用程序希望写入操作至少传播到每个数据中心中的一个服务器。这样可以确保在数据中心关闭时,其他数据中心将拥有应用程序写入的数据。
-
如果没有多个数据中心,副本集中至少有一个成员放在不同的机架上。例如,如果机架的电源供应中断,副本集仍然可用(不一定是用于写入),因为至少有一个成员在不同的机架上运行。在这种情况下,我们希望在向客户端返回成功写入之前,将写入传播到至少两个机架。
-
可能有一个报告应用程序定期查询副本集的一组辅助成员以生成一些报告。(这样的辅助成员可能被配置为永远不会成为主要成员)。在每次写入后,我们希望确保写入操作至少被复制到至少一个报告副本成员,然后才确认写入成功。
前面的用例是一些常见的用例,这些用例是我们之前看到的简单写入关注所不能解决的。我们需要一个不同的机制来满足这些要求,而带有标签的副本集就是我们需要的。
显然,下一个问题是标签到底是什么?让我们以博客为例。博客中的各种帖子都附有不同的标签。这些标签使我们能够轻松搜索、分组和关联帖子。标签是一些用户定义的文本,附有一些含义。如果我们将博客帖子和副本集成员进行类比,就像我们给帖子附上标签一样,我们可以给每个副本集成员附上标签。例如,在一个多数据中心的情况下,数据中心 1(dc1)有两个副本集成员,数据中心 2(dc2)有一个成员,我们可以给成员分配以下标签。键的名称和标签分配的值是任意的,并且在应用程序设计期间选择;您甚至可以选择分配任何标签,比如设置服务器的管理员,如果您真的发现它对您的用例有用:
| 副本集成员 | 标签 |
|---|---|
| 副本集成员 1 | {'datacentre': 'dc1', 'rack': 'rack-dc1-1'} |
| 副本集成员 2 | {'datacentre': 'dc1', 'rack': 'rack-dc1-2'} |
| 副本集成员 3 | {'datacentre': 'dc2', 'rack': 'rack-dc2-2'} |
这足以奠定副本集标签的基础。在本教程中,我们将看到如何为副本集成员分配标签,更重要的是,如何利用它们来解决我们之前看到的一些示例用例。
准备工作
要了解有关写入关注的信息,请参考第一章《安装和启动服务器》中的作为副本集的一部分启动多个实例的教程,了解先决条件并了解副本集的基础知识。按照教程中的说明,在计算机上设置一个简单的三节点副本集。打开一个 shell 并连接到副本集的主要成员。
如果您需要了解写入关注,请参阅本书的附录中的写入关注概述。
为了在数据库中插入文档,我们将使用 Python,因为它为我们提供了像 mongo shell 一样的交互式界面。有关如何安装 pymongo 的步骤,请参阅第一章中的配方使用 Python 客户端连接到单个节点,安装和启动服务器。Mongo shell 本来是插入操作演示的最理想候选者,但是在使用自定义写关注时存在某些限制。从技术上讲,任何编程语言都可以使用插入操作的配方中提到的写关注来正常工作。
操作步骤…
- 在复制集启动后,我们将为其添加标签并重新配置如下。以下命令从 mongo shell 中执行:
> var conf = rs.conf()
> conf.members[0].tags = {'datacentre': 'dc1', 'rack': 'rack-dc1-1'}
> conf.members[1].tags = {'datacentre': 'dc1', 'rack': 'rack-dc1-2'}
> conf.members[2].priority = 0
> conf.members[2].tags = {'datacentre': 'dc2', 'rack': 'rack-dc2-1'}
- 设置了复制集标签后(注意我们尚未重新配置复制集),我们需要定义一些自定义写关注。首先,我们定义一个可以确保数据至少被复制到每个数据中心中的一个服务器的写关注。再次在 mongo shell 中执行以下操作:
> conf.settings = {'getLastErrorModes' : {'MultiDC':{datacentre : 2}}}
> rs.reconfig(conf)
- 启动 Python shell 并执行以下操作:
>>> import pymongo
>>> client = pymongo.MongoClient('localhost:27000,localhost:27001', replicaSet='replSetTest')
>>> db = client.test
- 我们现在将执行以下插入操作:
>>>db.multiDCTest.insert({'i':1}, w='MultiDC', wtimeout=5000)
-
前面的插入成功进行,
ObjectId将被打印出;您可以从 mongo shell 或 Python shell 中查询集合以进行确认。 -
由于我们的主服务器是数据中心
1中的一个,我们现在将停止监听端口27002的服务器,该服务器的优先级为0,并标记为位于不同数据中心中的服务器。 -
一旦服务器停止(您可以使用 mongo shell 中的
rs.status()辅助函数进行确认),再次执行以下插入操作,这次应该会出现错误:
>>>db.multiDCTest.insert({'i':2}, w='MultiDC', wtimeout=5000)
-
重新启动已停止的 mongo 服务器。
-
类似地,我们可以通过定义一个新的配置来确保写入至少传播到两个机架(在任何数据中心中)来实现机架感知。从 mongo shell 中执行以下操作:
{'MultiRack':{rack : 2}}
- 然后,conf 对象的设置值将如下所示。设置后,再次使用 mongo shell 中的
rs.reconfig(conf)重新配置复制集:
{
'getLastErrorModes' : {
'MultiDC':{datacentre : 2},
'MultiRack':{rack : 2}
}
}
-
我们看到了使用复制集标签和
WriteConcern来实现数据中心和机架感知的一些功能。让我们看看如何在读取操作中使用复制集标签。 -
我们将看到如何使用复制集标签和读取偏好。通过添加一个标签来重新配置集合,以标记一个次要成员,该成员将用于执行一些每小时的统计报告。
-
从 mongo shell 中执行以下步骤重新配置集合:
> var conf = rs.conf()
> conf.members[2].tags.type = 'reports'
> rs.reconfig(conf)
-
这将使用额外的标签 type 和值 reports 配置相同的优先级为
0的成员,并且在不同数据中心中的服务器。 -
现在我们回到 Python shell 并执行以下步骤:
>>> curs = db.multiDCTest.find(read_preference=pymongo.ReadPreference.SECONDARY,
tag_sets=[{'type':'reports'}])
>>> curs.next()
-
前面的执行应该向我们展示集合中的一个文档(因为我们在之前的步骤中向测试集合中插入了数据)。
-
停止我们标记为报告的实例,即在端口
27002上监听连接的服务器,并再次在 Python shell 上执行以下操作:
>>> curs = db.multiDCTest.find(read_preference=pymongo.ReadPreference.SECONDARY,
tag_sets=[{'type':'reports'}])
>>> curs.next()
- 这一次,执行应该失败,并指出找不到具有所需标签集的次要服务器。
工作原理…
在这个配方中,我们对标记的复制集进行了许多操作,并看到了它如何影响使用WriteConcern的写操作和使用ReadPreference的读操作。现在让我们更详细地看一下它们。
在标记的复制集中的写关注
我们设置了一个正在运行的复制集,我们重新配置以添加标签。我们为数据中心 1 中的前两台服务器和不同机架(运行监听端口27000和27001以供客户端连接)添加了标签,并为数据中心 2 中的第三台服务器(运行监听端口27002以供客户端连接)添加了标签。我们还确保了数据中心 2 中的成员不会成为主服务器,将其优先级设置为0。
我们的第一个目标是确保对副本集的写操作至少被复制到两个数据中心中的一个成员。为了确保这一点,我们定义了一个写关注,如下所示{'MultiDC':{datacentre:2}}。在这里,我们首先将写关注的名称定义为 MultiDC。值是一个 JSON 对象,其中有一个名为 datacenter 的键,与我们附加到副本集的标签使用的键相同,值是一个数字2,在被视为成功之前,应该确认写入的给定标签的不同值的数量。
例如,在我们的情况下,当写入到数据中心 1 的服务器 1 时,标签数据中心的不同值数量为 1。如果写操作被复制到第二个服务器,数量仍然保持为 1,因为标签数据中心的值与第一个成员相同。只有当第三个服务器确认写操作时,写操作才满足将写操作复制到副本集中标签datacenter的两个不同值的定义条件。请注意,该值只能是一个数字,而不能像{datacentre:'dc1'}这样的定义是无效的,并且在重新配置副本集时会抛出错误。
但是我们需要在服务器的某个地方注册这个写关注。这是通过在配置 JSON 中设置 settings 值来完成的。要设置的值是getLastErrorModes。getLastErrorModes的值是一个包含所有可能的写关注的 JSON 文档。我们稍后为至少复制到两个机架的写操作定义了一个更多的写关注。这与 MultiDC 写关注的概念一致,因此我们在这里不会详细讨论它。设置所有必需的标签和设置后,我们重新配置副本集以使更改生效。
重新配置后,我们使用 MultiDC 写关注进行一些写操作。当两个不同数据中心中的两个成员可用时,写操作成功进行。然而,当第二个数据中心的服务器宕机时,写操作超时并向发起写操作的客户端抛出异常。这表明写操作将根据我们的意图成功或失败。
我们刚刚看到了这些自定义标签如何用于解决一些有趣的用例,这些用例在产品隐式支持的情况下,对于写操作而言是不支持的。与写操作类似,读操作可以充分利用这些标签来解决一些用例,例如从标有特定值的固定次要成员集中读取。
标记的副本集中的读取偏好
我们添加了另一个自定义标签,用于注释要用于报告目的的成员,然后我们发出一个带有读取偏好的查询操作,以查询次要成员,并提供在考虑成员作为读取操作候选之前应查找的标签集。请记住,当使用主读取偏好时,我们不能使用标签,这就是我们明确指定read_preference的值为SECONDARY的原因。
配置非分片集合的默认分片
在第一章的启动一个简单的两个分片的分片环境教程中,我们设置了一个简单的两个分片服务器。在第一章的连接到 shell 中的分片并执行操作教程中,我们向一个被分片的 person 集合添加了数据。然而,对于任何未被分片的集合,所有文档最终都会位于一个称为主分片的分片上。对于相对较小数量的集合的小型数据库,这种情况是可以接受的。然而,如果数据库大小增加,同时未被分片的集合数量也增加,我们就会使一个特定的分片(对于数据库来说是主分片)过载,因为来自这些未被分片的集合的大量数据都会存储在这个分片上。所有对这些未被分片的集合以及那些在分片中特定范围内的集合的查询操作都将被定向到这个分片。在这种情况下,我们可以将数据库的主分片更改为其他实例,以便这些未被分片的集合在不同的实例之间得到平衡。
在这个教程中,我们将看到如何查看这个主分片,并在需要时将其更改为其他服务器。
做好准备
按照第一章中的启动一个简单的两个分片的分片环境教程,设置并启动一个分片环境。从 shell 连接到已启动的 mongos 进程。另外,假设两个分片服务器分别监听端口27000和27001,从 shell 连接到这两个进程。因此,我们总共打开了三个 shell,一个连接到 mongos 进程,另外两个连接到这两个单独的分片。
我们需要在这个教程中使用test数据库,并且必须在其上启用分片。如果没有启用,则需要在连接到 mongos 进程的 shell 上执行以下操作:
mongos> use test
mongos> sh.enableSharding('test')
如何做...
- 从连接到 mongos 进程的 shell 中,执行以下两个命令:
mongos> db.testCol.insert({i : 1})
mongos> sh.status()
- 在数据库中,查找
test数据库,并记下primary。假设以下是sh.status()输出的一部分(仅显示数据库部分):
databases:
{ "_id" : "admin", "partitioned" : false, "primary" : "config" }
{ "_id" : "test", "partitioned" : true, "primary" : "shard0000" }
数据库下的第二个文档显示,数据库test已启用分片(因为 partitioned 为 true),主分片是shard0000。
- 在我们的情况下,主分片是
shard0000,是监听端口27000的 mongod 进程。打开连接到此进程的 shell,并在其中执行以下操作:
> db.testCol.find()
- 现在,连接到另一个监听端口
27001的 mongod 进程,并再次执行以下查询:
> db.testCol.find()
请注意,数据只会在主分片上找到,而不会在其他分片上找到。
- 从 mongos shell 执行以下命令:
mongos> use admin
mongos> db.runCommand({movePrimary:'test', to:'shard0001'})
- 从连接到 mongos 进程的 mongo shell 执行以下命令:
mongos> sh.status()
- 从运行在端口
27000和27001的 mongos 进程连接的 shell 中,执行以下查询:
> db.testCol.find()
它是如何工作的...
我们启动了一个分片设置,并从 mongos 进程连接到它。我们首先在test数据库中的testCol集合中插入了一个文档,该集合也没有启用分片。在这种情况下,数据位于称为主分片的分片上。不要将其误解为副本集的主分片。这是一个分片(它本身可以是一个副本集),它是默认选择的所有未启用分片的数据库和集合的分片。
当我们将数据添加到非分片集合时,只能在主分片上看到。执行sh.status()告诉我们主分片。要更改主分片,我们需要从连接到 mongos 进程的 shell 中的 admin 数据库执行命令。命令如下:
db.runCommand({movePrimary:'<database whose primary shard is to be changed>', to:'<target shard>'})
一旦主分片更改,所有非分片数据库和集合的现有数据都将迁移到新的主分片,并且所有后续写入非分片集合的操作都将转到此分片。
使用此命令时要小心,因为它将把所有未分片的集合迁移到新的主分片,这可能需要大量时间。
手动拆分和迁移块
尽管 MongoDB 在跨分片上拆分和迁移块以保持平衡方面做得很好,但在某些情况下,例如文档数量较少或相对较多的小文档数量,自动平衡器无法拆分集合,管理员可能希望手动拆分和迁移块。在本教程中,我们将看到如何手动在分片之间拆分和迁移集合。对于本教程,我们将设置一个简单的分片,就像我们在第一章中看到的那样,安装和启动服务器。
做好准备
请参阅第一章中的启动一个简单的两个分片的分片环境,安装和启动服务器,以设置和启动分片环境。最好在没有任何数据的情况下启动一个干净的环境。从 shell 连接到已启动的 mongos 进程。
如何做…
- 从 mongo shell 连接到 mongos 进程,并按以下方式在
test数据库和splitAndMoveTest集合上启用分片:
> sh.enableSharding('test')
> sh.shardCollection('test.splitAndMoveTest', {_id:1}, false)
- 让我们按照以下方式在集合中加载数据:
> for(i = 1; i <= 10000 ; i++) db.splitAndMoveTest.insert({_id : i})
- 一旦数据加载完成,执行以下操作:
> db. splitAndMoveTest.find().explain()
注意计划中两个分片中的文档数量。要注意的值在解释计划结果的 shards 键下的两个文档中。在这两个文档中,要注意的字段是n。
- 执行以下命令以查看集合的拆分:
> config = db.getSisterDB('config')
> config.chunks.find({ns:'test.splitAndMoveTest'}).pretty()
- 在
5000处将块拆分为两个部分:
> sh.splitAt('test.splitAndMoveTest', {_id:5000})
- 拆分它不会将其迁移到第二个服务器。通过再次执行以下查询来查看块的确切情况:
> config.chunks.find({ns:'test.splitAndMoveTest'}).pretty()
- 现在我们将第二个块移动到第二个分片:
> sh.moveChunk('test.splitAndMoveTest', {_id:5001}, 'shard0001')
- 再次执行以下查询并确认迁移:
> config.chunks.find({ns:'test.splitAndMoveTest'}).pretty()
- 或者,以下解释计划将显示大约 50-50 的拆分:
> db. splitAndMoveTest.find().explain()
工作原理…
我们通过添加单调递增的数字来模拟小数据负载,并发现查询计划中的数字没有均匀分布在两个分片上。这不是问题,因为在平衡器决定迁移块以保持平衡之前,块大小需要达到特定的阈值,默认为 64 MB。这非常完美,因为在现实世界中,当数据量变得巨大时,我们将看到随着时间的推移,分片将保持良好的平衡。
但是,如果管理人员决定手动拆分和迁移块,可以手动执行。有两个辅助函数sh.splitAt和sh.moveChunk来执行这项工作。让我们看看它们的签名并了解它们的作用。
函数sh.splitAt接受两个参数,第一个是命名空间,格式为<数据库>.<集合名称>,第二个参数是作为拆分点的查询,将块拆分为两个部分,可能是两个不均匀的部分,取决于给定文档在块中的位置。还有另一种方法sh.splitFind,它将尝试将块分成两个相等的部分。
拆分并不意味着块移动到另一个分片,它只是将一个大块分成两个,但数据仍然留在同一个分片上。这是一个廉价的操作,涉及更新配置数据库。
接下来,我们执行的是将块分成两部分后将其迁移到不同分片的操作。操作sh.MoveChunk就是用来做这个的。这个函数接受三个参数,第一个是集合的命名空间,格式为<数据库>.<集合名称>,第二个参数是一个查询文档,其块将被迁移,第三个参数是目标块。
迁移完成后,查询计划显示数据分为两个块。
使用标签进行领域驱动分片
在第一章的安装和启动服务器中,启动一个简单的两个分片的环境和在 shell 中连接到一个分片并执行操作解释了如何启动一个简单的两个服务器分片,然后在选择分片键后插入集合中的数据。被分片的数据更加技术化,Mongo 通过将其分成多个块并迁移这些块来保持块在分片之间的均匀分布。但是,如果我们希望分片更加领域化呢?假设我们有一个用于存储邮政地址的数据库,我们根据我们知道的城市的邮政编码范围来进行分片。我们可以根据城市名称作为标签对分片服务器进行标记,添加分片范围(邮政编码),并将此范围与标签关联起来。这样,我们可以说明哪些服务器可以包含哪些城市的邮政地址。例如,我们知道孟买是人口最多的城市,地址数量将会很大,因此我们为孟买添加了两个分片。另一方面,一个分片应该足够应对浦那市的数量。目前我们只标记了一个分片。在这个配方中,我们将看到如何使用标签感知分片来实现这个用例。如果描述令人困惑,不用担心,我们将看到如何实现我们刚刚讨论的内容。
准备工作
在第一章的安装和启动服务器中,参考配方启动一个简单的两个分片的环境,了解如何启动一个简单的分片的信息。然而,对于这个配方,我们将添加一个额外的分片。因此,我们现在将启动三个监听端口为27000、27001和27002的 mongo 服务器。同样,建议从一个干净的数据库开始。为了这个配方,我们将使用集合userAddress来存储数据。
如何做…
- 假设我们有三个分片正在运行,让我们执行以下操作:
mongos> sh.addShardTag('shard0000', 'Mumbai')
mongos> sh.addShardTag('shard0001', 'Mumbai')
mongos> sh.addShardTag('shard0002', 'Pune')
- 定义了标签后,让我们定义将映射到标签的邮政编码范围:
mongos> sh.addTagRange('test.userAddress', {pincode:400001}, {pincode:400999}, 'Mumbai')
mongos> sh.addTagRange('test.userAddress', {pincode:411001}, {pincode:411999}, 'Pune')
- 为测试数据库和
userAddress集合启用分片,如下所示:
mongos> sh.enableSharding('test')
mongos> sh.shardCollection('test.userAddress', {pincode:1})
- 在
userAddress集合中插入以下文档:
mongos> db.userAddress.insert({_id:1, name: 'Varad', city: 'Pune', pincode: 411001})
mongos> db.userAddress.insert({_id:2, name: 'Rajesh', city: 'Mumbai', pincode: 400067})
mongos> db.userAddress.insert({_id:3, name: 'Ashish', city: 'Mumbai', pincode: 400101})
- 执行以下计划:
mongos> db.userAddress.find({city:'Pune'}).explain()
mongos> db.userAddress.find({city:'Mumbai'}).explain()
它是如何工作的…
假设我们想要根据领域来分区数据,我们可以使用标签感知分片。这是一个很好的机制,让我们可以为分片打标签,然后根据标签将数据范围分布到被标记的分片上。我们实际上不需要担心托管分片的实际机器和它们的地址。标签在这方面起到了很好的抽象作用,我们可以为分片打上多个标签,一个标签可以应用到多个分片上。
在我们的情况下,我们有三个分片,并使用sh.addShardTag方法为每个分片应用标签。该方法接受分片 ID,我们可以在sh.status调用中看到带有shards键的分片 ID。sh.addShardTag方法可用于不断向分片添加标签。类似地,还有一个辅助方法sh.removeShardTag,用于从分片中删除标签的分配。这两种方法都接受两个参数,第一个是分片 ID,第二个是要删除的标签。
标记完成后,我们将分片键的值范围分配给标记。使用方法sh.addTagRange来执行。它接受四个参数,第一个是命名空间,即集合的完全限定名称,第二个和第三个参数是此分片键范围的起始和结束值,第四个参数是添加范围的分片的标记名称。例如,调用sh.addTagRange('test.userAddress', {pincode:400001}, {pincode:400999}, 'Mumbai')表示我们正在为集合test.userAddress添加分片范围400001到400999,并且此范围将存储在标记为Mumbai的分片中。
标记和添加标记范围完成后,我们在数据库和集合上启用了分片,并从孟买和浦那城市的相应邮政编码添加了数据。然后,我们查询并解释计划,以查看数据确实驻留在我们为浦那和孟买城市标记的分片上。我们还可以向此分片设置添加新的分片,并相应地标记新的分片。然后,平衡器将根据其标记的值平衡数据。例如,如果浦那的地址增加导致分片过载,我们可以添加一个新的标记为浦那的分片。然后,浦那的邮政地址将在这两个标记为浦那城市的服务器实例上进行分片。
在分片设置中探索配置数据库
配置数据库是 Mongo 中分片设置的支柱。它存储分片设置的所有元数据,并且有一个专用的 mongod 进程在运行。当启动 mongos 进程时,我们会为其提供配置服务器的 URL。在本教程中,我们将深入研究配置数据库中的一些集合,并深入了解它们的内容和重要性。
准备工作
我们需要为本教程准备一个分片设置。有关如何启动一个简单的分片的信息,请参考第一章中的启动一个简单的两个分片的分片环境,安装和启动服务器。此外,从 shell 连接到 mongos 进程。
如何做…
- 从连接到 mongos 进程的控制台,切换到配置数据库并执行以下操作:
mongos> use config
mongos>show collections
- 从所有集合的列表中,我们将访问一些。我们从数据库集合开始。这会跟踪此分片上的所有数据库。在 shell 上执行以下操作:
mongos> db.databases.find()
-
结果的内容非常简单,字段
_id的值是数据库的值。字段 partitioned 的值告诉我们数据库是否启用了分片;true 表示已启用,并且字段 primary 给出了非分片集合数据所在的主分片。 -
接下来,我们将访问
collections集合。在 shell 上执行以下操作:
mongos> db.collections.find().pretty()
与我们之前看到的数据库集合不同,此集合仅包含我们已启用分片的集合。字段_id以<database>.<collection name>格式给出集合的命名空间,字段 key 给出分片键,字段 unique 指示分片键是否唯一。这三个字段按照sh.shardCollection函数的三个参数的顺序给出。
- 接下来,我们查看
chunks集合。在 shell 上执行以下操作。如果数据库在我们开始本教程时是干净的,那么在这里我们不会有很多数据:
mongos> db.chunks.find().pretty()
- 然后,我们查看 tags 集合并执行以下查询:
mongos> db.tags.find().pretty()
- 让我们按照以下方式查询 mongos 集合。
mongos> db.mongos.find().pretty()
这是一个简单的集合,它列出了所有连接到具有详细信息的 mongos 实例的分片,例如 mongos 实例正在运行的主机和端口,这形成了_id字段。版本和数字,例如进程运行的时间。
- 最后,我们查看 version 集合。执行以下查询。请注意,这与我们执行的其他查询不同:
mongos>db.getCollection('version').findOne()
它是如何工作的...
当我们查询集合和数据库集合时,它们非常简单。让我们来看看名为chunks的集合。这是该集合中的一个示例文档:
{
"_id" : "test.userAddress-pincode_400001.0",
"lastmod" : Timestamp(1, 3),
"lastmodEpoch" : ObjectId("53026514c902396300fd4812"),
"ns" : "test.userAddress",
"min" : {
"pincode" : 400001
},
"max" : {
"pincode" : 411001
},
"shard" : "shard0000"
}
感兴趣的字段是ns、min、max和shard,分别是集合的命名空间、块中存在的最小值、块中存在的最大值以及该块所在的分片。默认情况下,块大小的值为 64 MB。这可以在设置集合中看到。从 shell 中执行db.settings.find()并查看字段值的值,即块的大小(以 MB 为单位)。块的大小受限于这个小的大小,以便在需要时跨分片进行迁移过程。当块的大小超过这个阈值时,Mongo 服务器会找到现有块中合适的点将其分成两部分,并在这个块集合中添加一个新条目。这个操作被称为分裂,它是廉价的,因为数据仍然保持在原地;它只是在逻辑上分成多个块。Mongo 上的平衡器会尝试保持分片之间的块平衡,一旦发现不平衡,它就会将这些块迁移到不同的分片。这是昂贵的,也在很大程度上取决于网络带宽。如果我们使用sh.status(),实际上是查询我们看到的集合,并打印出漂亮格式的结果。
第五章:高级操作
在本章中,我们将涵盖以下内容:
-
原子查找和修改操作
-
在 Mongo 中实现原子计数器
-
实现服务器端脚本
-
在 MongoDB 中创建和追踪封顶集合游标
-
将普通集合转换为封顶集合
-
在 Mongo 中存储二进制数据
-
使用 GridFS 在 Mongo 中存储大数据
-
从 Java 客户端将数据存储到 GridFS
-
从 Python 客户端将数据存储到 GridFS
-
使用 oplog 在 Mongo 中实现触发器
-
在 Mongo 中使用平面(2D)地理空间索引进行查询
-
在 Mongo 中使用球形索引和 GeoJSON 兼容数据
-
在 Mongo 中实现全文搜索
-
将 MongoDB 集成到 Elasticsearch 进行全文搜索
介绍
在第二章中,命令行操作和索引,我们看到了如何从 shell 执行基本操作来查询、更新和插入文档,还看到了不同类型的索引和索引创建。在本章中,我们将看到 Mongo 的一些高级功能,如 GridFS、地理空间索引和全文搜索。我们还将看到其他配方,包括封顶集合的介绍和使用以及在 MongoDB 中实现服务器端脚本。
原子查找和修改操作
在第二章中,命令行操作和索引,我们有一些配方解释了我们在 MongoDB 中执行的各种 CRUD 操作。有一个概念我们没有涵盖到,那就是原子查找和修改文档。修改包括更新和删除操作。在这个配方中,我们将介绍 MongoDB 的findAndModify操作的基础知识。在下一个配方中,我们将使用这种方法来实现一个计数器。
准备就绪
查看第一章中的安装单节点 MongoDB和安装和启动服务器的配方,并启动 MongoDB 的单个实例。这是这个配方的唯一先决条件。启动 mongo shell 并连接到已启动的服务器。
如何操作…
- 我们将在
atomicOperationsTest集合中测试一个文档。从 shell 执行以下操作:
> db.atomicOperationsTest.drop()
> db.atomicOperationsTest.insert({i:1})
- 从 mongo shell 执行以下操作并观察输出:
> db.atomicOperationsTest.findAndModify({
query: {i: 1},
update: {$set : {text : 'Test String'}},
new: false
}
)
- 这次我们将执行另一个操作,但参数略有不同;观察此操作的输出:
> db.atomicOperationsTest.findAndModify({
query: {i: 1},
update: {$set : {text : 'Updated String'}}, fields: {i: 1, text :1, _id:0},
new: true
}
)
- 这次我们将执行另一个更新,将会插入文档,如下所示:
>db.atomicOperationsTest.findAndModify({
query: {i: 2},
update: {$set : {text : 'Test String'}},
fields: {i: 1, text :1, _id:0},
upsert: true,
new: true
}
)
- 现在,按照以下方式查询集合并查看当前存在的文档:
> db.atomicOperationsTest.find().pretty()
- 最后,我们将按以下方式执行删除:
>db.atomicOperationsTest.findAndModify({
query: {i: 2},
remove: true,
fields: {i: 1, text :1, _id:0},
new: false
}
)
工作原理…
如果我们在 MongoDB 中首先查找文档,然后再更新它,结果可能不如预期。在查找和更新操作之间可能存在交错的更新,这可能已更改文档状态。在某些特定用例中,比如实现原子计数器,这是不可接受的,因此我们需要一种方法来原子地查找、更新和返回文档。返回的值是在更新应用之前或之后的值,由调用客户端决定。
现在我们已经执行了前一节中的步骤,让我们看看我们实际做了什么,以及作为参数传递给findAndModify操作的 JSON 文档中的所有这些字段的含义。从第 3 步开始,我们将一个包含字段query、update和new的文档作为参数传递给findAndModify函数。
query字段指定用于查找文档的搜索参数,update字段包含需要应用的修改。第三个字段new,如果设置为true,告诉 MongoDB 返回更新后的文档。
在第 4 步中,我们实际上向作为参数传递的文档添加了一个名为fields的新字段,用于从返回的结果文档中选择一组有限的字段。此外,new字段的值为true,表示我们希望更新的文档,即在执行更新操作之后的文档,而不是之前的文档。
第 5 步包含一个名为upsert的新字段,该字段执行 upsert(更新+插入)文档。也就是说,如果找到具有给定查询的文档,则更新该文档,否则创建并更新一个新文档。如果文档不存在并且发生了 upsert,那么将参数new的值设置为false将返回null。这是因为在执行更新操作之前没有任何内容存在。
最后,在第 7 步中,我们使用了remove字段,其值为true,表示要删除文档。此外,new字段的值为false,这意味着我们期望被删除的文档。
另请参阅
原子FindandModify操作的一个有趣的用例是在 Mongo 中开发原子计数器。在下一个配方中,我们将看到如何实现这个用例。
在 Mongo 中实现原子计数器
原子计数器是许多用例的必需品。Mongo 没有原子计数器的内置功能;然而,可以使用一些其很酷的功能很容易地实现。事实上,借助先前描述的findAndModify()命令,实现起来非常简单。参考之前的配方原子查找和修改操作,了解 Mongo 中的原子查找和修改操作是什么。
准备就绪
查看第一章中的配方安装单节点 MongoDB,开始 Mongo 的单个实例。这是此配方的唯一先决条件。启动 mongo shell 并连接到已启动的服务器。
如何操作…
- 从 mongo shell 中执行以下代码:
> function getNextSequence(counterId) {
return db.counters.findAndModify(
{
query: {_id : counterId},
update: {$inc : {count : 1}},
upsert: true,
fields:{count:1, _id:0},
new: true
}
).count
}
- 现在从 shell 中调用以下命令:
> getNextSequence('Posts Counter')
> getNextSequence('Posts Counter')
> getNextSequence('Profile Counter')
工作原理…
该函数就是在用于存储所有计数器的集合上执行的findAndModify操作。计数器标识符是存储的文档的_id字段,计数器的值存储在count字段中。传递给findAndModify操作的文档接受查询,该查询唯一标识存储当前计数的文档,即使用_id字段的查询。更新操作是一个$inc操作,将通过 1 递增count字段的值。但是如果文档不存在怎么办?这将发生在对计数器的第一次调用。为了处理这种情况,我们将将upsert标志设置为true。count的值将始终从 1 开始,没有办法接受任何用户定义的序列起始数字或自定义递增步长。为了满足这样的要求,我们将不得不将具有初始化值的文档添加到计数器集合中。最后,我们对计数器值递增后的状态感兴趣;因此,我们将new字段的值设置为true。
在调用此方法三次(就像我们做的那样)后,我们应该在计数器集合中看到以下内容。只需执行以下查询:
>db.counters.find()
{ "_id" : "Posts Counter", "count" : 2 }
{ "_id" : "Profile Counter", "count" : 1 }
使用这个小函数,我们现在已经在 Mongo 中实现了原子计数器。
另请参阅
我们可以将这样的通用代码存储在 Mongo 服务器上,以便在其他函数中执行。查看配方实现服务器端脚本,了解如何在 Mongo 服务器上存储 JavaScript 函数。这甚至允许我们从其他编程语言客户端调用此函数。
实现服务器端脚本
在这个配方中,我们将看到如何编写服务器存储的 JavaScript,类似于关系数据库中的存储过程。这是一个常见的用例,其他代码片段需要访问这些常见函数,我们将它们放在一个中心位置。为了演示服务器端脚本,该函数将简单地添加两个数字。
这个配方有两个部分。首先,我们看看如何从客户端 JavaScript shell 中的集合加载脚本,其次,我们将看到如何在服务器上执行这些函数。
注意
文档明确提到不建议使用服务器端脚本。安全性是一个问题,尽管如果数据没有得到适当的审计,因此我们需要小心定义哪些函数。自 Mongo 2.4 以来,服务器端 JavaScript 引擎是 V8,可以并行执行多个线程,而不是 Mongo 2.4 之前的引擎,每次只能执行一个线程。
准备工作
查看第一章中的配方安装单节点 MongoDB,安装和启动服务器并启动 Mongo 的单个实例。这是这个配方的唯一先决条件。启动一个 mongo shell 并连接到已启动的服务器。
如何做...
- 创建一个名为
add的新函数,并将其保存到集合db.system.js中,如下所示。当前数据库应该是 test:
> use test
> db.system.js.save({ _id : 'add', value : function(num1, num2) {return num1 + num2}})
- 现在这个函数已经定义,加载所有函数如下:
> db.loadServerScripts()
- 现在,调用
add并查看是否有效:
> add(1, 2)
- 现在我们将使用这个函数,并在服务器端执行它:从 shell 执行以下操作:
> use test
> db.eval('return add(1, 2)')
- 执行以下步骤(可以执行前面的命令):
> use test1
> db.eval('return add(1, 2)')
它是如何工作的...
集合system.js是一个特殊的 MongoDB 集合,用于存储 JavaScript 代码。我们使用该集合中的save函数添加一个新的服务器端 JavaScript。save函数只是一个方便的函数,如果文档不存在则插入文档,如果文档已存在则更新文档。目标是向该集合添加一个新文档,即使您可以使用insert或upsert来添加。
秘密在于loadServerScripts方法。让我们看看这个方法的代码:this.system.js.find().forEach(function(u){eval(u._id + " = " + u.value);});
它使用eval函数评估 JavaScript,并为system.js集合中每个文档的value属性中定义的函数分配一个与文档的_id字段中给定的名称相同的变量。
例如,如果集合system.js中存在以下文档,{ _id : 'add', value : function(num1, num2) {return num1 + num2}},那么文档的value字段中给定的函数将分配给当前 shell 中名为add的变量。文档的_id字段中给定了值add。
这些脚本实际上并不在服务器上执行,但它们的定义存储在服务器的一个集合中。JavaScript 方法loadServerScripts只是在当前 shell 中实例化一些变量,并使这些函数可用于调用。执行这些函数的是 shell 的 JavaScript 解释器,而不是服务器。集合system.js在数据库的范围内定义。一旦加载,这些函数就像在 shell 中定义的 JavaScript 函数一样,在 shell 的范围内都是可用的,而不管当前活动的数据库是什么。
就安全性而言,如果 shell 连接到启用了安全性的服务器,则调用loadServerScripts的用户必须具有读取数据库中集合的权限。有关启用安全性和用户可以拥有的各种角色的更多详细信息,请参阅第四章中的食谱在 Mongo 中设置用户,管理。正如我们之前所看到的,loadServerScripts函数从system.js集合中读取数据,如果用户没有权限从该集合中读取数据,则函数调用将失败。除此之外,从加载后的 shell 中执行的函数应该具有适当的权限。例如,如果函数在任何集合中插入/更新数据,则用户应该对从函数访问的特定集合具有读取和写入权限。
在服务器上执行脚本可能是人们期望的服务器端脚本,而不是在连接的 shell 中执行。在这种情况下,函数在服务器的 JavaScript 引擎上进行评估,安全检查更为严格,因为长时间运行的函数可能会持有锁,对性能产生不利影响。在服务器端调用 JavaScript 代码执行的包装器是db.eval函数,接受要在服务器端评估的代码以及参数(如果有)。
在评估函数之前,写操作会获取全局锁;如果使用参数nolock,则可以跳过这一步。例如,可以按照以下方式调用前面的add函数,而不是调用db.eval并获得相同的结果。我们另外提供了nolock字段,指示服务器在评估函数之前不要获取全局锁。如果此函数要在集合上执行写操作,则nolock字段将被忽略。
> db.runCommand({eval: function (num1, num2) {return num1 + num2}, args:[1, 2],nolock:true})
如果服务器启用了安全性,则调用用户需要具有以下四个角色:userAdminAnyDatabase、dbAdminAnyDatabase、readWriteAnyDatabase和clusterAdmin(在管理数据库上)才能成功调用db.eval函数。
编程语言确实提供了一种调用这种服务器端脚本的方法,使用eval函数。例如,在 Java API 中,类com.mongodb.DB有一个方法eval来调用服务器端的 JavaScript 代码。当我们想要避免数据不必要的网络流量并将结果传递给客户端时,这种服务器端执行非常有用。然而,在数据库服务器上有太多的逻辑可能会很快使事情难以维护,并严重影响服务器的性能。
注意
截至 MongoDB 3.0.3,db.eval()方法已被弃用,建议用户不要依赖该方法,而是使用客户端脚本。有关更多详细信息,请参阅jira.mongodb.org/browse/SERVER-17453。
在 MongoDB 中创建和追踪固定大小集合的游标
固定大小的集合是固定大小的集合,其中文档被添加到集合的末尾,类似于队列。由于固定大小的集合有一个固定的大小,如果达到限制,旧的文档将被删除。
它们按插入顺序自然排序,任何需要按时间顺序检索的检索都可以使用$natural排序顺序进行检索。这使得文档检索非常快速。
下图给出了一个有限大小的集合的图形表示,足以容纳最多三个相等大小的文档(对于任何实际用途来说都太小,但用于理解是很好的)。正如我们在图像中所看到的,该集合类似于循环队列,其中最旧的文档将被新添加的文档替换,如果集合变满。可追加的游标是特殊类型的游标,类似于 Unix 中的 tail 命令,它们遍历集合,类似于普通游标,但同时等待集合中的数据是否可用。我们将在本节详细介绍有限集合和可追加游标。

准备工作
查看第一章中的配方安装单节点 MongoDB,安装和启动服务器并启动 Mongo 的单个实例。这是本配方的唯一先决条件。启动 MongoDB shell 并连接到已启动的服务器。
操作步骤...
这个配方有两个部分:在第一部分中,我们将创建一个名为testCapped的有限集合,并尝试对其执行一些基本操作。接下来,我们将在这个有限集合上创建一个可追加游标。
- 如果已存在具有此名称的集合,请删除该集合。
> db.testCapped.drop()
- 现在按以下方式创建一个有限集合。请注意,此处给定的大小是为集合分配的字节数,而不是它包含的文档数量:
> db.createCollection('testCapped', {capped : true, size:100})
- 现在我们将按以下方式在有限集合中插入 100 个文档:
> for(i = 1; i < 100; i++) {
db.testCapped.insert({'i':i, val:'Test capped'})
}
- 现在按以下方式查询集合:
> db.testCapped.find()
- 尝试按以下方式从集合中删除数据:
> db.testCapped.remove()
-
现在我们将创建并演示一个可追加游标。建议您将以下代码片段输入/复制到文本编辑器中,并随时准备执行。
-
要在集合中插入数据,我们将使用以下代码片段。在 shell 中执行此代码片段:
> for(i = 101 ; i < 500 ; i++) {
sleep(1000)
db.testCapped.insert({'i': i, val :'Test Capped'})
}
- 要追加有限集合,我们使用以下代码片段:
> var cursor = db.testCapped.find().addOption(DBQuery.Option.tailable).addOption(DBQuery.Option.awaitData)
while(cursor.hasNext()) {
var next = cursor.next()
print('i: ' + next.i + ', value: ' + next.val)
}
-
打开一个 shell 并连接到正在运行的 mongod 进程。这将是第二个打开并连接到服务器的 shell。在此 shell 中复制并粘贴第 8 步中提到的代码,然后执行它。
-
观察插入的记录如何显示为它们插入到有限集合中。
工作原理...
我们将使用createCollection函数显式创建一个有限集合。这是创建有限集合的唯一方法。createCollection函数有两个参数。第一个是集合的名称,第二个是一个包含两个字段capped和size的 JSON 文档,用于通知用户集合是否被限制以及集合的大小(以字节为单位)。还可以提供一个额外的max字段来指定集合中的最大文档数。即使指定了max字段,也需要size字段。然后我们插入和查询文档。当我们尝试从集合中删除文档时,我们会看到一个错误,即不允许从有限集合中删除文档。它只允许在添加新文档并且没有空间可容纳它们时才能删除文档。
接下来我们看到的是我们创建的可追溯游标。我们启动两个 shell,其中一个是以 1 秒的间隔插入文档的普通插入。在第二个 shell 中,我们创建一个游标并遍历它,并将从游标获取的文档打印到 shell 上。然而,我们添加到游标的附加选项使得有所不同。添加了两个选项,DBQuery.Option.tailable和DBQuery.Option.awaitData。这些选项用于指示游标是可追溯的,而不是正常的,其中最后的位置被标记,我们可以恢复到上次离开的位置,其次是在没有数据可用时等待更多数据一段时间,以及当我们接近游标的末尾时立即返回而不是返回。awaitData选项只能用于可追溯游标。这两个选项的组合使我们感觉类似于 Unix 文件系统中的 tail 命令。
有关可用选项的列表,请访问以下页面:docs.mongodb.org/manual/reference/method/cursor.addOption/。
还有更多…
在下一个配方中,我们将看到如何将普通集合转换为固定集合。
将普通集合转换为固定集合
本配方将演示将普通集合转换为固定集合的过程。
准备就绪
查看第一章中的安装单节点 MongoDB和安装和启动服务器的配方,并启动 Mongo 的单个实例。这是本配方的唯一先决条件。启动 mongo shell 并连接到已启动的服务器。
如何做…
- 执行以下操作以确保您在
test数据库中:
> use test
- 按照以下方式创建一个普通集合。我们将向其添加 100 个文档,将以下代码片段输入/复制到 mongo shell 上并执行。命令如下:
for(i = 1 ; i <= 100 ; i++) {
db.normalCollection.insert({'i': i, val :'Some Text Content'})
}
- 按照以下方式查询集合以确认其中包含数据:
> db.normalCollection.find()
- 现在,按照以下方式查询集合
system.namespaces,并注意结果文档:
> db.system.namespaces.find({name : 'test.normalCollection'})
- 执行以下命令将集合转换为固定集合:
> db.runCommand({convertToCapped : 'normalCollection', size : 100})
- 查询集合以查看数据:
> db.normalCollection.find()
- 按照以下方式查询集合
system.namespaces,并注意结果文档:
> db.system.namespaces.find({name : 'test.normalCollection'})
它是如何工作的…
我们创建了一个包含 100 个文档的普通集合,然后尝试将其转换为具有 100 字节大小的固定集合。命令将以下 JSON 文档传递给runCommand函数,{convertToCapped: <普通集合的名称>, size: <固定集合的字节大小>}。此命令创建一个具有指定大小的固定集合,并将文档以自然顺序从普通集合加载到目标固定集合中。如果固定集合的大小达到所述限制,旧文档将以 FIFO 顺序删除,为新文档腾出空间。完成后,创建的固定集合将被重命名。在固定集合上执行查找确认,最初在普通集合中存在的 100 个文档并不都存在于固定集合中。在执行convertToCapped命令之前和之后对system.namespaces集合进行查询,显示了collection属性的变化。请注意,此操作获取全局写锁,阻止此数据库中的所有读取和写入操作。此外,对于转换后的固定集合,不会创建原始集合上存在的任何索引。
还有更多…
Oplog 是 MongoDB 中用于复制的重要集合,是一个有上限的集合。有关复制和 oplogs 的更多信息,请参阅第四章中的理解和分析 oplogs,管理中的配方。在本章的后面的一个配方中,我们将使用这个 oplog 来实现类似于关系数据库中的插入/更新/删除触发器的功能。
在 Mongo 中存储二进制数据
到目前为止,我们看到了如何在文档中存储文本值、日期和数字字段。有时还需要在数据库中存储二进制内容。考虑用户需要在数据库中存储文件的情况。在关系数据库中,BLOB 数据类型最常用于满足这一需求。MongoDB 也支持将二进制内容存储在集合中的文档中。问题在于文档的总大小不应超过 16MB,这是写作本书时文档大小的上限。在这个配方中,我们将把一个小图像文件存储到 Mongo 的文档中,并在以后检索它。如果您希望存储在 MongoDB 集合中的内容大于 16MB,则 MongoDB 提供了一个名为GridFS的开箱即用的解决方案。我们将在本章的另一个配方中看到如何使用 GridFS。
准备工作
查看第一章中的安装单节点 MongoDB配方,安装和启动服务器并启动 MongoDB 的单个实例。还有一个用于将二进制内容写入文档的程序是用 Java 编写的。有关 Java 驱动程序的更多详细信息,请参阅第三章中的使用 Java 客户端执行查询和插入操作,使用 Java 客户端实现 Mongo 中的聚合和使用 Java 客户端在 Mongo 中执行 MapReduce的配方,编程语言驱动程序。打开一个 mongo shell 并连接到监听端口27017的本地 MongoDB 实例。对于这个配方,我们将使用项目mongo-cookbook-bindata。这个项目可以从 Packt 网站下载的源代码包中获取。需要在本地文件系统上提取文件夹。打开一个命令行 shell 并转到提取的项目的根目录。应该是找到文件pom.xml的目录。
如何做…
- 在操作系统 shell 中,
mongo-cookbook-bindata项目的当前目录中存在pom.xml,执行以下命令:
$ mvn exec:java -Dexec.mainClass=com.packtpub.mongo.cookbook.BinaryDataTest
-
观察输出;执行应该成功。
-
切换到连接到本地实例的 mongo shell 并执行以下查询:
> db.binaryDataTest.findOne()
- 滚动文档并记下文档中的字段。
工作原理…
如果我们滚动查看打印出的大型文档,我们会看到字段fileName,size和data。前两个字段分别是字符串和数字类型,我们在文档创建时填充了这些字段,并保存了我们提供的文件名和以字节为单位的大小。数据字段是 BSON 类型 BinData 的字段,我们在其中看到数据以 Base64 格式编码。
以下代码行显示了我们如何填充添加到集合中的 DBObject:
DBObject doc = new BasicDBObject("_id", 1);
doc.put("fileName", resourceName);
doc.put("size", imageBytes.length);
doc.put("data", imageBytes);
如上所示,使用两个字段fileName和size来存储文件名和文件大小,分别为字符串和数字类型。数据字段作为字节数组添加到DBObject中,它会自动存储为文档中的 BSON 类型 BinData。
另请参阅
在这个配方中,我们看到的是直接的,只要文档大小小于 16MB。如果存储的文件大小超过这个值,我们必须求助于像 GridFS 这样的解决方案,这在下一个配方使用 GridFS 在 Mongo 中存储大数据中有解释。
使用 GridFS 在 Mongo 中存储大数据
MongoDB 中的文档大小可以达到 16 MB。 但这是否意味着我们不能存储超过 16 MB 大小的数据? 有些情况下,您更喜欢将视频和音频文件存储在数据库中,而不是在文件系统中,因为有许多优势,比如存储与它们一起的元数据,从中间位置访问文件时,以及在 MongoDB 服务器实例上启用复制时为了高可用性而复制内容。 GridFS 可以用来解决 MongoDB 中的这些用例。 我们还将看到 GridFS 如何管理超过 16 MB 的大容量,并分析其用于在幕后存储内容的集合。 为了测试目的,我们不会使用超过 16 MB 的数据,而是使用一些更小的数据来查看 GridFS 的运行情况。
准备工作
查看第一章中的配方安装单节点 MongoDB,安装和启动服务器并启动 Mongo 的单个实例。 这是此配方的唯一先决条件。 启动 Mongo shell 并连接到已启动的服务器。 另外,我们将使用 mongofiles 实用程序从命令行将数据存储在 GridFS 中。
如何做...
-
下载该书的代码包,并将图像文件
glimpse_of_universe-wide.jpg保存到本地驱动器(您可以选择任何其他大文件作为事实,并使用我们执行的命令提供适当的文件名)。 为了举例,图像保存在主目录中。 我们将把我们的步骤分为三个部分。 -
在服务器运行并且当前目录为主目录的情况下,从操作系统的 shell 中执行以下命令。 这里有两个参数。 第一个是本地文件系统上文件的名称,第二个是将附加到 MongoDB 中上传内容的名称。
$ mongofiles put -l glimpse_of_universe-wide.jpg universe.jpg
- 现在让我们查询集合,看看这些内容实际上是如何在幕后的集合中存储的。 打开 shell,执行以下两个查询。 确保在第二个查询中,您确保不选择数据字段。
> db.fs.files.findOne({filename:'universe.jpg'})
> db.fs.chunks.find({}, {data:0})
- 现在我们已经从操作系统的本地文件系统中将文件放入了 GridFS,我们将看到如何将文件获取到本地文件系统。 从操作系统 shell 中执行以下操作:
$ mongofiles get -l UploadedImage.jpg universe.jpg
- 最后,我们将删除我们上传的文件。 从操作系统 shell 中,执行以下操作:
$ mongofiles delete universe.jpg
- 再次使用以下查询确认删除:
> db.fs.files.findOne({filename:'universe.jpg'})
> db.fs.chunks.find({}, {data:0})
工作原理...
Mongo 分发包带有一个名为 mongofiles 的工具,它允许我们将大容量上传到 Mongo 服务器,该服务器使用 GridFS 规范进行存储。 GridFS 不是一个不同的产品,而是一个标准规范,由不同的 MongoDB 驱动程序遵循,用于存储大于 16 MB 的数据,这是最大文档大小。 它甚至可以用于小于 16 MB 的文件,就像我们在我们的示例中所做的那样,但实际上没有一个很好的理由这样做。 没有什么能阻止我们实现自己的存储这些大文件的方式,但最好遵循标准。 这是因为所有驱动程序都支持它,并且在需要时进行大文件的分割和组装。
我们从 Packt Publishing 网站下载的图像,并使用 mongofiles 上传到 MongoDB。 执行此操作的命令是put,-l选项给出了我们要上传的本地驱动器上的文件的名称。 最后,名称universe.jpg是我们希望它在 GridFS 上存储的文件的名称。
成功执行后,我们应该在控制台上看到以下内容:
connected to: 127.0.0.1
added file: { _id: ObjectId('5310d531d1e91f93635588fe'), filename: "universe.jpg
", chunkSize: 262144, uploadDate: new Date(1393612082137), md5:
d894ec31b8c5add
d0c02060971ea05ca", length: 2711259 }
done!
这给我们一些上传的细节,上传文件的唯一_id,文件的名称,块大小,这是这个大文件被分成的块的大小(默认为 256 KB),上传日期,上传内容的校验和以及上传的总长度。这个校验和可以事先计算,然后在上传后进行比较,以检查上传的内容是否损坏。
在测试数据库的 mongo shell 中执行以下查询:
> db.fs.files.findOne({filename:'universe.jpg'})
我们看到我们在mongofiles的put命令中看到的输出与上面在fs.files集合中查询的文档相同。这是当向 GridFS 添加数据时,所有上传的文件细节都会放在这个集合中。每次上传都会有一个文档。应用程序以后还可以修改此文档,以添加自己的自定义元数据以及在添加数据时添加到我的 Mongo 的标准细节。如果文档是用于图像上传,应用程序可以很好地使用此集合来添加诸如摄影师、图像拍摄地点、拍摄地点以及图像中个人的标签等细节。
文件内容是包含这些数据的内容。让我们执行以下查询:
> db.fs.chunks.find({}, {data:0})
我们故意从所选结果中省略了数据字段。让我们看一下结果文档的结构:
{
_id: <Unique identifier of type ObjectId representing this chunk>,
file_id: <ObjectId of the document in fs.files for the file whose chunk this document represent>,
n:<The chunk identifier starts with 0, this is useful for knowing the order of the chunks>,
data: <BSON binary content for the data uploaded for the file>
}
对于我们上传的文件,我们有 11 个最大为 256 KB 的块。当请求文件时,fs.chunks集合通过来自fs.files集合的_id字段的file_id和字段n(块的序列)进行搜索。当第一次使用 GridFS 上传文件时,为了快速检索使用文件 ID 按块序列号排序的块,这两个字段上创建了唯一索引。
与put类似,get选项用于从 GridFS 检索文件并将其放在本地文件系统上。命令的不同之处在于使用get而不是put,-l仍然用于提供此文件在本地文件系统上保存的名称,最后的命令行参数是 GridFS 中存储的文件的名称。这是fs.files集合中filename字段的值。最后,mongofiles的delete命令简单地从fs.files和fs.chunks集合中删除文件的条目。删除的文件名再次是fs.files集合中filename字段中的值。
使用 GridFS 的一些重要用例是当存在一些用户生成的内容,比如一些静态数据上的大型报告,这些数据不经常更改,而且频繁生成成本很高。与其每次都运行它们,不如运行一次并存储,直到检测到静态数据的更改;在这种情况下,存储的报告将被删除,并在下一次请求数据时重新执行。文件系统可能并不总是可用于应用程序写入文件,这种情况下这是一个很好的替代方案。有些情况下,人们可能对存储的一些中间数据块感兴趣,这种情况下可以访问包含所需数据的数据块。您可以获得一些不错的功能,比如数据的 MD5 内容,它会自动存储并可供应用程序使用。
既然我们已经了解了 GridFS 是什么,让我们看看在哪些情况下使用 GridFS 可能不是一个很好的主意。通过 GridFS 从 MongoDB 访问内容的性能和直接从文件系统访问的性能不会相同。直接文件系统访问将比 GridFS 更快,建议对要开发的系统进行概念验证(POC)以测量性能损失,并查看是否在可接受的范围内;如果是,那么性能上的折衷可能是值得的。此外,如果您的应用服务器前端使用 CDN,您实际上可能不需要在 GridFS 中存储静态数据的大量 IO。由于 GridFS 将数据存储在多个集合中的多个文档中,因此无法以原子方式更新它们。如果我们知道内容小于 16MB,这在许多用户生成的内容中是情况,或者上传了一些小文件,我们可以完全跳过 GridFS,并将内容存储在一个文档中,因为 BSON 支持在文档中存储二进制内容。有关更多详细信息,请参考上一个教程在 Mongo 中存储二进制数据。
我们很少使用 mongofiles 实用程序来从 GridFS 存储、检索和删除数据。虽然偶尔可能会使用它,但我们大多数情况下会从应用程序执行这些操作。在接下来的几个教程中,我们将看到如何连接到 GridFS,使用 Java 和 Python 客户端存储、检索和删除文件。
还有更多...
虽然这与 Mongo 不太相关,但 Openstack 是一个基础设施即服务(IaaS)平台,提供各种计算、存储、网络等服务。其中一个名为Glance的镜像存储服务支持许多持久存储来存储图像。Glance 支持的存储之一是 MongoDB 的 GridFS。您可以在以下网址找到有关如何配置 Glance 使用 GridFS 的更多信息:docs.openstack.org/trunk/config-reference/content/ch_configuring-openstack-image-service.html。
另请参阅
您可以参考以下教程:
-
从 Java 客户端将数据存储到 GridFS
-
从 Python 客户端将数据存储到 GridFS
从 Java 客户端将数据存储到 GridFS
在上一个教程中,我们看到了如何使用 MongoDB 自带的命令行实用程序 mongofiles 来将数据存储到 GridFS,以管理大型数据文件。要了解 GridFS 是什么,以及在幕后用于存储数据的集合,请参考上一个教程在 Mongo 中使用 GridFS 存储大型数据。
在本教程中,我们将看看如何使用 Java 客户端将数据存储到 GridFS。该程序将是 mongofiles 实用程序的一个大大简化版本,只关注如何存储、检索和删除数据,而不是试图提供像 mongofiles 那样的许多选项。
准备工作
有关本教程所需的所有必要设置,请参阅第一章中的教程安装单节点 MongoDB,安装和启动服务器。如果您对 Java 驱动程序有更多详细信息感兴趣,请参考第三章中的教程使用 Java 客户端在 Mongo 中实现聚合和使用 Java 客户端在 Mongo 中执行 MapReduce。打开一个 mongo shell 并连接到监听端口27017的本地 mongod 实例。对于本教程,我们将使用项目mongo-cookbook-gridfs。该项目可在 Packt 网站上提供的源代码包中找到。需要在本地文件系统上提取该文件夹。打开操作系统的终端并转到提取的项目的根目录。这应该是找到文件pom.xml的目录。还要像上一个教程一样,在本地文件系统上保存文件glimpse_of_universe-wide.jpg,该文件可以在 Packt 网站上提供的书籍可下载包中找到。
如何做…
- 我们假设 GridFS 的集合是干净的,没有先前上传的数据。如果数据库中没有重要数据,您可以执行以下操作来清除集合。在删除集合之前,请小心行事。
> use test
> db.fs.chunks.drop()
> db.fs.files.drop()
- 打开操作系统 shell 并执行以下操作:
$ mvn exec:java -Dexec.mainClass=com.packtpub.mongo.cookbook.GridFSTests -Dexec.args="put ~/glimpse_of_universe-wide.jpg universe.jpg"
-
我需要上传的文件放在主目录中。在
put命令之后,您可以选择给出图像文件的文件路径。请记住,如果路径中包含空格,则整个路径需要在单引号内给出。 -
如果前面的命令成功运行,我们应该期望在命令行输出以下内容:
Successfully written to universe.jpg, details are:
Upload Identifier: 5314c05e1c52e2f520201698
Length: 2711259
MD5 hash: d894ec31b8c5addd0c02060971ea05ca
Chunk Side in bytes: 262144
Total Number Of Chunks: 11
- 一旦前面的执行成功,我们可以从控制台输出确认,然后从 mongo shell 执行以下操作:
> db.fs.files.findOne({filename:'universe.jpg'})
> db.fs.chunks.find({}, {data:0})
- 现在,我们将从 GridFS 获取文件到本地文件系统,执行以下操作来执行此操作:
$ mvn exec:java -Dexec.mainClass=com.packtpub.mongo.cookbook.GridFSTests -Dexec.args="get '~/universe.jpg' universe.jpg"
确认文件是否存在于所述位置的本地文件系统上。我们应该看到以下内容打印到控制台输出,以指示成功的写操作:
Connected successfully..
Successfully written 2711259 bytes to ~/universe.jpg
- 最后,我们将从 GridFS 中删除文件:
$ mvn exec:java -Dexec.mainClass=com.packtpub.mongo.cookbook.GridFSTests -Dexec.args="delete universe.jpg"
- 成功删除后,我们应该在控制台中看到以下输出:
Connected successfully..
Removed file with name 'universe.jpg' from GridFS
它是如何工作的...
类com.packtpub.mongo.cookbook.GridFSTests接受三种类型的操作:put将文件上传到 GridFS,get从 GridFS 获取内容到本地文件系统,delete从 GridFS 删除文件。
该类最多接受三个参数,第一个是操作,有效值为get,put和delete。第二个参数与get和put操作相关,是本地文件系统上要写入下载内容的文件的名称,或者用于上传的内容的源。第三个参数是 GridFS 中的文件名,不一定与本地文件系统上的文件名相同。但是,对于delete,只需要 GridFS 上的文件名,该文件将被删除。
让我们看一下该类中与 GridFS 特定的一些重要代码片段。
在您喜欢的 IDE 中打开类com.packtpub.mongo.cookbook.GridFSTests,查找方法handlePut,handleGet和handleDelete。这些方法是所有逻辑的地方。我们将首先从handlePut方法开始,该方法用于将文件内容从本地文件系统上传到 GridFS。
无论我们执行什么操作,我们都将创建com.mongodb.gridfs.GridFS类的实例。在我们的情况下,我们将其实例化如下:
GridFS gfs = new GridFS(client.getDB("test"));
该类的构造函数接受com.mongodb.DB类的数据库实例。创建 GridFS 实例后,我们将调用其上的createFile方法。此方法接受两个参数,第一个是InputStream,用于提供要上传的内容的字节,第二个参数是 GridFS 上的文件名,该文件将保存在 GridFS 上。但是,此方法不会在 GridFS 上创建文件,而是返回com.mongodb.gridfs.GridFSInputFile的实例。只有在调用此返回对象中的save方法时,上传才会发生。此createFile方法有几个重载的变体。有关更多详细信息,请参阅com.mongodb.gridfs.GridFS类的 Javadocs。
我们的下一个方法是handleGet,它从 GridFS 上保存的文件中获取内容到本地文件系统。与com.mongodb.DBCollection类似,com.mongodb.gridfs.GridFS类具有用于搜索的find和findOne方法。但是,与接受任何 DBObject 查询不同,GridFS 中的find和findOne接受文件名或要在fs.files集合中搜索的文档的 ObjectID 值。同样,返回值不是 DBCursor,而是com.mongodb.gridfs.GridFSDBFile的实例。该类具有各种方法,用于获取 GridFS 文件中存在的内容的字节的InputStream,将文件或OutputStream写入文件的方法writeTo,以及一个getLength方法,用于获取文件中的字节数。有关详细信息,请参阅com.mongodb.gridfs.GridFSDBFile类的 Javadocs。
最后,我们来看看handleDelete方法,它用于删除 GridFS 上的文件,是最简单的方法。GridFS 对象上的方法是remove,它接受一个字符串参数:要在服务器上删除的文件的名称。此方法的return类型是void。因此,无论 GridFS 上是否存在内容,如果为此方法提供了一个不存在的文件的名称,该方法都不会返回值,也不会抛出异常。
另请参阅
您可以参考以下配方:
-
在 Mongo 中存储二进制数据
-
从 Python 客户端将数据存储到 GridFS
从 Python 客户端将数据存储到 GridFS
在配方使用 GridFS 在 Mongo 中存储大数据中,我们看到了 GridFS 是什么,以及如何使用它来在 MongoDB 中存储大文件。在上一个配方中,我们看到了如何从 Java 客户端使用 GridFS API。在这个配方中,我们将看到如何使用 Python 程序将图像数据存储到 MongoDB 中的 GridFS。
准备好
有关本配方的所有必要设置,请参考第一章中的配方使用 Java 客户端连接到单个节点,安装和启动服务器。如果您对 Python 驱动程序的更多详细信息感兴趣,请参考以下配方:使用 PyMongo 执行查询和插入操作和使用 PyMongo 执行更新和删除操作在第三章中,编程语言驱动程序。从 Packt 网站的可下载捆绑包中下载并保存图像glimpse_of_universe-wide.jpg到本地文件系统,就像我们在上一个配方中所做的那样。
如何做…
- 通过在操作系统 shell 中输入以下内容来打开 Python 解释器。请注意,当前目录与放置图像文件
glimpse_of_universe-wide.jpg的目录相同:
$ python
- 按如下方式导入所需的包:
>>>import pymongo
>>>import gridfs
- 一旦打开了 Python shell,就按如下方式创建
MongoClient和数据库对象到测试数据库:
>>>client = pymongo.MongoClient('mongodb://localhost:27017')
>>>db = client.test
- 要清除与 GridFS 相关的集合,请执行以下操作:
>>> db.fs.files.drop()
>>> db.fs.chunks.drop()
- 创建 GridFS 的实例如下:
>>>fs = gridfs.GridFS(db)
- 现在,我们将读取文件并将其内容上传到 GridFS。首先,按如下方式创建文件对象:
>>>file = open('glimpse_of_universe-wide.jpg', 'rb')
- 现在按如下方式将文件放入 GridFS
>>>fs.put(file, filename='universe.jpg')
-
成功执行
put后,我们应该看到上传文件的 ObjectID。这将与此文件的fs.files集合的_id字段相同。 -
从 Python shell 执行以下查询。它应该打印出包含上传详细信息的
dict对象。验证内容
>>> db.fs.files.find_one()
- 现在,我们将获取上传的内容并将其写入本地文件系统中的文件。让我们获取表示要从 GridFS 中读取数据的
GridOut实例,如下所示:
>>> gout = fs.get_last_version('universe.jpg')
- 有了这个实例,让我们按如下方式将数据写入本地文件系统中的文件。首先,按如下方式打开本地文件系统上的文件句柄以进行写入:
>>> fout = open('universe.jpg', 'wb')
- 然后,我们将按如下方式向其写入内容:
>>>fout.write(gout.read())
>>>fout.close()
>>>gout.close()
- 现在在本地文件系统的当前目录上验证文件。将创建一个名为
universe.jpg的新文件,其字节数与源文件相同。通过在图像查看器中打开它来进行验证。
工作原理…
让我们看一下我们执行的步骤。在 Python shell 中,我们导入了两个包,pymongo和gridfs,并实例化了pymongo.MongoClient和gridfs.GridFS实例。类gridfs.GridFS的构造函数接受一个参数,即pymongo.Database的实例。
我们使用open函数以二进制模式打开文件,并将文件对象传递给 GridFS 的put方法。还传递了一个名为filename的额外参数,这将是放入 GridFS 的文件的名称。第一个参数不需要是文件对象,而是任何定义了read方法的对象。
一旦put操作成功,return值就是fs.files集合中上传文档的 ObjectID。对fs.files的查询可以确认文件已上传。验证上传的数据大小是否与文件大小匹配。
我们的下一个目标是将文件从 GridFS 获取到本地文件系统。直觉上,人们会想象如果将文件放入 GridFS 的方法是put,那么获取文件的方法将是get。确实,该方法的确是get,但是它只会基于put方法返回的ObjectId进行获取。因此,如果您愿意按ObjectId获取,get就是您的方法。但是,如果您想按文件名获取,要使用的方法是get_last_version。它接受我们上传的文件名,并且此方法的返回类型是gridfs.gridfs_file.GridOut类型。该类包含read方法,它将从 GridFS 中上传的文件中读取所有字节。我们以二进制模式打开一个名为universe.jpg的文件进行写入,并将从GridOut对象中读取的所有字节写入其中。
另请参阅
您可以参考以下配方:
-
在 Mongo 中存储二进制数据
-
从 Java 客户端将数据存储到 GridFS
使用 oplog 在 Mongo 中实现触发器
在关系型数据库中,触发器是在数据库表上执行insert、update或delete操作时被调用的代码。触发器可以在操作之前或之后被调用。MongoDB 中并没有内置实现触发器,如果您需要在应用程序中任何insert/update/delete操作执行时得到通知,您需要自己在应用程序中管理。一种方法是在应用程序中有一种数据访问层,这是唯一可以从集合中查询、插入、更新或删除文档的地方。但是,这也存在一些挑战。首先,您需要在应用程序中明确编写逻辑以满足此要求,这可能是可行的,也可能是不可行的。如果数据库是共享的,并且多个应用程序访问它,事情会变得更加困难。其次,访问需要严格管理,不允许其他来源的插入/更新/删除。
或者,我们需要考虑在靠近数据库的层中运行某种逻辑。跟踪所有写操作的一种方法是使用 oplog。请注意,无法使用 oplog 跟踪读操作。在本配方中,我们将编写一个小型的 Java 应用程序,该应用程序将尾随 oplog 并获取在 Mongo 实例上发生的所有insert、update和delete操作。请注意,此程序是用 Java 实现的,并且在任何其他编程语言中同样有效。关键在于实现的逻辑,实现的平台可以是任何。此外,只有在 mongod 实例作为副本集的一部分启动时,此触发器类功能才能被调用,而不是在数据被插入/更新或从集合中删除之前。
准备工作
有关此示例的所有必要设置,请参考第一章中的示例作为副本集的一部分启动多个实例,安装和启动服务器。如果您对 Java 驱动程序的更多细节感兴趣,请参考第三章中的以下示例使用 Java 客户端执行查询和插入操作和使用 Java 客户端执行更新和删除操作。这两个示例的先决条件是我们这个示例所需要的一切。
如果您不了解或需要复习,请参考本章中的示例在 MongoDB 中创建和跟踪封顶集合游标,了解有关封顶集合和可跟踪游标的更多信息。最后,尽管不是强制性的,第四章中的管理解释了 oplog 的深度,解释了理解和分析 oplog中的 oplog。这个示例不会像我们在第四章中所做的那样深入解释 oplog。打开一个 shell 并将其连接到副本集的主服务器。
对于这个示例,我们将使用项目mongo-cookbook-oplogtrigger。该项目可以从 Packt 网站下载的源代码包中获取。需要在本地文件系统上提取文件夹。打开命令行 shell 并转到提取的项目的根目录。这应该是找到文件pom.xml的目录。还需要TriggerOperations.js文件来触发我们打算捕获的数据库中的操作。
操作步骤…
- 打开操作系统 shell 并执行以下操作:
$ mvn exec:java -Dexec.mainClass=com.packtpub.mongo.cookbook.OplogTrigger -Dexec.args="test.oplogTriggerTest"
- Java 程序启动后,我们将打开 shell,当前目录中存在文件
TriggerOperations.js,mongod 实例监听端口27000作为主服务器:
$ mongo --port 27000 TriggerOperations.js --shell
- 连接到 shell 后,执行我们从 JavaScript 中加载的以下函数:
test:PRIMARY> triggerOperations()
- 观察在控制台上打印出的输出,Java 程序
com.packtpub.mongo.cookbook.OplogTrigger正在使用 Maven 执行。
工作原理…
我们实现的功能对于许多用例非常方便,但首先让我们看一下更高层次上做了什么。Java 程序com.packtpub.mongo.cookbook.OplogTrigger是一个在 MongoDB 中插入、更新或删除集合中的新数据时触发的东西。它使用 oplog 集合,这是 Mongo 中复制的支柱,来实现这个功能。
我们刚刚编写的 JavaScript 作为一个数据的生产、更新和删除的源。您可以选择打开TriggerOperations.js文件,看一下它是如何实现的。它执行的集合位于测试数据库中,称为oplogTriggerTest。
当我们执行 JavaScript 函数时,应该看到类似以下内容打印到输出控制台:
[INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) @ mongo-cookbook-oplogtriger <<<
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ mongo-cookbook-oplogtriger ---
Connected successfully..
Starting tailing oplog...
Operation is Insert ObjectId is 5321c4c2357845b165d42a5f
Operation is Insert ObjectId is 5321c4c2357845b165d42a60
Operation is Insert ObjectId is 5321c4c2357845b165d42a61
Operation is Insert ObjectId is 5321c4c2357845b165d42a62
Operation is Insert ObjectId is 5321c4c2357845b165d42a63
Operation is Insert ObjectId is 5321c4c2357845b165d42a64
Operation is Update ObjectId is 5321c4c2357845b165d42a60
Operation is Delete ObjectId is 5321c4c2357845b165d42a61
Operation is Insert ObjectId is 5321c4c2357845b165d42a65
Operation is Insert ObjectId is 5321c4c2357845b165d42a66
Operation is Insert ObjectId is 5321c4c2357845b165d42a67
Operation is Insert ObjectId is 5321c4c2357845b165d42a68
Operation is Delete ObjectId is 5321c4c2357845b165d42a5f
Operation is Delete ObjectId is 5321c4c2357845b165d42a62
Operation is Delete ObjectId is 5321c4c2357845b165d42a63
Operation is Delete ObjectId is 5321c4c2357845b165d42a64
Operation is Delete ObjectId is 5321c4c2357845b165d42a60
Operation is Delete ObjectId is 5321c4c2357845b165d42a65
Operation is Delete ObjectId is 5321c4c2357845b165d42a66
Operation is Delete ObjectId is 5321c4c2357845b165d42a67
Operation is Delete ObjectId is 5321c4c2357845b165d42a68
Maven 程序将持续运行,永远不会终止,因为 Java 程序不会。您可以按Ctrl + C停止执行。
让我们分析一下 Java 程序,这是内容的核心所在。首先假设这个程序要工作,必须设置一个副本集,因为我们将使用 Mongo 的 oplog 集合。Java 程序创建了一个连接到副本集成员的主服务器,连接到本地数据库,并获取了oplog.rs集合。然后,它所做的就是找到 oplog 中的最后一个或几乎最后一个时间戳。这样做是为了防止在启动时重放整个 oplog,而是标记 oplog 末尾的一个点。以下是找到这个时间戳值的代码:
DBCursor cursor = collection.find().sort(new BasicDBObject("$natural", -1)).limit(1);
int current = (int) (System.currentTimeMillis() / 1000);
return cursor.hasNext() ? (BSONTimestamp)cursor.next().get("ts") : new BSONTimestamp(current, 1);
oplog 按照自然逆序排序,以找到其中最后一个文档中的时间。由于 oplog 遵循先进先出模式,将 oplog 按降序自然顺序排序等同于按时间戳降序排序。
完成后,像以前一样找到时间戳,我们通常查询操作日志集合,但增加了两个额外的选项:
DBCursor cursor = collection.find(QueryBuilder.start("ts")
.greaterThan(lastreadTimestamp).get())
.addOption(Bytes.QUERYOPTION_TAILABLE)
.addOption(Bytes.QUERYOPTION_AWAITDATA);
查询找到所有大于特定时间戳的文档,并添加两个选项,Bytes.QUERYOPTION_TAILABLE和Bytes.QUERYOPTION_AWAITDATA。只有在添加前一个选项时才能添加后一个选项。这不仅查询并返回数据,还在执行到游标末尾时等待一段时间以获取更多数据。最终,当没有数据到达时,它终止。
在每次迭代期间,还要存储上次看到的时间戳。当游标关闭且没有更多数据可用时,我们再次查询以获取新的可追溯游标实例时会使用这个时间戳。这个过程将无限期地继续下去,基本上我们以类似于在 Unix 中使用tail命令追踪文件的方式追踪集合。
操作日志文档包含一个名为op的字段,其值为i,u和d,分别表示插入,更新和删除的操作。字段o包含插入或删除对象的 ID(_id)(在插入和删除的情况下)。在更新的情况下,文件o2包含_id。我们所做的就是简单地检查这些条件,并打印出插入/删除或更新的操作和文档的 ID。
有一些需要注意的事情如下。显然,已删除的文档在集合中将不可用,因此,如果您打算进行查询,_id将不会真正有用。此外,在使用我们获得的 ID 更新后选择文档时要小心,因为操作日志中的某些其他操作可能已经对同一文档执行了更多的更新,而我们应用程序的可追溯游标尚未达到那一点。这在高容量系统中很常见。同样,对于插入,我们也有类似的问题。我们可能使用提供的 ID 查询的文档可能已经被更新/删除。使用此逻辑跟踪这些操作的应用程序必须意识到这些问题。
或者,查看包含更多详细信息的操作日志。比如插入的文档,执行的update语句等。操作日志集合中的更新是幂等的,这意味着它们可以应用任意次数而不会产生意外的副作用。例如,如果实际的更新是将值增加 1,那么操作日志集合中的更新将具有set运算符,并且最终值将被期望。这样,相同的更新可以应用多次。然后,您将使用的逻辑必须更复杂,以实现这样的情况。
此外,这里没有处理故障转移。这对于基于生产的系统是必要的。另一方面,无限循环在第一个游标终止时立即打开一个新的游标。在再次查询操作日志之前,可以引入一个睡眠持续时间,以避免用查询过度压倒服务器。请注意,此处提供的程序不是生产质量的代码,而只是使用了许多其他系统用于获取有关 MongoDB 中集合的新数据插入,删除和更新的通知技术的简单演示。
MongoDB 直到 2.4 版本之前都没有文本搜索功能,之前所有的全文搜索都是使用 Solr 或 Elasticsearch 等外部搜索引擎处理的。即使现在,尽管 MongoDB 中的文本搜索功能已经可以投入生产使用,许多人仍然会使用外部专用的搜索索引器。如果决定使用外部全文索引搜索工具而不是利用 MongoDB 内置的工具,这也不足为奇。在 Elasticsearch 中,将数据流入索引的抽象称为“river”。Elasticsearch 中的 MongoDB river 会在 Mongo 中的集合添加数据时将数据添加到索引中,其构建逻辑与我们在 Java 中实现的简单程序中看到的逻辑相同。
使用地理空间索引在 Mongo 中进行平面 2D 地理空间查询
在这个配方中,我们将看到什么是地理空间查询,然后看看如何在平面上应用这些查询。我们将在一个测试地图应用程序中使用它。
地理空间查询可以在创建了地理空间索引的数据上执行。有两种类型的地理空间索引。第一种称为 2D 索引,是两者中较简单的一种,它假定数据以x,y坐标的形式给出。第二种称为 3D 或球面索引,相对更复杂。在这个配方中,我们将探索 2D 索引,并对 2D 数据执行一些查询。我们将要处理的数据是一个 25 x 25 的网格,其中一些坐标表示公交车站、餐厅、医院和花园。

准备工作
有关此配方的所有必要设置,请参阅第一章中的配方使用 Java 客户端连接单个节点,安装和启动服务器。下载数据文件2dMapLegacyData.json,并将其保存在本地文件系统上以备导入。打开一个连接到本地 MongoDB 实例的 mongo shell。
如何做…
- 从操作系统 shell 执行以下命令将数据导入到集合中。文件
2dMapLegacyData.json位于当前目录中。
$ mongoimport -c areaMap -d test --drop 2dMapLegacyData.json
- 如果我们在屏幕上看到类似以下内容,我们可以确认导入已成功进行:
connected to: 127.0.0.1
Mon Mar 17 23:58:27.880 dropping: test.areaMap
Mon Mar 17 23:58:27.932 check 9 26
Mon Mar 17 23:58:27.934 imported 26 objects
- 成功导入后,从打开的 mongo shell 中,通过执行以下查询验证集合及其内容:
> db.areaMap.find()
这应该让你感受到集合中的数据。
- 下一步是在这些数据上创建 2D 地理空间索引。执行以下命令创建 2D 索引:
$ db.areaMap.ensureIndex({co:'2d'})
- 创建了索引后,我们现在将尝试找到离一个人所站的地方最近的餐厅。假设这个人对美食不挑剔,让我们执行以下查询,假设这个人站在位置(12, 8),如图所示。此外,我们只对最近的三个地方感兴趣。
$ db.areaMap.find({co:{$near:[12, 8]}, type:'R'}).limit(3)
-
这应该给我们三个结果,从最近的餐厅开始,随后的结果按距离递增给出。如果我们看一下之前给出的图像,我们可能会对这里给出的结果表示同意。
-
让我们给查询添加更多选项。个人需要步行,因此希望结果中的距离受到限制。让我们使用以下修改重新编写查询:
$ db.areaMap.find({co:{$near:[12, 8], $maxDistance:4}, type:'R'})
- 观察这次检索到的结果数量。
工作原理…
让我们现在看看我们做了什么。在继续之前,让我们定义一下两点之间的距离是什么意思。假设在笛卡尔平面上我们有两点(x[1], y[1])和(x[2], y[2]),它们之间的距离将使用以下公式计算:
√(x[1] – x[2])² + (y[1] – y[2])²
假设两点分别为(2, 10)和(12, 3),距离将是:√(2 – 12)² + (10 – 3)² = √(-10)² + (7)² = √149 =12.207。
在了解了 MongoDB 在幕后如何进行距离计算的计算方法之后,让我们从第 1 步开始看看我们做了什么。
我们首先将数据正常导入到test数据库中的一个集合areaMap中,并创建了一个索引db.areaMap.ensureIndex({co:'2d'})。索引是在文档中的字段co上创建的,其值是一个特殊值2d,表示这是一种特殊类型的索引,称为 2D 地理空间索引。通常,在其他情况下,我们会给出值1或-1,表示索引的顺序。
有两种类型的索引。第一种是 2D 索引,通常用于跨度较小且不涉及球面的平面。它可能是建筑物的地图,一个地区,甚至是一个小城市,其中地球的曲率覆盖的土地部分并不真正重要。然而,一旦地图的跨度增加并覆盖全球,2D 索引将不准确地预测值,因为需要考虑地球的曲率在计算中。在这种情况下,我们将使用球形索引,我们将很快讨论。
创建 2D 索引后,我们可以使用它来查询集合并找到一些接近查询点的点。执行以下查询:
> db.areaMap.find({co:{$near:[12, 8]}, type:'R'}).limit(3)
它将查询类型为 R 的文档,这些文档的类型是restaurants,并且接近坐标(12,8)。此查询返回的结果将按照与所查询点(在本例中为(12,8))的距离递增的顺序排列。限制只是将结果限制为前三个文档。我们还可以在查询中提供$maxDistance,它将限制距离小于或等于提供的值的结果。我们查询的位置不超过四个单位,如下所示:
> db.areaMap.find({co:{$near:[12, 8], $maxDistance:4}, type:'R'})
Mongo 中的球形索引和 GeoJSON 兼容数据
在继续本食谱之前,我们需要查看之前的食谱使用地理空间索引在 Mongo 中进行平面 2D 地理空间查询,以了解 MongoDB 中的地理空间索引是什么,以及如何使用 2D 索引。到目前为止,我们已经在 MongoDB 集合中以非标准格式导入了 JSON 文档,创建了地理空间索引,并对其进行了查询。这种方法完全有效,实际上,直到 MongoDB 2.4 版本之前,这是唯一可用的选项。MongoDB 2.4 版本支持一种额外的方式来存储、索引和查询集合中的文档。有一种标准的方式来表示地理空间数据,特别是用于 JSON 中的地理数据交换,并且 GeoJSON 的规范在以下链接中详细说明:geojson.org/geojson-spec.html。我们现在可以以这种格式存储数据。
此规范支持各种地理图形类型。但是,对于我们的用例,我们将使用类型Point。首先让我们看看我们之前使用非标准格式导入的文档是什么样子的,以及使用 GeoJSON 格式的文档是什么样子的。
- 非标准格式的文档:
{"_id":1, "name":"White Street", "type":"B", co:[4, 23]}
- GeoJSON 格式的文档:
{"_id":1, "name":"White Street", "type":"B", co:{type: 'Point', coordinates : [4, 23]}}
对于我们的特定情况来说,它看起来比非标准格式更复杂,我同意。然而,当表示多边形和其他线时,非标准格式可能必须存储多个文档。在这种情况下,只需更改type字段的值,就可以将其存储在单个文档中。有关更多详细信息,请参阅规范。
准备工作
这个食谱的先决条件与上一个食谱的先决条件相同,只是要导入的文件将是2dMapGeoJSONData.json和countries.geo.json。从 Packt 网站下载这些文件,并将它们保存在本地文件系统中,以便稍后导入它们。
注意
特别感谢 Johan Sundström 分享世界数据。世界的 GeoJSON 取自github.com/johan/world.geo.json。该文件经过处理,以便在 Mongo 中进行导入和索引创建。2.4 版本不支持 MultiPolygon,因此所有 MultiPolygon 类型的形状都被省略了。然而,这个缺点似乎在 2.6 版本中得到了修复。
如何做…
- 按照以下方式将 GeoJSON 兼容数据导入新集合。这包含了 26 个类似于我们上次导入的文档,只是它们是使用 GeoJSON 格式进行格式化的。
$ mongoimport -c areaMapGeoJSON -d test --drop 2dMapGeoJSONData.json
$ mongoimport -c worldMap -d test --drop countries.geo.json
- 在这些集合上创建一个地理空间索引,如下所示:
> db.areaMapGeoJSON.ensureIndex({"co" : "2dsphere"})
> db.worldMap.ensureIndex({geometry:'2dsphere'})
- 我们现在将首先查询
areaMapGeoJSON集合,如下所示:
> db.areaMapGeoJSON.find(
{ co:{
$near:{
$geometry:{
type:'Point',
coordinates:[12, 8]
}
}
},
type:'R'
}).limit(3)
-
接下来,我们将尝试找到所有落在由点(0, 0)、(0, 11)、(11, 11)和(11, 0)之间的正方形内的餐馆。请参考上一个食谱介绍中给出的图形,以清晰地看到点和预期结果。
-
编写以下查询并观察结果:
> db.areaMapGeoJSON.find(
{ co:{
$geoIntersects:{
$geometry:{
type:'Polygon',
coordinates:[[[0, 0], [0, 11], [11, 11], [11, 0], [0, 0]]]
}
}
},
type:'R'
})
检查它是否包含预期的坐标(2, 6)、(10, 5)和(10, 1)处的三家餐馆。
- 接下来,我们将尝试执行一些操作,找到完全位于另一个封闭多边形内的所有匹配对象。假设我们想找到一些位于给定正方形街区内的公交车站。可以使用
$geoWithin操作符来解决这类用例,实现它的查询如下:
> db.areaMapGeoJSON.find(
{co:{
$geoWithin:{
$geometry:{ type: 'Polygon', coordinates : [[ [3, 9], [3, 24], [6, 24], [6, 9], [3, 9]] ]}
}
},
type:'B'
}
)
-
验证结果;我们应该在结果中有三个公交车站。参考上一个食谱介绍中的地图图像,以获取查询的预期结果。
-
当我们执行上述命令时,它们只是按距离升序打印文档。但是,我们在结果中看不到实际的距离。让我们执行与第 3 点中相同的查询,并额外获取计算出的距离如下:
> db.runCommand({
geoNear: "areaMapGeoJSON",
near: [ 12, 8 ],
spherical: true,
limit:3,
query:{type:'R'}
}
)
-
查询返回一个文档,其中包含一个名为 results 的字段内的数组,其中包含匹配的文档和计算出的距离。结果还包含一些额外的统计信息,包括最大距离,结果中距离的平均值,扫描的总文档数以及以毫秒为单位的所用时间。
-
最后,我们将在世界地图集合上查询,找出提供的坐标位于哪个国家。从 mongo shell 执行以下查询:
> db.worldMap.find(
{geometry:{
$geoWithin:{
$geometry:{
type:'Point',
coordinates:[7, 52]
}
}
}
}
,{properties:1, _id:0}
)
- 我们可以对
worldMap集合执行的所有可能操作都很多,而且并非所有操作都在这个食谱中都能实际覆盖到。我鼓励你尝试使用这个集合并尝试不同的用例。
它是如何工作的...
从 MongoDB 2.4 版本开始,JSON 中存储地理空间数据的标准方式也得到了支持。请注意,我们看到的传统方法也得到了支持。但是,如果你是从头开始的,建议出于以下原因采用这种方法。
-
这是一个标准的,任何了解规范的人都可以轻松理解文档的结构
-
它使存储复杂形状、多边形和多条线变得容易。
-
它还让我们可以使用
$geoIntersect和其他一组新的操作符轻松查询形状的交集
为了使用 GeoJSON 兼容的文档,我们将 JSON 文档导入到areaMapGeoJSON集合中,并按以下方式创建索引:
> db.areaMapGeoJSON.ensureIndex({"co" : "2dsphere"})
集合中的数据与我们在上一个食谱中导入到areaMap集合中的数据类似,但结构不同,与 JSON 格式兼容。这里使用的类型是 2Dsphere 而不是 2D。2Dsphere 类型的索引还考虑了球面表面的计算。请注意,我们正在创建地理空间索引的字段co不是坐标数组,而是一个符合 GeoJSON 的文档本身。
我们查询$near操作符的值不是坐标数组,而是一个带有$geometry键的文档,其值是一个具有坐标的 GeoJSON 兼容文档。无论我们使用的查询是什么,结果都是相同的。参考本食谱中的第 3 点和上一个食谱中的第 5 点,以查看查询中的差异。使用 GeoJSON 的方法看起来更复杂,但它有一些优势,我们很快就会看到。
重要的是要注意,我们不能混合两种方法。尝试在areaMap集合上执行我们刚刚执行的 GeoJSON 格式的查询,尽管我们不会收到任何错误,但结果是不正确的。
我们在本示例的第 5 点中使用了$geoIntersects运算符。这只有在数据库中以 GeoJSON 格式存储文档时才可能。查询简单地找到我们的情况下与我们创建的任何形状相交的所有点。我们使用 GeoJSON 格式创建多边形如下:
{
type:'Polygon',
coordinates:[[[0, 0], [0, 11], [11, 11], [11, 0], [0, 0]]]
}
这些坐标是正方形的,按顺时针方向给出四个角,最后一个坐标与第一个坐标相同,表示它是完整的。执行的查询与$near相同,除了$near运算符被$geoIntersects替换,$geometry字段的值是我们希望在areaMapGeoJSON集合中找到相交点的多边形的 GeoJSON 文档。如果我们看一下得到的结果,并查看介绍部分或上一个示例中的图形,它们确实是我们期望的。
我们还在第 12 点看到了$geoWithin运算符,当我们想要找到点或者在另一个多边形内部时,这是非常方便的。请注意,只有完全在给定多边形内部的形状才会被返回。假设,类似于我们的worldMap集合,我们有一个cities集合,其中的坐标以类似的方式指定。然后,我们可以使用一个国家的多边形来查询在cities集合中位于其中的所有多边形,从而给出城市。显然,一个更简单和更快的方法是在城市文档中存储国家代码。或者,如果城市集合中有一些数据缺失,而且国家不存在,可以使用城市多边形内的任意一点(因为一个城市完全位于一个国家内),并在worldMap集合上执行查询来获取它的国家,这是我们在第 12 点中演示的。
我们之前看到的一些组合可以很好地用于计算两点之间的距离,甚至执行一些几何操作。
一些功能,比如获取存储在集合中的 GeoJSON 多边形图形的质心,甚至是多边形的面积,都不是开箱即用的,应该有一些实用函数来帮助计算这些坐标。这些功能很好,通常是必需的,也许在将来的版本中我们可能会有一些支持;这些操作需要开发人员自己实现。此外,没有直接的方法来查找两个多边形之间是否有重叠,它们的坐标在哪里重叠,重叠的面积等等。我们看到的$geoIntersects运算符告诉我们哪些多边形与给定的多边形、点或线相交。
尽管与 Mongo 无关,但 GeoJSON 格式不支持圆,因此无法使用 GeoJSON 格式在 Mongo 中存储圆。有关地理空间运算符的更多详细信息,请参考以下链接docs.mongodb.org/manual/reference/operator/query-geospatial/。
在 Mongo 中实现全文搜索
我们中的许多人(我可以毫不夸张地说所有人)每天都使用 Google 在网上搜索内容。简单来说:我们在 Google 页面的文本框中提供的文本用于搜索它所索引的网页。搜索结果然后以一定顺序返回给我们,这个顺序是由 Google 的页面排名算法确定的。我们可能希望在我们的数据库中有类似的功能,让我们搜索一些文本内容并给出相应的搜索结果。请注意,这种文本搜索与查找作为句子的一部分的文本不同,后者可以很容易地使用正则表达式来完成。它远远超出了那个范围,可以用来获取包含相同单词、类似发音的单词、具有相似基本单词,甚至是实际句子中的同义词的结果。
自 MongoDB 2.4 版本以来,引入了文本索引,它让我们可以在文档的特定字段上创建文本索引,并在这些单词上启用文本搜索。在这个示例中,我们将导入一些文档,并在它们上创建文本索引,然后查询以检索结果。
准备工作
测试需要一个简单的单节点。参考第一章的安装单节点 MongoDB一节,了解如何启动服务器。但是,不要立即启动服务器。在启动过程中将提供一个额外的标志来启用文本搜索。从 Packt 网站下载文件BlogEntries.json,并将其保存在本地驱动器上以备导入。
操作步骤…
- 启动 MongoDB 服务器监听端口
27017,如下所示。一旦服务器启动,我们将按以下方式在集合中创建测试数据。将文件BlogEntries.json放在当前目录中,我们将使用mongoimport创建userBlog集合,如下所示:
$ mongoimport -d test -c userBlog --drop BlogEntries.json
- 现在,通过在操作系统 shell 中输入以下命令,从 mongo shell 连接到
mongo进程:
$ mongo
- 连接后,按照以下步骤对
userBlog集合中的文档有所了解:
> db.userBlog.findOne()
-
我们感兴趣的字段是
blog_text,这是我们将创建文本搜索索引的字段。 -
按照以下步骤在文档的
blog_text字段上创建文本索引:
> db.userBlog.ensureIndex({'blog_text':'text'})
- 现在,在 mongo shell 中对集合执行以下搜索:
$ db.userBlog.find({$text: {$search : 'plot zoo'}})
查看所得到的结果。
- 执行另一个搜索,如下所示:
$ db.userBlog.find({$text: {$search : 'Zoo -plot'}})
工作原理…
现在让我们看看它是如何工作的。文本搜索是通过一个称为反向索引的过程来完成的。简单来说,这是一个机制,将句子分解为单词,然后这些单词分别指向它们所属的文档。然而,这个过程并不是直接的,所以让我们高层次地逐步看看这个过程中发生了什么:
-
考虑以下输入句子,
I played cricket yesterday。第一步是将这个句子分解为标记,它们变成了[I,played,cricket,yesterday]。 -
接下来,从拆分的句子中删除停用词,我们将得到这些词的子集。停用词是一组非常常见的词,它们被排除在外是因为将它们索引化没有意义,因为它们在搜索查询中使用时可能会影响搜索的准确性。在这种情况下,我们将得到以下单词[
played,cricket,yesterday]。停用词是与语言相关的,对于不同的语言将会有不同的停用词。 -
最后,这些单词被转换为它们的基本词,这种情况下将会是[
play,cricket,yesterday]。词干提取是将一个词减少到其词根的过程。例如,所有的单词play,playing,played, 和plays都有相同的词根词play。有很多算法和框架用于将一个词提取为其词根形式。参考维基百科en.wikipedia.org/wiki/Stemming页面,了解更多关于词干提取和用于此目的的算法的信息。与消除停用词类似,词干提取算法是与语言相关的。这里给出的例子是针对英语的。
如果我们查看索引创建过程,它是如下创建的db.userBlog.ensureIndex({'blog_text':'text'})。JSON 参数中给定的键是要在其上创建文本索引的字段的名称,值将始终是表示要创建的索引是文本索引的文本。创建索引后,在高层次上,前面的三个步骤在每个文档中所创建的索引字段的内容上执行,并创建反向索引。您还可以选择在多个字段上创建文本索引。假设我们有两个字段,blog_text1和blog_text2;我们可以将索引创建为{'blog_text1': 'text', 'blog_text2':'text'}。值{'$**':'text'}在文档的所有字段上创建索引。
最后,我们通过调用以下命令执行了搜索操作:db.userBlog.find({$text: {$search : 'plot zoo'}})。
此命令在集合userBlog上运行文本搜索,使用的搜索字符串是plot zoo。这会按任意顺序在文本中搜索值plot或zoo。如果我们查看结果,我们会看到有两个匹配的文档,并且文档按得分排序。得分告诉我们所搜索的文档的相关性如何,得分越高,相关性越大。在我们的情况下,一个文档中同时包含单词 plot 和 zoo,因此得分比另一个文档高。
要在结果中获取得分,我们需要稍微修改查询,如下所示:
db.userBlog.find({$text:{$search:'plot zoo'}}, {score: { $meta: "textScore"}})
现在我们在find方法中提供了一个额外的文档,询问文本匹配的计算得分。结果仍然没有按得分降序排序。让我们看看如何按得分排序:
db.userBlog.find({$text:{$search:'plot zoo'}}, { score: { $meta: "textScore" }}).sort({score: { $meta: "textScore"}})
正如我们所看到的,查询与以前相同,只是我们添加了额外的sort函数,它将按得分降序对结果进行排序。
当搜索执行为{$text:{$search:'Zoo -plot'}时,它会搜索包含单词zoo但不包含单词plot的所有文档,因此我们只得到一个结果。-符号用于否定,并且将包含该单词的文档排除在搜索结果之外。但是,不要期望通过在搜索中只给出-plot来找到所有不包含单词 plot 的文档。
如果我们查看作为搜索结果返回的内容,它包含了整个匹配的文档。如果我们对整个文档不感兴趣,而只对其中的一些文档感兴趣,我们可以使用投影来获取文档的所需字段。例如,以下查询db.userBlog.find({$text: {$search : 'plot zoo'}},{_id:1})将与在userBlog集合中查找包含单词 zoo 或 plot 的所有文档相同,但结果将包含所得文档的_id字段。
如果多个字段用于创建索引,则文档中的不同字段可能具有不同的权重。例如,假设blog_text1和blog_text2是集合的两个字段。我们可以创建一个索引,其中blog_text1的权重高于blog_text2,如下所示:
db.collection.ensureIndex(
{
blog_text1: "text", blog_text2: "text"
},
{
weights: {
blog_text1: 2,
blog_text2: 1,
},
name: "MyCustomIndexName"
}
)
这使得blog_text1中的内容的权重是blog_text2的两倍。因此,如果一个词在两个文档中被找到,但是在第一个文档的blog_text1字段和第二个文档的blog_text2中出现,那么第一个文档的得分将比第二个文档更高。请注意,我们还使用MyCustomIndexName字段提供了索引的名称。
我们还从语言键中看到,这种情况下的语言是英语。MongoDB 支持各种语言来实现文本搜索。语言在索引内容时很重要,因为它们决定了停用词,并且词干提取也是特定于语言的。
访问链接docs.mongodb.org/manual/reference/command/text/#text-search-languages以获取 Mongo 支持的文本搜索语言的更多详细信息。
那么,在创建索引时如何选择语言呢?默认情况下,如果没有提供任何内容,索引将被创建,假定语言是英语。但是,如果我们知道语言是法语,我们将如下创建索引:
db.userBlog.ensureIndex({'text':'text'}, {'default_language':'french'})
假设我们最初是使用法语创建索引的,getIndexes方法将返回以下文档:
[
{
"v" : 1,
"key" : {
"_id" : 1
},
"ns" : "test.userBlog",
"name" : "_id_"
},
{
"v" : 1,
"key" : {
"_fts" : "text",
"_ftsx" : 1
},
"ns" : "test.userBlog",
"name" : "text_text",
"default_language" : "french",
"weights" : {
"text" : 1
},
"language_override" : "language",
"textIndexVersion" : 1
}
]
但是,如果每个文档的语言不同,这在博客等场景中非常常见,我们有一种方法。如果我们查看上面的文档,language_override字段的值是 language。这意味着我们可以使用此字段在每个文档的基础上存储内容的语言。如果没有,该值将被假定为默认值,在前面的情况下为french。因此,我们可以有以下内容:
{_id:1, language:'english', text: ….} //Language is English
{_id:2, language:'german', text: ….} //Language is German
{_id:3, text: ….} //Language is the default one, French in this case
还有更多...
要在生产中使用 MongoDB 文本搜索,您需要 2.6 或更高版本。还可以将 MongoDB 与 Solr 和 Elasticsearch 等其他系统集成。在下一个配方中,我们将看到如何使用 mongo-connector 将 Mongo 集成到 Elasticsearch 中。
另请参阅
- 有关
$text运算符的更多信息,请访问docs.mongodb.org/manual/reference/operator/query/text/
将 MongoDB 集成到 Elasticsearch 进行全文搜索
MongoDB 已经集成了文本搜索功能,就像我们在上一个配方中看到的那样。但是,有多种原因会导致人们不使用 Mongo 文本搜索功能,而是退回到 Solr 或 Elasticsearch 等传统搜索引擎,以下是其中的一些原因:
-
文本搜索功能在 2.6 版本中已经准备就绪。在 2.4 版本中,它是以测试版引入的,不适用于生产用例。
-
像 Solr 和 Elasticsearch 这样的产品是建立在 Lucene 之上的,它在搜索引擎领域已经证明了自己。Solr 和 Elasticsearch 也是相当稳定的产品。
-
您可能已经对 Solr 和 Elasticsearch 等产品有所了解,并希望将其作为全文搜索引擎,而不是 MongoDB。
-
您可能会发现在 MongoDB 搜索中缺少一些特定功能,而您的应用程序可能需要这些功能,例如 facets。
设置专用搜索引擎确实需要额外的工作来将其与 MongoDB 实例集成。在这个配方中,我们将看到如何将 MongoDB 实例与搜索引擎 Elasticsearch 集成。
我们将使用 mongo-connector 进行集成。这是一个开源项目,可以在github.com/10gen-labs/mongo-connector上找到。
准备工作
有关使用 Python 客户端连接单节点的配方,请参阅第一章中的安装和启动服务器。pip 工具用于获取 mongo-connector。但是,如果您在 Windows 平台上工作,之前没有提到安装 pip 的步骤。访问网址sites.google.com/site/pydatalog/python/pip-for-windows以获取 Windows 版的 pip。
开始单实例所需的先决条件是我们在这个配方中所需要的。然而,为了演示目的,我们将作为一个节点副本集启动服务器。
从 Packt 网站下载文件BlogEntries.json,并将其保存在本地驱动器上,准备导入。
从以下 URL 下载您的目标平台的 elastic search:www.elasticsearch.org/overview/elkdownloads/。提取下载的存档,并从 shell 中转到提取的bin目录。
我们将从 GitHub.com 获取 mongo-connector 源代码并运行它。为此需要 Git 客户端。在您的计算机上下载并安装 Git 客户端。访问 URLgit-scm.com/downloads并按照说明在目标操作系统上安装 Git。如果您不愿意在操作系统上安装 Git,则有另一种选择,可以让您将源代码作为存档下载。
访问以下 URLgithub.com/10gen-labs/mongo-connector。在这里,我们将获得一个选项,让我们将当前源代码作为存档下载,然后我们可以在本地驱动器上提取它。以下图片显示了下载选项位于右下角:

注意
请注意,我们也可以使用 pip 以非常简单的方式安装 mongo-connector,如下所示:
pip install mongo-connector
但是,PyPi 中的版本非常旧,不支持许多功能,因此建议使用存储库中的最新版本。
与之前的配方类似,在那里我们在 Mongo 中看到了文本搜索,我们将使用相同的五个文档来测试我们的简单搜索。下载并保留BlogEntries.json文件。
如何做…
- 在这一点上,假设 Python 和 PyMongo 已安装,并且为您的操作系统平台安装了 pip。我们现在将从源代码获取 mongo-connector。如果您已经安装了 Git 客户端,我们将在操作系统 shell 上执行以下操作。如果您决定将存储库下载为存档,则可以跳过此步骤。转到您想要克隆连接器存储库的目录,并执行以下操作:
$ git clone https://github.com/10gen-labs/mongo-connector.git
$ cd mongo-connector
$ python setup.py install
-
上述设置还将安装将被此应用程序使用的 Elasticsearch 客户端。
-
我们现在将启动单个 mongo 实例,但作为副本集。从操作系统控制台执行以下操作:
$ mongod --dbpath /data/mongo/db --replSet textSearch --smallfiles --oplogSize 50
- 启动 mongo shell 并连接到已启动的实例:
$ mongo
- 从 mongo shell 开始初始化副本集如下:
> rs.initiate()
-
副本集将在几分钟内初始化。与此同时,我们可以继续启动
elasticsearch服务器实例。 -
在提取的
elasticsearch存档的bin目录中执行以下命令:
$ elasticsearch
-
我们不会涉及 Elasticsearch 设置,我们将以默认模式启动它。
-
一旦启动,输入以下 URL 到浏览器
http://localhost:9200/_nodes/process?pretty。 -
如果我们看到以下 JSON 文档,给出了进程详细信息,我们已成功启动了
elasticsearch。
{
"cluster_name" : "elasticsearch",
"nodes" : {
"p0gMLKzsT7CjwoPdrl-unA" : {
"name" : "Zaladane",
"transport_address" : "inet[/192.168.2.3:9300]",
"host" : "Amol-PC",
"ip" : "192.168.2.3",
"version" : "1.0.1",
"build" : "5c03844",
"http_address" : "inet[/192.168.2.3:9200]",
"process" : {
"refresh_interval" : 1000,
"id" : 5628,
"max_file_descriptors" : -1,
"mlockall" : false
}
}
}
}
-
一旦
elasticsearch服务器和 mongo 实例启动并运行,并且安装了必要的 Python 库,我们将启动连接器,该连接器将在启动的 mongo 实例和elasticsearch服务器之间同步数据。出于这个测试的目的,我们将在test数据库中使用user_blog集合。我们希望在文档中实现文本搜索的字段是blog_text。 -
从操作系统 shell 启动 mongo-connector 如下。以下命令是在 mongo-connector 的目录中执行的。
$ python mongo_connector/connector.py -m localhost:27017 -t http://localhost:9200 -n test.user_blog --fields blog_text -d mongo_connector/doc_managers/elastic_doc_manager.py
- 使用
mongoimport实用程序将BlogEntries.json文件导入集合如下。该命令是在当前目录中执行的.json文件。
$ mongoimport -d test -c user_blog BlogEntries.json --drop
-
打开您选择的浏览器,并在其中输入以下 URL:
http://localhost:9200/_search?q=blog_text:facebook。 -
您应该在浏览器中看到类似以下内容的内容:
![如何做…]()
它是如何工作的…
Mongo-connector 基本上是尾随 oplog 以查找它发布到另一个端点的新更新。在我们的情况下,我们使用了 elasticsearch,但也可以是 Solr。您可以选择编写一个自定义的 DocManager,它将插入连接器。有关更多详细信息,请参阅维基github.com/10gen-labs/mongo-connector/wiki,自述文件github.com/10gen-labs/mongo-connector也提供了一些详细信息。
我们给连接器提供了选项-m,-t,-n,--fields和-d,它们的含义如下表所述:
| 选项 | 描述 |
|---|---|
-m |
连接器连接到以获取要同步的数据的 MongoDB 主机的 URL。 |
-t |
要将数据与之同步的系统的目标 URL。在本例中是 elasticsearch。URL 格式将取决于目标系统。如果选择实现自己的 DocManager,则格式将是您的 DocManager 理解的格式。 |
-n |
这是我们希望与外部系统保持同步的命名空间。连接器将在 oplog 中寻找这些命名空间的更改以获取数据。如果要同步多个命名空间,则值将以逗号分隔。 |
--fields |
这些是将发送到外部系统的文档字段。在我们的情况下,索引整个文档并浪费资源是没有意义的。建议只向索引中添加您希望添加文本搜索支持的字段。在结果中还包括标识符_id和源的命名空间,正如我们在前面的屏幕截图中所看到的。然后可以使用_id字段来查询目标集合。 |
-d |
这是要使用的文档管理器,在我们的情况下,我们使用了 elasticsearch 的文档管理器。 |
有关更多支持的选项,请参阅 GitHub 上连接器页面的自述文件。
一旦在 MongoDB 服务器上执行插入操作,连接器就会检测到其感兴趣的集合user_blog中新添加的文档,并开始从新文档中发送要索引的数据到 elasticsearch。为了确认添加,我们在浏览器中执行查询以查看结果。
Elasticsearch 将抱怨索引名称中有大写字符。mongo-connector 没有处理这个问题,因此集合的名称必须是小写。例如,名称userBlog将失败。
还有更多...
我们没有对 elasticsearch 进行任何额外的配置,因为这不是本教程的目标。我们更感兴趣的是集成 MongoDB 和 elasticsearch。您需要参考 elasticsearch 文档以获取更高级的配置选项。如果需要与 elasticsearch 集成,elasticsearch 中还有一个称为 rivers 的概念可以使用。Rivers 是 elasticsearch 从另一个数据源获取数据的方式。对于 MongoDB,可以在github.com/richardwilly98/elasticsearch-river-mongodb/找到 river 的代码。此存储库中的自述文件中有关于如何设置的步骤。
在本章中,我们看到了一个名为在 Mongo 中使用 oplog 实现触发器的教程,介绍了如何使用 Mongo 实现类似触发器的功能。这个连接器和 elasticsearch 的 MongoDB river 依赖于相同的逻辑,以在需要时从 Mongo 中获取数据。
另请参阅
- 您可以在
www.elasticsearch.org/guide/en/elasticsearch/reference/找到更多的 elasticsearch 文档。
第六章:监控和备份
在本章中,我们将查看以下教程:
-
注册 MMS 并设置 MMS 监控代理
-
在 MMS 控制台中管理用户和组
-
在 MMS 中监视实例并设置警报
-
在 MMS 中设置监控警报
-
使用现成的工具备份和恢复 Mongo 中的数据
-
配置 MMS 备份服务
-
在 MMS 备份服务中管理备份
介绍
在生产中,监控和备份是任何关键任务关键软件的重要方面。主动监控让我们在系统中发生异常事件时采取行动,这些事件可能危及数据一致性、可用性或系统性能。如果没有主动监控系统,问题可能在对系统产生重大影响后才会显现出来。我们在第四章中涵盖了与管理相关的教程,这两个活动都是其中的一部分;但是,它们需要一个单独的章节,因为要涵盖的内容很广泛。在本章中,我们将看到如何使用Mongo Monitoring Service(MMS)监控 MongoDB 集群的各种参数并设置警报。我们将研究一些使用现成工具和 MMS 备份服务备份数据的机制。
注册 MMS 并设置 MMS 监控代理
MMS 是一个基于云或本地的服务,可以让您监视 MongoDB 集群。本地版本仅适用于企业订阅。它为管理员提供了一个中心位置,让管理员监视服务器实例的健康状况以及实例所在的服务器。在本教程中,我们将看到软件要求是什么,以及如何为 Mongo 设置 MMS。
准备工作
我们将启动一个mongod的单个实例,用于监视目的。参考第一章中的安装单节点 MongoDB的步骤,启动 MongoDB 实例并从 Mongo shell 连接到它。用于将 mongo 实例的统计信息发送到监控服务的监控代理使用 Python 和 pymongo。参考第一章中的使用 Python 客户端连接到单节点的步骤,了解如何安装 Python 和 pymongo,MongoDB 的 Python 客户端。
操作步骤…
如果您还没有 MMS 帐户,请登录mms.mongodb.com/并注册一个帐户。注册并登录后,您应该看到以下页面:

单击监控下的开始按钮。
-
一旦到达菜单中的下载代理选项,请单击适当的操作系统平台以下载代理。选择适当的操作系统平台后,按照给定的说明进行操作。也记下apiKey。例如,如果选择了 Windows 平台,我们将看到以下内容:
![操作步骤…]()
-
安装完成后,打开
monitoring-agent.config文件。它将位于安装代理时选择的配置文件夹中。 -
在文件中查找关键的
mmsApiKey,并将其值设置为在第 1 步中记录的 API 密钥。 -
一旦服务启动(我们必须在 Windows 上转到
services.msc,可以通过在运行对话框中输入services.msc(Windows + R)并手动启动服务来完成)。服务将被命名为MMS Monitoring Agent。在网页上,点击验证代理按钮。如果一切顺利,启动的代理将被验证,并显示成功消息。 -
下一步是配置主机。这个主机是从代理的角度看到的,在组织或个人基础设施上运行。下面的屏幕显示了用于添加主机的屏幕。主机名是内部主机名(客户网络上的主机名),云上的 MMS 不需要访问 MongoDB 进程。收集这些 mongodb 进程的数据并将数据发送到 MMS 服务的是代理。
![如何操作...]()
一旦添加了主机的详细信息,请单击验证主机按钮。验证完成后,单击开始监视按钮。
我们已经成功设置了 MMS 并向其添加了一个将被监视的主机。
它是如何工作的...
在这个教程中,我们设置了一个 MMS 代理和监视一个独立的 MongoDB 实例。安装和设置过程非常简单。我们还添加了一个独立的实例,一切都很好。
假设我们已经设置并运行了一个副本集(参考第一章中的教程作为副本集的一部分启动多个实例,安装和启动服务器,了解如何启动副本集的更多细节),三个成员正在监听端口27000,27001和27002。参考如何操作...部分中的第 6 点,我们设置了一个独立的主机。在主机类型的下拉菜单中选择副本集,在内部主机名中,给出副本集的任何成员的有效主机名(在我的情况下,给出了Amol-PC和端口27001,这是一个辅助实例);所有其他实例将被自动发现,并在主机下可见,如下所示:

我们没有看到在集群上启用安全性时应该做什么,这在生产环境中非常常见,我们有副本集或分片设置。如果启用了身份验证,我们需要 MMS 代理收集统计信息的正确凭据。在添加新主机时(如何操作...部分的第 6 点),我们给出的DB 用户名和DB 密码应该具有至少clusterAdmin和readAnyDatabase角色。
还有更多...
在这个教程中,我们看到了如何设置 MMS 代理并从 MMS 控制台创建帐户。但是,我们可以作为管理员为 MMS 控制台添加组和用户,授予各种用户在不同组上执行各种操作的权限。在下一个教程中,我们将对 MMS 控制台中的用户和组管理进行一些解释。
在 MMS 控制台中管理用户和组
在上一个教程中,我们看到了如何设置 MMS 帐户并设置 MMS 代理。在这个教程中,我们将对如何设置组和用户访问 MMS 控制台进行一些解释。
准备工作
有关设置代理和 MMS 帐户,请参阅上一个教程。这是本教程的唯一先决条件。
如何操作...
- 首先,转到屏幕左侧的管理 | 用户,如下所示:
![如何操作...]()
在这里,您可以查看现有用户并添加新用户。单击前图中右上角的添加用户(圈起来的)按钮,您应该看到以下弹出窗口,允许您添加新用户:

前面的屏幕将用于添加用户。注意各种可用角色。
-
同样,转到管理 | 我的组,通过单击添加组按钮查看和添加新组。在文本框中,输入组的名称。请记住,您输入的组名应该在全球范围内可用。所给组的名称应在 MMS 的所有用户中是唯一的,而不仅仅是您的帐户。
-
创建新组后,所有组的顶部左侧将显示一个下拉菜单,如下所示:
![如何做…]()
-
您可以使用此下拉菜单在组之间切换,该菜单应显示所选组相关的所有详细信息和统计信息。
注意
请记住,一旦创建了一个组,就无法删除。因此在创建时要小心。
工作原理…
我们在配方中所做的任务非常简单,不需要太多的解释,除了一个问题。何时以及为什么要添加一个组?当我们想要通过不同的环境或应用程序对 MongoDB 实例进行分隔时。每个组都将运行一个不同的 MMS 代理。当我们想要为应用程序的不同环境(开发、QA、生产等)创建单独的监控组时,就需要创建一个新组,并且每个组对用户有不同的权限。也就是说,同一个代理不能用于两个不同的组。在配置 MMS 代理时,我们为其提供一个唯一的 API 密钥。要查看组的 API 密钥,请从屏幕顶部的下拉菜单中选择适当的组(如果您的用户只能访问一个组,则看不到下拉菜单),然后转到管理 | 组设置,如下一个截图所示。组 ID和API 密钥都将显示在页面顶部。

请注意,并非所有用户角色都会看到此选项。例如,只读用户只能个性化其个人资料,大多数其他选项将不可见。
在 MMS 上监视实例并设置警报
前面的几个配方向我们展示了如何设置 MMS 帐户,设置代理,添加主机以及管理用户对 MMS 控制台的访问。MMS 的核心目标是监视主机实例,这一点尚未讨论。在这个配方中,我们将对我们在第一个配方中添加到 MMS 的主机执行一些操作,并从 MMS 控制台监视它。
准备工作
按照配方注册 MMS 并设置 MMS 监控代理,这基本上就是这个配方所需的一切。您可以选择独立实例或副本集,两种方式都可以。此外,打开一个 mongo shell 并从中连接到主实例(它是一个副本集)。
如何做…
- 首先登录 MMS 控制台,然后单击左侧的部署。然后再次单击子菜单中的部署链接,如下截图所示:
![如何做…]()
单击其中一个主机名以查看显示各种统计信息的大量图表。在这个配方中,我们将分析其中大部分。
-
打开为本书下载的捆绑包。在第四章中,管理,我们使用了一个 JavaScript 来使服务器忙于一些操作,名为
KeepServerBusy.js。这次我们将使用相同的脚本。 -
在操作系统 shell 中,使用当前目录中的
.js文件执行以下操作。在我的情况下,shell 连接到主端口27000:
$ mongo KeepServerBusy.js --port 27000 --quiet
- 一旦启动,保持运行并在开始监视 MMS 控制台上的图表之前给予 5 到 10 分钟。
工作原理…
在第四章中,管理,我们看到了一个配方,mongostat 和 mongotop 实用程序,演示了如何使用这些实用程序来获取当前操作和资源利用率。这是一种相当基本和有用的监视特定实例的方法。然而,MMS 为我们提供了一个地方来监视 MongoDB 实例,具有非常易于理解的图表。MMS 还为我们提供了mongostat和mongotop无法提供的历史统计信息。
在我们继续分析指标之前,我想提一下,在 MMS 监控的情况下,数据不会在公共网络上查询或发送。只有统计数据通过代理以安全通道发送。代理的源代码是开源的,如果需要,可以进行检查。mongod 服务器不需要从公共网络访问,因为基于云的 MMS 服务从不直接与服务器实例通信。是 MMS 代理与 MMS 服务通信。通常,一个代理足以监视多个服务器,除非您计划将它们分成不同的组。此外,建议在专用机器/虚拟机上运行代理,并且不与任何 mongod 或 mongos 实例共享,除非它是您正在监视的不太关键的测试实例组。
让我们在控制台上查看一些这些统计数据;我们从与内存相关的统计数据开始。下图显示了驻留内存、映射内存和虚拟内存。

正如我们所看到的,数据集的驻留内存为 82 MB,这是非常低的,它是 mongod 进程实际使用的物理内存。当前值明显低于可用的空闲内存,并且通常会随着时间的推移而增加,直到达到使用了大部分可用物理内存的程度。这由 mongod 服务器进程自动处理,即使机器上有可用内存,我们也不能强制它使用更多内存。
另一方面,映射内存大约是数据库的总大小,并由 MongoDB 进行映射。这个大小可以(通常)比可用的物理内存大得多,这使得 mongod 进程能够在内存中寻址整个数据集,即使它并不在内存中。MongoDB 将映射和加载数据的责任转移到底层操作系统。每当访问一个内存位置并且它在 RAM 中不可用(即驻留内存),操作系统会将页面加载到内存中,如果需要的话,会驱逐一些页面为新页面腾出空间。什么是内存映射文件?让我们尝试用一个超级精简的版本来看看。假设我们有一个 1 KB(1024 字节)的文件,而 RAM 只有 512 字节,显然我们无法将整个文件加载到内存中。但是,您可以要求操作系统将此文件映射到可用的 RAM 页面中。假设每个页面是 128 字节,那么总文件大小为 8 页(128 * 8 = 1024)。但是操作系统只能加载四个页面,我们假设它加载了前 4 个页面(达到 512 字节)。当我们访问第 200 个字节时,它是可以在内存中找到的,因为它在第 2 页上。但是如果我们访问第 800 个字节,逻辑上在第 7 页上,而这一页没有加载到内存中怎么办?操作系统会从内存中取出一页,并加载包含第 800 个字节的第 7 页。作为一个应用程序,MongoDB 给人的印象是所有东西都加载到了内存中,并且通过字节索引进行访问,但实际上并非如此,操作系统在背后为我们做了透明的工作。由于访问的页面不在内存中,我们必须去磁盘加载它到内存中,这就是所谓的页面错误。
回到图表中显示的统计数据,虚拟内存包含所有内存使用,包括映射内存以及任何额外使用的内存,比如与每个连接相关的线程堆栈的内存。如果启用了日志记录,这个大小肯定会比映射内存的两倍还要多,因为日志记录也会有一个单独的内存映射用于数据。因此,我们有两个地址映射相同的内存位置。这并不意味着页面会被加载两次。这只是意味着可以使用两个不同的内存位置来寻址相同的物理内存。非常高的虚拟内存可能需要一些调查。没有预先确定的太高或太低的定义;通常在你对系统的性能感到满意的正常情况下,这些值会被监视。然后应该将这些基准值与系统性能下降时看到的数字进行比较,然后采取适当的行动。
正如我们之前所看到的,当访问的内存位置不在常驻内存中时,会导致页面错误,从而使操作系统从内存中加载页面。这种 IO 活动肯定会导致性能下降,太多的页面错误会严重影响数据库性能。下面的屏幕截图显示了每分钟发生的相当多的页面错误。然而,如果使用的是固态硬盘而不是旋转硬盘,那么来自驱动器的寻道时间的影响可能不会显著。

当物理内存不足以容纳数据集并且操作系统需要将数据从磁盘加载到内存时,通常会发生大量页面错误。请注意,此统计数据显示在 Windows 平台上,并且对于非常琐碎的操作可能会显得很高。这个值是硬页错误和软页错误的总和,实际上并不能真正反映系统的好坏。在基于 Unix 的操作系统上,这些数字会有所不同。在撰写本书时,有一个 JIRA(jira.mongodb.org/browse/SERVER-5799)正在开放,报告了这个问题。
你可能需要记住的一件事是,在生产系统中,MongoDB 与 NUMA 架构不兼容,即使可用内存似乎足够高,你可能会看到大量页面错误发生。有关更多详细信息,请参阅网址docs.mongodb.org/manual/administration/production-notes/。
还有一个额外的图表,提供了一些关于未映射内存的细节。正如我们在本节前面看到的,有三种类型的内存:映射内存、常驻内存和虚拟内存。映射内存始终小于虚拟内存。如果启用了日志记录,虚拟内存将是映射内存的两倍以上。如果我们看一下本节前面给出的图像,我们会发现映射内存为 192MB,而虚拟内存为 532MB。由于启用了日志记录,内存是映射内存的两倍以上。启用日志记录时,相同的数据页在内存中被映射两次。请注意,该页只被物理加载一次,只是可以使用两个不同的地址访问相同的位置。让我们找出虚拟内存(532MB)和两倍映射内存(384MB)之间的差异(2 * 192 = 384)。这些数字之间的差异是 148MB(532-384)。
我们在这里看到的是未映射内存的部分。这个值与我们刚刚计算的值相同。

如前所述,非映射内存的高低值并没有明确定义,但是当值达到 GB 时,我们可能需要进行调查;可能打开的连接数很高,我们需要检查是否有客户端应用程序在使用后没有关闭连接。有一个图表显示了打开的连接数,如下所示:

一旦我们知道连接数,并且发现它与预期计数相比太高,我们将需要找到打开连接到该实例的客户端。我们可以从 shell 中执行以下 JavaScript 代码来获取这些详细信息。不幸的是,在撰写本书时,MMS 没有这个功能来列出客户端连接的详细信息。
testMon:PRIMARY> var currentOps = db.currentOp(true).inprog;
currentOps.forEach(function(c) {
if(c.hasOwnProperty('client')) {
print('Client: ' + c.client + ", connection id is: " + c.desc);
}
//Get other details as needed
});
db.currentOp方法返回结果中所有空闲和系统操作。然后我们遍历所有结果并打印出客户端主机和连接详细信息。currentOp结果中的典型文档如下。您可以选择调整前面的代码,根据需要包含更多详细信息:
{
"opid" : 62052485,
"active" : false,
"op" : "query",
"ns" : "",
"query" : {
"replSetGetStatus" : 1,
"forShell" : 1
},
"client" : "127.0.0.1:64460",
"desc" : "conn3651",
"connectionId" : 3651,
"waitingForLock" : false,
"numYields" : 0,
"lockStats" : {
"timeLockedMicros" : {
},
"timeAcquiringMicros" : {
}
}
}
在第四章中,我们看到了一个名为* mongostat 和 mongotop 实用程序*的配方,用于获取数据库被锁定的时间百分比以及每秒执行的更新、插入、删除和获取操作的数量。您可以参考这些配方并尝试它们。我们使用了与当前用于使服务器繁忙的相同的 JavaScript。
在 MMS 控制台中,我们有图表显示以下详细信息:

第一个opcounters显示在特定时间点执行的操作数量。这应该类似于我们使用mongostat实用程序看到的内容。右侧的内容显示了数据库被锁定的时间百分比。下拉菜单列出了数据库名称。我们可以选择要查看统计信息的适当数据库。同样,这个统计数据可以使用mongostat实用程序来查看。唯一的区别是,使用命令行实用程序,我们可以看到当前时间的统计数据,而在这里我们也可以看到历史统计数据。
在 MongoDB 中,索引存储在 B 树中,下图显示了 B 树索引被访问、命中和未命中的次数。最低限度,RAM 应该足够容纳索引以实现最佳性能。因此,在这个度量标准中,未命中应该为 0 或非常低。未命中的次数过高会导致索引的页面错误,如果查询没有被覆盖,可能会导致相应数据的额外页面错误,也就是说,所有数据无法从索引中获取,这对性能来说是一个双重打击。在查询时的一个好的做法是使用投影,并且只从文档中获取必要的字段。每当我们选择的字段存在于索引中时,这对于查询是有帮助的,这种情况下查询变成了覆盖查询,所有必要的数据只从索引中获取。要了解更多关于覆盖索引的信息,请参考第二章中的创建索引和查看查询计划这个章节。

对于繁忙的应用程序,如果卷非常大,多个写入和读取操作争夺锁定,操作排队。直到 MongoDB 的 2.4 版本,锁定是在数据库级别进行的。因此,即使在另一个集合上进行写入,对该数据库中的任何集合进行读取操作也会被阻塞。这种排队操作会影响系统的性能,并且是数据可能需要分片以扩展系统的良好指标。
提示
请记住,没有定义高或低的值;它是应用程序到应用程序基础上的可接受值。

MongoDB 立即从日志中刷新数据,并定期将数据文件刷新到磁盘。以下指标给出了在给定时间点每分钟的刷新时间。如果刷新占据了相当大的时间百分比,我们可以安全地说写操作正在形成性能瓶颈。

还有更多...
在这篇文章中,我们看到了如何监视 MongoDB 实例/集群。然而,设置警报以在某些阈值值被超过时收到通知,这是我们还没有看到的。在下一篇文章中,我们将看到如何通过一个示例警报来实现这一点,当页面错误超过预定值时会通过电子邮件发送警报。
另请参阅
-
监视硬件,如 CPU 使用率,非常有用,MMS 控制台也支持。然而,需要安装 munin-node 才能启用 CPU 监视。请参考页面
mms.mongodb.com/help/monitoring/configuring/设置 munin-node 和硬件监视。 -
要更新监控代理,请参考页面
mms.mongodb.com/help/monitoring/tutorial/update-mms/。
在 MMS 中设置监控警报
在上一篇文章中,我们看到了如何从 MMS 控制台监视各种指标。这是一个很好的方式,可以在一个地方看到所有的统计数据,并了解 MongoDB 实例和集群的健康状况。然而,不可能持续 24/7 监视系统,对于支持人员来说必须有一些机制在某些阈值超过时自动发送警报。在这篇文章中,我们将设置一个警报,每当页面错误超过 1000 时就会触发。
准备工作
参考上一篇文章,设置使用 MMS 监视 Mongo 实例。这是本篇文章的唯一先决条件。
操作步骤...
-
单击左侧菜单中的活动选项,然后单击警报设置。在警报设置页面上,单击添加警报。
-
为主机添加一个新的警报,如果页面错误超过给定数量,即每分钟 1000 个页面错误。在这种情况下,通知选择为电子邮件,警报发送间隔为 10 分钟。
![操作步骤...]()
-
单击保存以保存警报。
工作原理...
这些步骤非常简单,我们成功地设置了当页面错误超过每分钟 1000 次时的 MMS 警报。正如我们在上一篇文章中看到的,没有固定值被归类为高或低。这是可以接受的,需要在您的环境中的测试阶段对系统进行基准测试。与页面错误类似,还有大量可以设置的警报。一旦触发警报,将按照我们设置的每 10 分钟发送一次,直到不满足发送警报的条件为止。在这种情况下,如果页面错误数量低于 1000 或有人手动确认了警报,那么将不会再发送进一步的警报。
如下面的屏幕截图所示,警报已打开,我们可以确认警报:

单击确认后,将弹出以下窗口,让我们选择确认的持续时间:

这意味着在这种特定情况下,直到所选时间段过去,将不会再发送警报。
单击左侧的活动菜单选项即可查看打开的警报。
另请参阅
- 访问网址
www.mongodb.com/blog/post/five-mms-monitoring-alerts-keep-your-mongodb-deployment-track了解一些应该为您的部署设置的重要警报
使用现成的工具备份和恢复 Mongo 中的数据
在本教程中,我们将使用mongodump和mongorestore等实用程序进行一些基本的备份和恢复操作,以备份和恢复文件。
准备工作
我们将启动一个 mongod 的单个实例。请参阅第一章中的安装单节点 MongoDB教程,安装和启动服务器,以启动一个 mongo 实例并从 mongo shell 连接到它。我们需要一些要备份的数据。如果您的 test 数据库中已经有一些数据,那就很好。如果没有,请使用以下命令从代码包中的countries.geo.json文件创建一些数据:
$ mongoimport -c countries -d test --drop countries.geo.json
如何做…
- 有了
test数据库中的数据,执行以下操作(假设我们要将数据导出到当前目录中名为dump的本地目录):
$ mongodump -o dump -oplog -h localhost -port 27017
验证dump目录中是否有数据。所有文件将是.bson文件,每个文件对应相应数据库文件夹中的一个集合。
- 现在让我们使用以下命令将数据导入 mongo 服务器。这里假设我们在当前目录中有一个名为
dump的目录,并且其中有所需的.bson文件:
mongorestore --drop -h localhost -port 27017 dump -oplogReplay
它是如何工作的…
只需执行几个步骤即可导出和恢复数据。现在让我们看看它到底是做什么的,以及这个实用程序的命令行选项是什么。mongodump实用程序用于将数据库导出到.bson文件中,然后可以稍后用于恢复数据库中的数据。导出实用程序为每个数据库导出一个文件夹,除了本地数据库,然后每个文件夹中将有一个.bson文件。在我们的情况下,我们使用了-oplog选项来导出 oplog 的一部分,数据将导出到oplog.bson文件中。类似地,我们使用mongorestore实用程序将数据导入到数据库中。我们在导入和重放内容之前通过提供--drop选项显式要求删除现有数据,并重放 oplog 中的内容(如果有)。
mongodump实用程序简单地查询集合并将内容导出到文件中。集合越大,恢复内容所需的时间就越长。因此,在进行转储时建议防止写操作。在分片环境中,应关闭平衡器。如果在系统运行时进行转储,则使用-oplog选项导出 oplog 的内容。然后可以使用此 oplog 将数据恢复到特定时间点。以下表格显示了mongodump和mongorestore实用程序的一些重要选项,首先是mongodump:
| 选项 | 描述 |
|---|---|
--help |
显示所有可能的支持选项以及这些选项的简要描述。 |
-h或--host |
要连接的主机。默认情况下,它是端口27017上的 localhost。如果要连接到独立实例,则可以将主机名设置为<主机名>:<端口号>。对于副本集,格式将是<副本集名称>/<主机名>:<端口>,….<主机名>:<端口>,其中逗号分隔的主机名和端口列表称为种子列表。它可以包含副本集中所有或部分主机名。 |
--port |
目标 MongoDB 实例的端口号。如果在之前的-h或--host选项中提供了端口号,则这并不重要。 |
-u或--username |
提供要导出数据的用户的用户名。由于数据是从所有数据库中读取的,因此至少期望用户在所有数据库中具有读取权限。 |
-p或--password |
与用户名一起使用的密码。 |
--authenticationDatabase |
存储用户凭据的数据库。如果未指定,则使用--db选项中指定的数据库。 |
-d或--db |
要备份的数据库。如果未指定,则导出所有数据库。 |
-c或--collection |
要导出的数据库中的集合。 |
-o或--out |
要导出文件的目录。默认情况下,实用程序将在当前目录中创建一个 dump 文件夹,并将内容导出到该目录。 |
--dbpath |
如果我们不打算连接到数据库服务器,而是直接从数据库文件中读取。值是数据库文件所在目录的路径。在直接从数据库文件中读取时,服务器不应该处于运行状态,因为导出会锁定数据文件,如果服务器正在运行,这是不可能的。在获取锁时,将在目录中创建一个锁文件。 |
--oplog |
启用此选项后,还会导出自导出过程开始时的 oplog 数据。如果不启用此选项,导出的数据将不会代表一个时间点,如果写操作同时进行,因为导出过程可能需要几个小时,它只是对所有集合进行查询操作。导出 oplog 可以恢复到某个时间点的数据。如果在导出过程中阻止写操作,则无需指定此选项。 |
同样,对于mongorestore实用程序,以下是选项的含义:--help,-h或--host,--port,-u或--username,-p或--password,--authenticationDatabase,-d或--db,-c或--collection。
| 选项 | 描述 |
|---|---|
--dbpath |
如果我们不打算连接到数据库服务器,而是直接写入数据库文件,请使用此选项。值是数据库文件所在目录的路径。在直接写入数据库文件时,服务器不应该处于运行状态,因为恢复操作会锁定数据文件,如果服务器正在运行,这是不可能的。在获取锁时,将在目录中创建一个锁文件。 |
--drop |
在从导出的转储数据中恢复数据之前删除集合中的现有数据。 |
--oplogReplay |
如果在允许对数据库进行写操作的情况下导出了数据,并且在导出过程中启用了--oplog选项,则将在数据上重放导出的 oplog,以使数据库中的所有数据达到相同的时间点。 |
--oplogLimit |
此参数的值是表示时间的秒数。此选项与oplogReplay命令行选项一起使用,用于告诉恢复实用程序重放 oplog,并在此选项指定的限制处停止。 |
你可能会想,“为什么不复制文件并备份呢?”这样做效果很好,但与此相关的问题有几个。首先,除非禁用写操作,否则无法获得点时间备份。其次,备份所使用的空间非常大,因为复制还会复制数据库的 0 填充文件,而mongodump只会导出数据。
话虽如此,文件系统快照是备份的常用做法。需要记住的一件事是,在进行快照时,日志文件和数据文件需要在同一个快照中以保持一致性。
如果您使用亚马逊网络服务(AWS),强烈建议您将数据库备份上传到 AWS S3。您可能知道,AWS 提供极高的数据冗余性,存储成本非常低。
从 Packt Publishing 网站下载脚本generic_mongodb_backup.sh,并使用它来自动创建备份并上传到 AWS S3。
配置 MMS 备份服务
MMS 备份是 MongoDB 的一个相对较新的功能,用于实时增量备份 MongoDB 实例、副本集和分片,并为您提供实例的时点恢复。该服务可用作本地部署(在您的数据中心)或云端。但是,我们将演示云端服务,这是 Community 和 Basic 订阅的唯一选项。有关可用选项的更多详细信息,您可以访问 MongoDB 在www.mongodb.com/products/subscriptions上提供的不同产品。
准备就绪
Mongo MMS 备份服务仅适用于 Mongo 2.0 及以上版本。我们将启动一个我们将备份的单个服务器。MMS 备份依赖于 oplog 进行连续备份,由于 oplog 仅在副本集中可用,因此服务器需要作为副本集启动。有关如何安装 Python 和 Mongo 的 Python 客户端 PyMongo 的更多信息,请参阅第一章中的使用 Python 客户端连接到单个节点、安装和启动服务器。
操作步骤如下:
如果您还没有 MMS 帐户,请登录mms.mongodb.com/并注册一个帐户。有关屏幕截图,请参阅本章中的注册 MMS 并设置 MMS 监控代理。
- 启动 Mongo 的单个实例,并替换您的机器上适当文件系统路径的值:
$ mongod --replSet testBackup --smallfiles --oplogSize 50 --dbpath /data/mongo/db
请注意,smallfiles和oplogSize仅用于测试目的,并且不应在生产中使用。
- 启动一个 shell,连接到第 1 步中的实例,并按以下方式启动副本集:
> rs.initiate()
副本集将在一段时间内启动并运行。
-
返回到
mms.mongodb.com的浏览器。点击+添加主机按钮添加新主机。将类型设置为副本集,主机名设置为您的主机名,端口设置为默认端口27017。有关添加主机过程的屏幕截图,请参阅注册 MMS 并设置 MMS 监控代理。 -
一旦成功添加主机,请点击左侧的备份选项,然后点击开始设置注册 MMS 备份。
-
可以使用短信或 Google Authenticator 进行注册。如果智能手机上有 Android、iOS 或 Blackberry OS,Google Authenticator 是一个不错的选择。对于印度等国家,Google Authenticator 是唯一可用的选项。
-
假设 Google Authenticator 尚未配置,并且我们计划使用它,我们需要在智能手机上安装该应用。转到您的移动操作系统平台的相应应用商店并安装 Google Authenticator 软件。
-
安装了手机软件后,返回浏览器。在选择 Google Authenticator 后,您应该看到以下屏幕:
![操作步骤]()
-
通过扫描 Google Authenticator 应用程序中的 QR 码开始设置新帐户。如果条形码扫描有问题,您可以选择在屏幕右侧手动输入给定的密钥。
-
一旦成功扫描或输入密钥,您的智能手机应该显示一个每 30 秒更改一次的 6 位数字。在屏幕上的认证代码框中输入该数字。
注意
重要的是不要在手机上删除 Google Authenticator 中的此帐户,因为这将在将来我们希望更改与备份相关的任何设置时使用。一旦设置完成,QR 码和密钥将不再可见。您将需要联系 MongoDB 支持以重置配置。
-
一旦认证完成,您应该看到的下一个屏幕是账单地址和账单详细信息,比如您注册的卡。所有低于 5 美元的费用都将免除,因此在收费之前,您应该可以尝试一个小的测试实例。
-
一旦信用卡详细信息保存,我们将继续进行设置。我们将安装一个备份代理。这是一个与监控代理分开的代理。选择适当的平台,并按照其安装说明进行操作。记下代理的配置文件将放置的位置。
-
一个新的弹出窗口将包含平台的存档/安装程序的指令/链接以及安装步骤。它还应该包含
apiKey。记下 API 密钥;我们将在下一步中需要它。 -
安装完成后,打开代理安装的
config目录中的local.config文件(在代理安装期间显示/修改的位置),并粘贴/输入在上一步中记下的apiKey。 -
一旦代理配置并启动,点击验证代理按钮。
-
一旦代理成功验证,我们将开始添加一个要备份的主机。下拉菜单应该显示我们添加的所有副本集和分片。选择适当的副本集,并将同步源设置为主实例,因为这是我们独立实例中唯一的实例。同步源仅用于初始同步过程。每当我们有一个合适的副本集和多个实例时,我更喜欢使用次要作为同步过程实例。
![如何做…]()
由于实例未启动安全性,将DB 用户名和DB 密码字段留空。
-
如果您希望跳过特定数据库或集合的备份,请单击管理排除的命名空间按钮。如果没有提供任何内容,默认情况下将备份所有内容。集合名称的格式将是
<数据库名称>.<集合名称>。或者,它可以只是数据库名称,在这种情况下,该数据库中的所有集合都不符合备份条件。 -
一旦细节都没问题,点击开始按钮。这应该完成在 MMS 上为副本集设置备份过程的设置。
提示
我执行的安装步骤是在 Windows 操作系统上,服务在这种情况下需要手动启动。按下 Windows + R,输入services.msc。服务的名称是 MMS 备份代理。
工作原理…
这些步骤非常简单,这就是我们为 Mongo MMS 备份设置服务器所需做的一切。之前提到的一个重要事项是,一旦设置了备份,MMS 备份在任何操作中都使用多因素身份验证,并且为 MongoDB 在 Google Authenticator 中设置的帐户不应删除。没有办法恢复用于设置验证器的原始密钥。您将不得不清除 Google Authenticator 设置并设置一个新密钥。要做到这一点,点击屏幕左下角的帮助和支持链接,然后点击如何重置我的双因素身份验证?。
单击链接后,将打开一个新窗口并要求输入用户名。将向注册的电子邮件 ID 发送一封电子邮件,该电子邮件允许您重置双因素身份验证。

如前所述,oplog 用于将当前 MongoDB 实例与 MMS 服务同步。但是,对于初始同步,将使用实例的数据文件。在设置副本集的备份时,我们提供要使用的实例。由于这是一个资源密集型操作,在繁忙的系统上,我更喜欢使用次要实例,以便不会通过 MMS 备份代理向主实例添加更多的查询。一旦实例完成初始同步,主实例的 oplog 将被用于持续获取数据。代理会定期向 admin 数据库中的mms.backup集合写入数据。
MMS 备份的备份代理与 MMS 监控代理不同。虽然在同一台机器上同时运行它们没有限制,但在生产环境中进行这样的设置之前,您可能需要评估一下。最保险的做法是让它们在不同的机器上运行。在生产环境中,不要在同一台机器上运行这两个代理与 mongod 或 mongos 实例。不建议在同一台机器上运行代理和 mongod 实例的原因有几个重要的原因:
-
代理的资源利用率取决于其监视的集群大小。我们不希望代理使用大量资源影响生产实例的性能。
-
代理可能同时监视许多服务器实例。由于只有一个代理实例,我们不希望在数据库服务器维护和重启期间出现故障。
使用 SSL 构建的 MongoDB 社区版或使用 SSL 选项进行通信的企业版必须执行一些额外的步骤。第一步是在为备份设置副本集时检查我的部署支持 MongoDB 连接的 SSL标志(见第 15 步)。请注意截图底部的复选框应该被选中。其次,打开 MMS 配置的local.config文件,并查找以下两个属性:
sslTrustedServerCertificates=
sslRequireValidServerCertificates=true
第一个是 PEM 格式的认证机构证书的完全限定路径。此证书将用于验证通过 SSL 运行的 mongod 实例呈现的证书。如果要禁用证书验证,则可以将第二个属性设置为false,但这并不是一个推荐的选项。就从代理向 MMS 服务发送的数据而言,无论您的 MongoDB 实例是否启用 SSL,通过 SSL 发送的数据都是安全的。备份数据中心中的数据在静态状态下是未加密的。
如果在 mongod 实例上启用了安全性,则需要提供用户名和密码,这将被 MMS 备份代理使用。在为副本集设置备份时提供用户名和密码,如第 15 步所示。由于代理需要读取 oplog,可能需要对所有数据库进行初始同步并将数据写入admin数据库,因此用户需要具有以下角色:readAnyDatabase,clusterAdmin,admin和local数据库上的readWrite,以及userAdminAnyDatabase。这适用于版本 2.4 及以上。在 v2.4 之前的版本中,我们期望用户对所有数据库具有读取权限,并对 admin 和 local 数据库具有读/写权限。
在为备份设置副本集时,您可能会遇到错误,如Insufficient oplog size: The oplog window must be at least 1 hours over the last 24 hours for all active replica set members. Please increase the oplog.。虽然您可能认为这总是与 oplog 大小有关,但当副本集中有一个处于恢复状态的实例时,也会出现这种情况。这可能会让人感到误导,因此在为副本集设置备份时,请注意查看是否有正在恢复的节点。根据 MMS 支持,似乎不允许为具有一些正在恢复节点的备份设置副本集,并且这可能会在将来得到修复。
在 MMS 备份服务中管理备份
在上一篇文章中,我们看到了如何设置 MMS 备份服务,并为备份设置了一个简单的单成员副本集。尽管单成员副本集根本没有意义,但由于独立实例无法在 MMS 中设置备份,因此需要它。在本篇文章中,我们将深入探讨在为备份设置的服务器上可以执行的操作,例如启动、停止或终止备份;管理排除列表;管理备份快照和保留;以及恢复到特定时间点的数据。
准备就绪
前面的步骤就是这个步骤所需的一切。预计已经完成了必要的设置,因为我们将在这个步骤中使用与备份相同的服务器。
操作步骤...
服务器已经运行,让我们向其导入一些数据。可以是任何数据,但我们选择使用上一章中使用的countries.geo.json文件。它应该在从 Packt 网站下载的捆绑软件中可用。
首先将数据导入到test数据库中名为countries的集合中。使用以下命令来执行。当前目录中有countries.geo.json文件时,执行以下导入命令:
$ mongoimport -c countries -d test --drop countries.geo.json
我们已经看到了在设置副本集备份时如何排除命名空间。现在我们将看到在为副本集备份完成后如何排除命名空间。点击左侧的备份菜单选项,然后点击副本集状态,这在点击备份时会默认打开。点击显示副本集的行右侧的齿轮按钮。它应该看起来像这样:

-
如前面的图片所示,点击编辑排除的命名空间,然后输入要排除的集合名称。假设我们要在
test数据库中排除applicationLogs集合,输入test.applicationLogs。 -
保存后,您将被要求输入当前在您的 Google Authenticator 上显示的令牌代码。
-
成功验证代码后,
test.applicationLogs命名空间将被添加到排除备份的命名空间列表中。 -
现在我们将看到如何管理快照调度。快照是数据库在特定时间点的状态。要管理快照频率和保留策略,请点击前一个截图中显示的齿轮按钮,然后点击编辑快照调度。
-
正如我们在下一张图片中所看到的,我们可以设置快照的拍摄时间和保留期限。我们将在下一节中更多讨论这个问题。对此的任何更改都需要多因素身份验证来保存更改。
![操作步骤...]()
-
现在我们将看到如何使用 MMS 备份恢复数据。在任何时候,当我们想要恢复数据时,点击备份和副本集状态/分片集群状态,然后点击集合/集群名称。
点击后,我们将看到保存在此集合中的快照。应该看起来像这样:

我们已经圈出了屏幕上的一些部分,我们将逐一看到。
-
要恢复到快照拍摄时的时间点,请点击网格的操作列中的恢复此快照链接。
![操作步骤...]()
-
前面的图片向我们展示了如何通过 HTTPS 或 SCP 导出数据。我们现在选择 HTTPS,然后点击验证。我们将在下一节中了解 SCP。
-
输入通过短信接收或在 Google Authenticator 上看到的令牌,然后点击完成请求以输入认证代码。
-
成功验证后,点击恢复作业。这是一次性下载,让您可以下载
tar.gz存档。点击下载链接以下载tar.gz存档。![操作步骤...]()
-
下载存档后,解压以获取其中的数据库文件。
-
停止 mongod 实例,用提取出的文件替换数据库文件,并重新启动服务器以获取快照拍摄时的数据。请注意,如果所有数据都被排除在备份之外,数据库文件将不包含该集合的数据。
-
现在我们将看到如何使用 MMS 备份获取特定时间点的数据。
-
点击副本集状态/分片集群状态,然后点击要恢复的集群/集合。
-
在屏幕右侧,点击恢复按钮。
-
这应该列出可用的快照,或者您可以输入自定义时间。勾选使用自定义时间点。单击日期字段,选择日期和时间,以便在小时和分钟中恢复数据,然后单击下一步。请注意,时间点功能只能恢复到过去 24 小时的时间点。
在这里,您将被要求指定格式为 HTTPS 或 SCP。后续步骤与我们上次做的类似,从第 14 步开始。
它是如何工作的...
设置副本集的备份后,我们向数据库导入了随机数据。这个数据库的备份将由 MMS 完成,稍后我们将使用这个备份来恢复数据库。我们看到了如何在步骤 2-5 中排除要备份的命名空间。
查看快照和保留策略设置,我们可以看到我们可以选择快照拍摄的时间间隔和保留的天数(步骤 9)。我们可以看到,默认情况下,快照每 6 小时拍摄一次,保存 2 天。在一天结束时拍摄的快照保存一周。在一周和一个月结束时拍摄的快照分别保存 4 周和 13 个月。快照可以每 6、8、12 和 24 小时拍摄一次。然而,您需要了解长时间间隔后拍摄快照的另一面。假设最后一张快照是在 18 小时拍摄的;获取那时的数据进行恢复非常容易,因为它存储在 MMS 备份服务器上。然而,我们需要 21:30 时的数据进行恢复。由于 MMS 备份支持时间点备份,它将使用 18:00 时的基本快照,然后在 21:30 时拍摄快照后对其进行更改。这类似于如何在数据上重放 oplog。这种重放是有成本的,因此获取时间点备份比从快照获取数据略微昂贵。在这里,我们需要重放 3.5 小时的数据,从 18:00 时到 21:30 时。想象一下,如果快照设置为每 12 小时拍摄一次,我们的第一张快照是在 00:00 时拍摄的,那么我们每天都会在 00:00 时和 12:00 时拍摄快照。要将数据恢复到 21:30 时,以 12:00 时为最后一个快照,我们将不得不重放 9.5 小时的数据。这要昂贵得多。
更频繁的快照意味着更多的存储空间使用,但恢复数据库到特定时间点所需的时间更少。与此同时,较少频繁的快照需要更少的存储空间,但以恢复数据到特定时间点为代价的时间更长。您需要在这两者之间做出决定并进行权衡,即空间和恢复时间。对于每日快照,我们可以选择保留 3 到 180 天。同样,对于每周和每月的快照,保留期可以分别选择 1 到 52 周和 1 到 36 个月。
在第 9 步的屏幕截图中,有一个列显示快照的到期时间。对于第一张拍摄的快照,到期时间是 1 年,而其他快照在 2 天后到期。到期时间如我们在上一段讨论的那样。更改到期值时,旧的快照不会受到影响或根据更改的时间进行调整。然而,根据修改后的保留和频率设置拍摄的新快照将会受到影响。
我们看到了如何下载转储(从第 10 步开始),然后使用它来恢复数据库中的数据。这非常简单,不需要太多解释,除了一些事情。首先,如果数据是用于分片,将会有多个文件夹,每个分片一个文件夹,每个文件夹都有数据库文件,与我们在副本集的情况下看到的不同,那里我们有一个包含数据库文件的单个文件夹。最后,让我们看看当我们选择 SCP 作为选项时的屏幕:

SCP 是安全复制的缩写。文件将通过安全通道复制到计算机的文件系统中。给定的主机需要具有公共 IP,该 IP 将用于 SCP。当我们希望从 MMS 获取的数据传递到云上运行 Unix OS 的机器时,这是非常有意义的,例如,AWS 虚拟实例之一。与其在本地机器上使用 HTTPS 获取文件,然后重新上传到云上的服务器,不如在目标目录块中指定需要复制数据的位置,主机名和凭据。还有几种身份验证的方式。密码是一种简单的方式,还有一个额外的选项是 SSH 密钥对。如果您必须配置云上主机的防火墙以允许通过 SSH 端口的传入流量,公共 IP 地址将显示在屏幕底部(在我们的截图中为64.70.114.115/32或4.71.186.0/24)。您应该将它们列入白名单,以允许通过端口22进行安全复制请求的传入流量。
另请参阅
我们已经看到了使用 MMS 运行备份,该备份使用 oplogs 来实现这一目的。在第五章 高级操作中有一个名为在 Mongo 中使用 oplog 实现触发器的配方,该配方使用 oplog 来实现类似触发器的功能。这个概念是 MMS 备份服务使用的实时备份的基础。
第七章:在云上部署 MongoDB
在本章中,我们将涵盖以下配方:
-
设置和管理 MongoLab 账户
-
在 MongoLab 上设置沙箱 MongoDB 实例
-
从 MongoLab GUI 上操作 MongoDB
-
在 Amazon EC2 上设置 MongoDB 而不使用 AMI
-
使用 Docker 容器设置 MongoDB
介绍
虽然解释云计算不在本书的范围内,但我将在一段话中解释一下。任何规模的企业都需要硬件基础设施,并在其上安装不同的软件。操作系统是基本软件,还需要不同的服务器(从软件角度)用于存储、邮件、网络、数据库、DNS 等。所需的软件框架/平台列表将变得很长。这里的重点是,这些硬件和软件平台的初始预算很高,所以我们甚至没有考虑托管它所需的房地产。这就是亚马逊、Rackspace、Google 和微软等云计算提供商的作用所在。他们在全球不同的数据中心托管了高端硬件和软件,并让我们从不同的配置中选择开始一个实例。然后通过公共网络远程访问以进行管理。我们所有的设置都是在云提供商的数据中心中完成的,我们只是按需付费。关闭实例,停止付费。不仅是小型初创企业,大型企业也经常暂时转向云服务器以满足临时的计算资源需求。提供商提供的价格也非常有竞争力,特别是 AWS,其受欢迎程度说明了一切。
维基页面en.wikipedia.org/wiki/Cloud_computing有很多细节,对于新概念的人来说可能有点太多,但仍然是一篇不错的阅读。computer.howstuffworks.com/cloud-computing/cloud-computing.htm上的文章也很不错,如果你对云计算的概念不熟悉,也建议你阅读一下。
在本章中,我们将使用 MongoDB 服务提供商在云上设置 MongoDB 实例,然后在亚马逊网络服务(AWS)上自己设置。
设置和管理 MongoLab 账户
在这个配方中,我们将评估 MongoLab 这样的供应商,他们提供 MongoDB 作为一项服务。这个介绍性的配方将向你介绍 MongoDB 作为一项服务是什么,然后演示如何在 MongoLab(mongolab.com/)中设置和管理一个账户。
到目前为止,本书中的所有配方都涵盖了在组织/个人场所设置、管理、监控和开发 MongoDB 实例。这不仅需要具有适当技能的人手来管理部署,还需要适当的硬件来安装和运行 Mongo 服务器。这需要大量的前期投资,这对于初创企业甚至对于尚不确定是否要采用或迁移到这项技术的组织来说可能不是一个可行的解决方案。他们可能希望评估一下,看看情况如何,然后再全面转向这个解决方案。理想的情况是有一个服务提供商来负责托管 MongoDB 部署、管理和监控部署,并提供支持。选择这些服务的组织无需事先投资来设置服务器或招聘或外包顾问来管理和监控实例。你需要做的就是选择硬件和软件平台和配置以及适当的 MongoDB 版本,然后从用户友好的 GUI 中设置环境。它甚至给了你一个选项来使用你现有的云提供商的服务器。
我们简要地看到了这些供应商托管服务的作用以及它们为什么是必要的;我们将通过在 MongoLab 上设置帐户并查看一些基本用户和帐户管理来开始这个配方。MongoLab 绝不是 MongoDB 的唯一托管提供商。您还可以查看www.mongohq.com/和www.objectrocket.com/。在撰写本书时,MongoDB 自己开始在 Azure 云上提供 MongoDB 作为服务,目前处于测试阶段。
如何操作…
-
如果您尚未创建帐户,请访问
mongolab.com/signup/进行注册;只需填写相关详细信息并创建一个帐户。 -
创建帐户后,单击右上角的“帐户”链接:
![如何操作…]()
-
在顶部单击“帐户用户”选项卡;它应该是默认选中的:
![如何操作…]()
-
要添加新帐户,请单击“+添加帐户用户”按钮。一个弹出窗口将要求输入用户名、电子邮件 ID 和密码。输入相关详细信息,然后单击“添加”按钮。
-
单击用户,您应该被导航到一个页面,您可以在该页面更改用户名、电子邮件 ID 和密码。您可以通过在此屏幕上单击“更改为管理员”按钮将管理权限转移给用户。
-
同样,通过单击自己的用户详细信息,您可以选择更改用户名、电子邮件 ID 和密码。
-
单击“设置双因素身份验证”按钮以激活使用 Google Authenticator 的多因素身份验证。您需要在 Android、iOS 或 BlackBerry 手机上安装 Google Authenticator 才能继续设置多因素身份验证。
-
单击按钮后,我们应该看到可以使用 Google Authenticator 扫描的 QR 码,或者如果无法扫描,可以单击 QR 码下面的 URL,这将显示代码。在 Google Authenticator 中手动设置基于时间的帐户。Google Authenticator 有两种类型的帐户,基于时间和基于计数器。
提示
请参阅en.wikipedia.org/wiki/Google_Authenticator获取更多详细信息。
- 同样,您可以通过单击“帐户用户”中用户行旁边的叉号来从帐户页面中删除用户。
工作原理…
在这一部分没有太多需要解释的。设置过程和用户管理非常简单。请注意,我们在这里添加的用户不是数据库用户。这些是可以访问 MongoLab 帐户的用户。帐户可以是组织的名称,并且可以在屏幕顶部看到。在手持设备上 Google Authenticator 软件中设置的多因素身份验证帐户不应被删除,因为每当用户从浏览器登录到 MongoLab 帐户时,他将被要求输入 Google Authenticator 帐户以继续。
在 MongoLab 上设置沙箱 MongoDB 实例
在上一篇文章中,我们看到了如何在 MongoLab 上设置帐户并向帐户添加用户。我们还没有看到如何在云上启动实例并使用它执行一些简单的操作。在这个配方中,这正是我们要做的事情。
准备工作
请参考前一篇文章,“设置和管理 MongoLab 帐户”,以在 MongoLab 上设置帐户。我们将设置一个免费的沙箱实例。我们将需要一种连接到这个已启动的mongo实例的方法,因此将需要一个仅随完整的 mongo 安装一起提供的 mongo shell,或者您可以选择使用您选择的编程语言来连接到已启动的mongo实例以执行操作。请参阅第三章,“编程语言驱动程序”中有关使用 Java 或 Python 客户端连接和执行操作的配方。
如何操作…
-
转到主页,
mongolab.com/home,然后点击创建新按钮。 -
选择云提供商,例如,我们选择亚马逊网络服务(AWS):
![操作步骤…]()
-
点击单节点(开发),然后选择沙盒选项。不要更改云服务器的位置,因为免费沙盒实例并非在所有数据中心都可用。由于这是一个沙盒,我们可以接受任何位置。
-
为您的数据库添加任何名称;我选择的名称是
mongolab-test。在输入名称后,点击创建新的 MongoDB 部署。 -
这将带您到主页,现在应该可以看到数据库。点击实例名称。此页面显示所选的 MongoDB 实例的详细信息。在页面顶部提供了在 shell 或编程语言中连接的指示,以及已启动实例的公共主机名。
-
点击用户选项卡,然后点击添加数据库用户按钮。在弹出窗口中,分别添加用户名和密码为
testUser和testUser(或者您自己选择的任何用户名和密码)。![操作步骤…]()
-
添加用户后,按照以下步骤启动 mongo shell,假设数据库名称为
mongolab-test,用户名和密码为testUser:
$ mongo <host-name>/mongolab-test –u testUser –p testUser
连接后,在 shell 中执行以下操作,并检查数据库名称是否为mongolab-test:
> db
- 按以下方式向集合中插入一个文档:
> db.messages.insert({_id:1, message:'Hello mongolab'})
- 按以下方式查询集合:
> db.messages.findOne()
工作原理…
执行的步骤非常简单,我们在云中创建了一个共享的沙盒实例。MongoLab 本身不托管实例,而是使用云提供商之一来托管。MongoLab 并不支持所有提供商的沙盒实例。沙盒实例的存储空间为 0.5 GB,并与同一台机器上的其他实例共享。共享实例比在专用实例上运行要便宜,但性能方面要付出代价。CPU 和 IO 与其他实例共享,因此我们共享实例的性能并不一定在我们的控制之下。对于生产用例,共享实例不是一个推荐的选项。同样,当在生产环境中运行时,我们需要设置一个副本集。如果我们看一下步骤 2 中的图像,我们会看到单节点(开发)选项旁边还有另一个选项卡。在这里,您可以选择机器的配置,包括 RAM 和磁盘容量(以及价格),并设置一个副本集。

如您所见,您可以选择要使用的 MongoDB 版本。即使 MongoDB 发布了新版本,MongoLab 也不会立即开始支持,因为他们通常会等待几个次要版本的发布,然后才支持生产用户。此外,当我们选择配置时,默认可用的选项是两个数据节点和一个仲裁者,这对于大多数用例来说已经足够了。
所选择的 RAM 和磁盘完全取决于数据的性质以及查询密集程度或写入密集程度。这种大小选择是我们无论是在自己的基础设施上部署还是在云上部署都需要做的。工作集是在选择硬件的 RAM 之前必须了解的重要内容。概念验证和实验是为了处理数据的一个子集,然后可以对整个数据集进行估算。如果 IO 活动很高并且需要低 IO 延迟,您甚至可以选择 SSD,就像前面的图像中所示的那样。独立实例在可伸缩性方面与副本集一样好,除了可用性。因此,我们可以选择独立实例进行此类估算和开发目的。共享实例,无论是免费还是付费,都是开发目的的良好选择。请注意,与专用实例一样,共享实例不能按需重新启动。
我们选择哪个云服务提供商?如果您已经在云中部署了应用服务器,那么显然必须与您现有的供应商相同。建议您为应用服务器和数据库使用相同的云供应商,并确保它们都部署在同一位置,以最小化延迟并提高性能。如果您是从头开始的,那么请花些时间选择云服务提供商。查看应用程序所需的所有其他服务,例如存储、计算、其他服务(如邮件、通知服务等)。所有这些分析都超出了本书的范围,但一旦完成并确定了供应商,您可以相应地在 MongoLab 中选择要使用的供应商。就定价而言,所有主要供应商都提供有竞争力的定价。
从 MongoLab GUI 对 MongoDB 执行操作
在上一个步骤中,我们看到了如何在云中使用 MongoLab 为 MongoDB 设置一个简单的沙箱实例。在本步骤中,我们将在此基础上构建,并查看 MongoLab 从管理、管理、监控和备份的角度为您提供了哪些服务。
准备工作
请参阅上一个步骤,在 MongoLab 上设置沙箱 MongoDB 实例,了解如何在云中使用 MongoLab 设置沙箱实例。
如何做…
-
转到
mongolab.com/home;您应该看到数据库、服务器和集群的列表。如果您遵循了上一个步骤,您应该会看到一个独立的数据库,mongolab-test(或者您为数据库选择的任何名称)。单击数据库名称,这应该会带您到数据库详细信息页面。 -
单击集合选项卡后,应该默认选择,我们应该看到数据库中存在的集合列表。如果在执行本步骤之前执行了上一个步骤,您应该会在数据库中看到一个名为 messages 的集合。
-
单击集合的名称,我们应该会被导航到集合详细信息页面,如下所示:
![如何做…]()
-
单击统计选项以查看集合的统计信息。
-
在文档选项卡中,我们可以查询集合。默认情况下,我们看到每页显示 10 个文档的所有文档,可以从每页记录下拉菜单中进行更改。可以选择的最大值为 100。
-
还有另一种查看文档的方法,即作为表格。单击显示模式中的表格单选按钮,并单击链接以创建/编辑表视图。在显示的弹出窗口中,输入以下消息集合的文档,然后单击提交:
{
"id": "_id",
"Message Text": "message"
}
在这样做的情况下,显示将会按以下方式更改:

-
从--开始新搜索--下拉菜单中,选择[新搜索]选项,如下图所示:
![如何做…]()
-
使用新查询,我们看到以下字段,让我们输入查询字符串、排序顺序和投影。将查询输入为
{"_id":1},字段输入为{"message":1, "_id":0}:![如何做…]()
-
您可以选择通过单击保存此搜索按钮并为要保存的查询命名来保存查询。
-
可以通过单击每条记录旁边的叉号来删除单个文档。同样,顶部的删除全部按钮将删除集合的所有内容。
-
类似地,单击+添加文档将弹出一个编辑器,用于输入要插入集合的文档。由于 MongoDB 是无模式的,文档不需要具有固定的字段集;应用程序应该能够理解它。
-
转到
https://mongolab.com/databases/<your database name>(在本例中为 mongolab-test),也可以通过从主页单击数据库名称来到达。 -
单击统计选项卡旁边的用户选项卡。在表中显示的内容是
db.stats()命令的结果。 -
类似地,单击备份选项卡,位于统计选项卡旁边的顶部。在这里,我们可以选择定期备份或一次性备份。
-
当您单击计划定期备份时,会弹出一个窗口,让您输入调度的详细信息,例如备份的频率,需要进行备份的时间以及要保留的备份数量。
-
备份位置可以选择为 MongoLab 自己的 S3 存储桶或 Rackspace 云文件。您可以选择使用自己帐户的存储空间,在这种情况下,您将不得不共享 AWS 访问密钥/秘密密钥或 Rackspace 的 UserID/API 密钥。
工作原理...
步骤 1 到 5 非常简单。在第 6 步,我们提供了一个 JSON 文档,以表格格式显示结果。文档的格式如下:
{
<display column 1> : <name of the field in the JSON document> ,
<display column 2> : <name of the field in the JSON document> ,
<display column n> : <name of the field in the JSON document>
}
键是要显示的列的名称,值是实际文档中字段的名称,其值将显示为此列的值。为了更清楚地理解,请查看为消息集合定义的文档,然后查看显示的表格数据。以下是我们提供的 JSON 文档,其中将列的名称作为键的值,并将文档中的实际字段作为列的值:
{
"id": "_id",
"Message Text": "message"
}
请注意,这里的 JSON 文档的字段名称和值都用引号括起来。Mongo shell 在这方面很宽松,允许我们在不使用引号的情况下给出字段名称。
如果我们访问关于备份的第 16 步,我们会发现备份要么存储在 MongoLab 的 AWS S3/Rackspace 云文件中,要么存储在您自定义的 AWS S3 存储桶/Rackspace 云文件中。在后一种情况下,您需要与 MongoLab 共享您的 AWS/Rackspace 凭据。如果这是一个问题,并且凭据可能被用来访问其他资源,建议您创建一个单独的帐户,并将其用于从 MongoLab 进行备份。您还可以使用创建的备份来从 MongoLab 创建一个新的 MongoDB 服务器实例。不用说,如果您使用自己的 AWS S3 存储桶/Rackspace 云文件,存储费用是额外的,因为它们不是 MongoLab 费用的一部分。
有一些值得一提的重要点。MongoLab 为各种操作提供了 REST API。 REST API 可以用来代替标准驱动程序执行 CRUD 操作;但是,使用 MongoDB 客户端库是推荐的方法。现在使用 REST API 而不是语言驱动程序的一个很好的理由是,如果客户端通过公共网络连接到 MongoDB 服务器。我们在本地机器上启动的 shell 连接到云上的 MongoDB 服务器会将未加密的数据发送到服务器,这使其容易受到攻击。另一方面,如果使用 REST API,流量将通过安全通道发送,因为使用了 HTTPS。MongoLab 计划在未来支持客户端和服务器之间通信的安全通道,但在撰写本书时,这是不可用的。如果应用程序和数据库位于云提供商的同一数据中心,则您是安全的,并且可以依赖云提供商为其本地网络提供的安全性,这通常不是一个问题。但是,除了确保您的数据不通过公共网络传输之外,您无法做任何安全通信的事情。
还有一种情况是 MongoLab 无法使用的,那就是当您希望实例在您自己的虚拟机实例上运行,而不是由 MongoLab 选择的实例,或者当我们希望应用程序在虚拟专用云中。云提供商确实提供诸如 Amazon VPC 之类的服务,其中 AWS 云的一部分可以被视为您网络的一部分。如果您打算在这样的环境中部署 MongoDB 实例,那么 MongoLab 将无法使用。
在 Amazon EC2 上手动设置 MongoDB
在之前的几个配方中,我们看到了如何使用 MongoLab 提供的托管服务在云中启动 MongoDB,该服务为我们提供了在所有主要云供应商上设置 MongoDB 的替代方案。但是,如果我们计划自己托管和监控实例以获得更大的控制权,或者在我们自己的虚拟私有云中设置,我们可以自己做。虽然各个云供应商的流程有所不同,但我们将使用 AWS 进行演示。有几种方法可以做到这一点,但在这个配方中,我们将使用Amazon Machine Image(AMI)。AMI 是一个模板,包含了启动云上新虚拟机实例时将使用的操作系统、软件等详细信息。要了解更多关于 AMI 的信息,请参考en.wikipedia.org/wiki/Amazon_Machine_Image。
谈到 AWS EC2,它代表弹性云计算,是一个让您在云中创建、启动和停止不同配置的服务器的服务,运行您选择的操作系统。(价格也相应不同。)同样,亚马逊弹性块存储(EBS)是一个提供高可用性和低延迟的持久块存储的服务。初始时,每个实例都附有一个称为临时存储的存储。这是一个临时存储,当实例重新启动时,数据可能会丢失。因此,EBS 块存储被附加到 EC2 实例上,以保持持久性,即使实例停止然后重新启动。标准 EBS 不提供每秒保证的最小IO 操作(IOPS)。对于中等工作负载,大约 100 IOPS 的默认值是可以的。但是,对于高性能 IO,也可以使用具有保证 IOPS 的 EBS 块。与标准 EBS 块相比,价格更高,但如果低 IO 速率可能成为系统性能瓶颈的话,这是一个不错的选择。
在这个配方中,我们将设置一个小型微实例,作为一个足够好的沙盒实例,并附加一个 EBS 块卷。
准备工作
首先,您需要做的是注册一个 AWS 账户。访问aws.amazon.com/,然后点击注册。如果您有亚马逊账户,请登录,否则,请创建一个新账户。尽管我们这里使用的配方将使用免费的微实例,但您仍需要提供信用卡信息,除非我们另有明确说明。我们将使用 Putty 连接到云上的实例。如果您的机器上尚未安装 Putty,可以下载并安装。下载地址为www.putty.org/。
对于使用 AMI 进行安装的特定配方,我们不能使用微实例,而必须使用标准大型实例。您可以在aws.amazon.com/ec2/pricing/上获取不同地区 EC2 实例定价的更多详细信息。根据地理和财务因素选择适当的地区。
-
首先,您需要做的是创建一个密钥对,以防您尚未创建。从 1 到 5 的以下步骤仅用于创建密钥对。此密钥对将用于从 Putty 客户端登录到云中启动的 Unix 实例。如果密钥对已经创建并且
.pem文件对您可用,请跳到第 6 步。 -
转到
console.aws.amazon.com/ec2/,确保右上角显示的地区(如下图所示)与您计划设置实例的地区相同。![准备工作]()
-
选择区域后,资源标题的页面将显示该区域的所有实例、密钥对、IP 地址等。单击密钥对链接,这将引导您到显示所有现有密钥对并且您可以创建新密钥对的页面。
-
单击创建密钥对按钮,在弹出窗口中输入您选择的任何名称。假设我们称之为
EC2 测试密钥对,然后单击创建。 -
创建后,将生成一个
.pem文件。确保保存该文件,因为随后需要访问该机器。 -
接下来,我们将把这个
.pem文件转换为一个.ppk文件,以便与 Putty 一起使用。 -
打开 puttygen;如果尚未提供,可以从
www.chiark.greenend.org.uk/~sgtatham/putty/download.html下载。
您应该在屏幕上看到以下内容:

-
选择SSH-2 RSA选项,然后单击加载按钮。在文件对话框中,选择所有文件,然后选择与在 EC2 控制台中生成的密钥对一起下载的
.pem文件。 -
一旦导入了
.pem文件,单击保存私钥选项,并使用任何名称保存文件;这次文件是.ppk文件。将此文件保存以便将来从 putty 登录到 EC2 实例。
注意
如果您使用的是 Mac OS X 或 Linux,可以使用ssh-keygen实用程序生成 SSH 密钥。
如何操作…
-
转到
console.aws.amazon.com/ec2/,然后单击左侧的实例选项,然后单击启动实例按钮:![如何操作…]()
-
由于我们想要启动一个免费的微实例,在左侧勾选仅限免费套餐复选框。在右侧,选择我们想要设置的实例。我们选择使用Ubuntu 服务器。单击选择以导航到下一个窗口。
-
选择微实例,然后单击审阅和启动。忽略安全警告;您将拥有的默认安全组将接受来自公共网络上所有主机的端口 22 的连接。
-
不更改任何默认设置,单击启动。启动后,将弹出一个窗口,让您选择现有的密钥对。如果您继续没有密钥对,您将需要密码或需要创建一个新的密钥对。在上一篇文章中,我们已经创建了一个密钥对,这就是我们将在这里使用的内容。
-
单击启动实例以启动新的微实例。
-
参考上一篇文章中第 9 至 12 步,了解如何使用 Putty 连接到已启动的实例。请注意,这次我们将使用 Ubuntu 用户,而不是上一篇文章中使用的
ec2-user,因为这次我们使用的是 Ubuntu 而不是 Amazon Linux。 -
在添加 MongoDB 存储库之前,我们需要按照以下步骤导入 MongoDB 公钥:
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
- 在操作系统 shell 中执行以下命令:
$ echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list
- 通过执行以下命令加载本地数据库:
$ sudo apt-get install mongodb-org
- 执行以下命令以创建所需的目录:
$ sudo mkdir /data /log
- 按照以下步骤启动
mongod进程:
$ sudo mongod --dbpath /data --logpath /log/mongodb.log --smallfiles --oplogsize 50 –fork
为了确保服务器进程正在运行,执行以下命令,并且我们应该在日志中看到以下内容:
$ tail /log/mongodb.log
2015-05-04T13:41:16.533+0000 [initandlisten] journal dir=/data/journal
2015-05-04T13:41:16.534+0000 [initandlisten] recover : no journal files present, no recovery needed
2015-05-04T13:41:16.628+0000 [initandlisten] waiting for connections on port 27017
- 按照以下步骤启动 mongo shell 并执行以下命令:
$ mongo
> db.ec2Test.insert({_id: 1, message: 'Hello World !'})
> db.ec2Test.findOne()
工作原理…
许多步骤都是不言自明的。建议您至少阅读前一篇文章,因为那里解释了许多概念。在前一篇文章中解释的大多数概念也适用于这里。这一节中解释了一些不同的地方。对于安装,我们选择了 Ubuntu,而不是使用 AMI 设置服务器时的标准 Amazon Linux。不同的操作系统在安装方面有不同的步骤。请参阅docs.mongodb.org/manual/installation/,了解如何在不同平台上安装 MongoDB 的步骤。本文中步骤 7 至 9 是特定于在 Ubuntu 上安装 MongoDB 的。请参阅help.ubuntu.com/12.04/serverguide/apt-get.html,了解我们在这里执行的apt-get命令的更多细节,以安装 MongoDB。
在我们的情况下,我们选择将数据、日志和日志文件夹放在同一个 EBS 卷上。这是因为我们设置的是一个dev实例。在prod实例的情况下,有不同的 EBS 卷,为了最佳性能,有预留的 IOPS。这种设置使我们能够利用这样一个事实,即这些不同的卷有不同的控制器,因此可以进行并发写操作。预留 IOPS 的 EBS 卷由 SSD 驱动器支持。docs.mongodb.org/manual/administration/production-notes/上的生产部署说明指出,MongoDB 部署应该由 RAID-10 磁盘支持。在 AWS 上部署时,优先选择 PIOPS 而不是 RAID-10。例如,如果需要 4000 IOPS,则选择具有 4000 IOPS 的 EBS 卷,而不是具有 2 X 2000 IOPS 或 4 X 1000 IOPS 设置的 RAID-10 设置。这不仅消除了不必要的复杂性,而且使得可以对单个磁盘进行快照,而不是处理 RAID-10 设置中的多个磁盘。谈到快照,大多数生产部署中的日志和数据是写入到不同的卷中的。这是快照无法工作的情况。我们需要刷新 DB 写入,锁定数据以进行进一步的写入,直到备份完成,然后释放锁定。有关快照和备份的更多详细信息,请参阅docs.mongodb.org/manual/tutorial/backup-with-filesystem-snapshots/。
请参阅docs.mongodb.org/ecosystem/platforms/,了解在不同云提供商上部署的更多详细信息。有一个专门针对 Amazon EC2 实例备份的部分。在生产部署中,最好使用 AMI 来设置 MongoDB 实例,就像在前一篇文章中演示的那样,而不是手动设置实例。手动设置适用于小型开发目的,而具有预留 IOPS 的 EBS 卷的大型实例则过于复杂。
另请参阅
-
云形成是一种可以定义模板并自动化 EC2 实例创建的方式。您可以在
aws.amazon.com/cloudformation/了解更多云形成是什么,并参考mongodb-documentation.readthedocs.org/en/latest/ecosystem/tutorial/automate-deployment-with-cloudformation.html。 -
另一种选择是使用 Mongo 的云服务:
docs.cloud.mongodb.com/tutorial/nav/add-servers-through-aws-integration/。 -
您可以通过参考维基百科上的这两个 URL 了解有关 RAID 的更多信息:
en.wikipedia.org/wiki/Standard_RAID_levels和en.wikipedia.org/wiki/Nested_RAID_levels。这里给出的描述非常全面。
使用 Docker 容器设置 MongoDB
容器移动,我喜欢称之为,已经触及了信息技术的几乎所有方面。作为首选工具的 Docker 对于创建和管理容器至关重要。
在本教程中,我们将在 Ubuntu(14.04)服务器上安装 Docker 并在容器中运行 MongoDB。
准备工作
- 首先,我们需要在我们的 Ubuntu 服务器上安装 Docker,可以通过运行此命令来完成:
$ wget -qO- https://get.docker.com/ | sh
- 启动 Docker 服务:
$ service docker start
> docker start/running, process 24369
- 确认 Docker 是否正在运行如下:
$ docker info
> Containers: 40
> Images: 311
> Storage Driver: aufs
> Root Dir: /var/lib/docker/aufs
> Dirs: 395
> Execution Driver: native-0.2
> Kernel Version: 3.13.0-37-generic
> Operating System: Ubuntu 14.04.2 LTS
> WARNING: No swap limit support
如何做…
- 从 Docker Hub 获取默认的 MongoDB 图像如下:
$ docker pull mongo
- 让我们确认图像是否已安装以下命令:
$ docker images | grep mongo
- 启动 MongoDB 服务器:
$ docker run -d --name mongo-server-1 mongo
> dfe7684dbc057f2d075450e3c6c96871dea98ff6b78abe72944360f4c239a72e
或者,您也可以运行docker ps命令来检查正在运行的容器列表。
- 获取此容器的 IP:
$ docker inspect mongo-server-1 | grep IPAddress
> "IPAddress": "172.17.0.3",
- 使用 mongo 客户端连接到我们的新容器:
$ mongo 172.17.0.3
>MongoDB shell version: 3.0.4
> connecting to: 172.17.0.3/test
>
- 在服务器上创建一个目录:
$ mkdir –p /data/db2
- 启动一个新的 MongoDB 容器:
$ docker run -d --name mongo-server-2 -v /data/db1:/data/db mongo
- 获取此新容器的 IP,如第 4 步所述,并使用 Mongo 客户端进行连接:
$ docker inspect mongo-server-2 | grep IPAddress
> "IPAddress": "172.17.0.4",
$ mongo 172.17.0.4
>MongoDB shell version: 3.0.4
> connecting to: 172.17.0.4/test
>
- 让我们为我们的最终容器创建另一个目录:
$ mkdir –p /data/db3
启动一个新的 MongoDB 容器:
$ docker run -d --name mongo-server-3 -v /data/db3:/data/db -p 9999:27017 mongo
- 让我们通过 localhost 连接到这个容器:
$ mongo localhost:9999
> MongoDB shell version: 3.0.4
> connecting to: localhost:9999/test
它是如何工作的…
我们首先从 DockerHub(hub.docker.com/_/mongo/)下载默认的 MongoDB 图像。Docker 图像是为其应用程序定制的自持续 OS 图像。所有 Docker 容器都是这些图像的隔离执行。这与使用 OS 模板创建虚拟机非常相似。
图像下载操作默认为获取最新的稳定的 MongoDB 图像,但您可以通过提及标签来指定您选择的版本,例如docker pull mongo:2.8。
我们通过运行docker images命令来验证图像是否已下载,该命令将列出服务器上安装的所有图像。在第 3 步,我们使用名称mongo-server-1在分离(-d)模式下启动容器,使用我们的 mongo 图像。描述容器内部可能超出了本教程的范围,但简而言之,我们现在在我们的 Ubuntu 机器内部运行一个隔离的docker 伪服务器。
默认情况下,每个 Docker 容器都会被 docker 服务器分配一个 RFC 1918(不可路由)的 IP 地址空间。为了连接到这个容器,我们在第 4 步获取 IP 地址,并在第 5 步连接到mongodb实例。
但是,每个 Docker 容器都是短暂的,因此销毁容器意味着丢失数据。在第 6 步,我们创建一个本地目录,用于存储我们的 mongo 数据库。在第 7 步中启动一个新的容器;它类似于我们之前的命令,但增加了 Volumes(-v)开关。在我们的示例中,我们将/data/db2目录暴露给 mongo 容器命名空间作为/data/db。这类似于 NFS 样的文件挂载,但在内核命名空间的限制内。
最后,如果我们希望外部系统连接到此容器,我们将容器的端口绑定到主机的端口。在第 9 步,我们使用端口(-p)开关将 Ubuntu 服务器上的 TCP 9999端口绑定到此容器的 TCP 27017端口。这确保任何连接到服务器端口9999的外部系统将被路由到这个特定的容器。
另请参阅
您还可以尝试使用 docker 命令的 Link(-l)命令行参数链接两个容器。
有关更多信息,请访问docs.docker.com/userguide/dockerlinks/。
第八章:与 Hadoop 集成
在本章中,我们将涵盖以下示例:
-
使用 mongo-hadoop 连接器执行我们的第一个样本 MapReduce 作业
-
编写我们的第一个 Hadoop MapReduce 作业
-
在 Hadoop 上使用流式处理运行 MapReduce 作业
-
在 Amazon EMR 上运行 MapReduce 作业
介绍
Hadoop 是一个众所周知的用于处理大型数据集的开源软件。它还有一个用于 MapReduce 编程模型的 API,被广泛使用。几乎所有的大数据解决方案都有某种支持,以便将它们与 Hadoop 集成,以使用其 MapReduce 框架。MongoDB 也有一个连接器,可以与 Hadoop 集成,让我们使用 Hadoop MapReduce API 编写 MapReduce 作业,处理驻留在 MongoDB/MongoDB 转储中的数据,并将结果写入 MongoDB/MongoDB 转储文件。在本章中,我们将看一些关于基本 MongoDB 和 Hadoop 集成的示例。
使用 mongo-hadoop 连接器执行我们的第一个样本 MapReduce 作业
在这个示例中,我们将看到如何从源代码构建 mongo-hadoop 连接器,并设置 Hadoop,以便仅用于在独立模式下运行示例。连接器是在 Mongo 中使用数据运行 Hadoop MapReduce 作业的支柱。
准备工作
Hadoop 有各种发行版;但是,我们将使用 Apache Hadoop (hadoop.apache.org/)。安装将在 Ubuntu Linux 上进行。Apache Hadoop 始终在 Linux 环境下运行用于生产,Windows 未经过生产系统测试。开发目的可以使用 Windows。如果您是 Windows 用户,我建议您安装虚拟化环境,如 VirtualBox (www.virtualbox.org/),设置 Linux 环境,然后在其上安装 Hadoop。在这个示例中没有展示设置 VirtualBox 和 Linux,但这不是一项繁琐的任务。这个示例的先决条件是一台安装了 Linux 操作系统的机器和一个互联网连接。我们将在这里设置 Apache Hadoop 的 2.4.0 版本。在撰写本书时,mongo-hadoop 连接器支持的最新版本是 2.4.0。
需要 Git 客户端来克隆 mongo-hadoop 连接器的存储库到本地文件系统。参考git-scm.com/book/en/Getting-Started-Installing-Git来安装 Git。
您还需要在操作系统上安装 MongoDB。参考docs.mongodb.org/manual/installation/并相应地安装它。启动监听端口27017的mongod实例。不需要您成为 Hadoop 的专家,但对它有一些了解会有所帮助。了解 MapReduce 的概念很重要,了解 Hadoop MapReduce API 将是一个优势。在这个示例中,我们将解释完成工作所需的内容。您可以从其他来源获取有关 Hadoop 及其 MapReduce API 的更多详细信息。维基页面en.wikipedia.org/wiki/MapReduce提供了有关 MapReduce 编程的一些很好的信息。
如何做…
- 我们将首先安装 Java、Hadoop 和所需的软件包。我们将从在操作系统上安装 JDK 开始。在操作系统的命令提示符上键入以下内容:
$ javac –version
- 如果程序无法执行,并告知您包含 javac 和程序的各种软件包,则需要按照以下方式安装 Java:
$ sudo apt-get install default-jdk
这就是我们安装 Java 所需要做的一切。
-
从
www.apache.org/dyn/closer.cgi/hadoop/common/下载当前版本的 Hadoop,并下载 2.4.0 版本(或最新的 mongo-hadoop 连接器支持)。 -
在下载
.tar.gz文件后,在命令提示符上执行以下操作:
$ tar –xvzf <name of the downloaded .tar.gz file>
$ cd <extracted directory>
打开etc/hadoop/hadoop-env.sh文件,并将export JAVA_HOME = ${JAVA_HOME}替换为export JAVA_HOME = /usr/lib/jvm/default-java。
现在,我们将在本地文件系统上从 GitHub 获取 mongo-hadoop 连接器代码。请注意,您无需 GitHub 帐户即可克隆存储库。请按照以下操作系统命令提示符中的 Git 项目进行克隆:
$git clone https://github.com/mongodb/mongo-hadoop.git
$cd mongo-hadoop
- 创建软链接- Hadoop 安装目录与我们在第 3 步中提取的目录相同:
$ln –s <hadoop installation directory> ~/hadoop-binaries
例如,如果 Hadoop 在主目录中提取/安装,则应执行以下命令:
$ln –s ~/hadoop-2.4.0 ~/hadoop-binaries
默认情况下,mongo-hadoop 连接器将在〜/hadoop-binaries文件夹下查找 Hadoop 分发。因此,即使 Hadoop 存档在其他位置提取,我们也可以创建软链接。创建软链接后,我们应该在〜/hadoop-binaries/hadoop-2.4.0/bin路径中拥有 Hadoop 二进制文件。
- 现在,我们将从源代码为 Apache Hadoop 版本 2.4.0 构建 mongo-hadoop 连接器。默认情况下,构建最新版本,因此现在可以省略
-Phadoop_version参数,因为 2.4 是最新版本。
$./gradlew jar –Phadoop_version='2.4'
此构建过程将需要一些时间才能完成。
-
构建成功后,我们将准备执行我们的第一个 MapReduce 作业。我们将使用 mongo-hadoop 连接器项目提供的
treasuryYield示例来执行此操作。第一步是将数据导入 Mongo 的集合中。 -
假设
mongod实例正在运行并监听端口27017进行连接,并且当前目录是 mongo-hadoop 连接器代码库的根目录,请执行以下命令:
$ mongoimport -c yield_historical.in -d mongo_hadoop --drop examples/treasury_yield/src/main/resources/yield_historical_in.json
- 导入操作成功后,我们需要将两个 jar 文件复制到
lib目录中。在操作系统 shell 中执行以下操作:
$ wget http://repo1.maven.org/maven2/org/mongodb/mongo-java-driver/2.12.0/mongo-java-driver-2.12.0.jar
$ cp core/build/libs/mongo-hadoop-core-1.2.1-SNAPSHOT-hadoop_2.4.jar ~/hadoop-binaries/hadoop-2.4.0/lib/
$ mv mongo-java-driver-2.12.0.jar ~/hadoop-binaries/hadoop-2.4.0/lib
注意
为了 mongo-hadoop 核心构建的 JAR 文件要复制,根据代码的前面部分和为 Hadoop-2.4.0 构建的版本,更改 JAR 的名称。当您为连接器和 Hadoop 的不同版本自行构建时,Mongo 驱动程序可以是最新版本。在撰写本书时,版本 2.12.0 是最新版本。
- 现在,在操作系统 shell 的命令提示符上执行以下命令:
~/hadoop-binaries/hadoop-2.4.0/bin/hadoop jar examples/treasury_yield/build/libs/treasury_yield-1.2.1-SNAPSHOT-hadoop_2.4.jar \com.mongodb.hadoop.examples.treasury.TreasuryYieldXMLConfig \-Dmongo.input.split_size=8 -Dmongo.job.verbose=true \-Dmongo.input.uri=mongodb://localhost:27017/mongo_hadoop.yield_historical.in \-Dmongo.output.uri=mongodb://localhost:27017/mongo_hadoop.yield_historical.out
- 输出应该打印出很多内容;但是,输出中的以下行告诉我们 MapReduce 作业成功:
14/05/11 21:38:54 INFO mapreduce.Job: Job job_local1226390512_0001 completed successfully
- 从 mongo 客户端连接运行在本地主机上的
mongod实例,并对以下集合执行查找:
$ mongo
> use mongo_hadoop
switched to db mongo_hadoop
> db.yield_historical.out.find()
工作原理…
安装 Hadoop 并不是一项简单的任务,我们不需要进行这项工作来尝试 hadoop-mongo 连接器的示例。有专门的书籍和文章可供学习 Hadoop、其安装和其他内容。在本章中,我们将简单地下载存档文件,提取并以独立模式运行 MapReduce 作业。这是快速入门 Hadoop 的最快方式。在步骤 6 之前的所有步骤都是安装 Hadoop 所需的。在接下来的几个步骤中,我们将克隆 mongo-hadoop 连接器配方。如果您不想从源代码构建,也可以在github.com/mongodb/mongo-hadoop/releases下载适用于您 Hadoop 版本的稳定版本。然后,我们为我们的 Hadoop 版本(2.4.0)构建连接器,直到第 13 步。从第 14 步开始,我们将运行实际的 MapReduce 作业来处理 MongoDB 中的数据。我们将数据导入到yield_historical.in集合中,这将作为 MapReduce 作业的输入。继续使用mongo_hadoop数据库在 mongo shell 中查询集合,以查看文档。如果您不理解内容,不用担心;我们想要看到这个示例中的数据意图。
下一步是在数据上调用 MapReduce 操作。执行 Hadoop 命令,给出一个 jar 的路径(examples/treasury_yield/build/libs/treasury_yield-1.2.1-SNAPSHOT-hadoop_2.4.jar)。这个 jar 包含了实现国库收益率样本 MapReduce 操作的类。在这个 JAR 文件中的com.mongodb.hadoop.examples.treasury.TreasuryYieldXMLConfig类是包含主方法的引导类。我们很快就会访问这个类。连接器支持许多配置。完整的配置列表可以在github.com/mongodb/mongo-hadoop/找到。现在,我们只需要记住mongo.input.uri和mongo.output.uri是 map reduce 操作的输入和输出集合。
项目克隆后,您现在可以将其导入到您选择的任何 Java IDE 中。我们特别感兴趣的是位于/examples/treasury_yield的项目和位于克隆存储库根目录中的核心。
让我们看一下com.mongodb.hadoop.examples.treasury.TreasuryYieldXMLConfig类。这是 MapReduce 方法的入口点,并在其中有一个主方法。要使用 mongo-hadoop 连接器为 mongo 编写 MapReduce 作业,主类始终必须扩展自com.mongodb.hadoop.util.MongoTool。这个类实现了org.apache.hadoop.Tool接口,该接口具有 run 方法,并由MongoTool类为我们实现。主方法需要做的就是使用org.apache.hadoop.util.ToolRunner类执行这个类,通过调用其静态run方法传递我们的主类的实例(这是Tool的实例)。
有一个静态块,从两个 XML 文件hadoop-local.xml和mongo-defaults.xml中加载一些配置。这些文件(或任何 XML 文件)的格式如下。文件的根节点是具有多个属性节点的配置节点:
<configuration>
<property>
<name>{property name}</name>
<value>{property value}</value>
</property>
...
</configuration>
在这种情况下有意义的属性值是我们之前提到的 URL 中提供的所有值。我们在引导类TreasuryYieldXmlConfig的构造函数中实例化com.mongodb.hadoop.MongoConfig,将org.apache.hadoop.conf.Configuration的实例包装起来。MongoConfig类提供了合理的默认值,这足以满足大多数用例。我们需要在MongoConfig实例中设置的一些最重要的事情是输出和输入格式、mapper和reducer类、mapper 的输出键和值,以及 reducer 的输出键和值。输入格式和输出格式将始终是com.mongodb.hadoop.MongoInputFormat和com.mongodb.hadoop.MongoOutputFormat类,这些类由 mongo-hadoop 连接器库提供。对于 mapper 和 reducer 的输出键和值,我们有任何org.apache.hadoop.io.Writable实现。有关org.apache.hadoop.io包中不同类型的 Writable 实现,请参考 Hadoop 文档。除此之外,mongo-hadoop 连接器还在com.mongodb.hadoop.io包中为我们提供了一些实现。对于国库收益率示例,我们使用了BSONWritable实例。这些可配置的值可以在之前看到的 XML 文件中提供,也可以以编程方式设置。最后,我们可以选择将它们作为vm参数提供,就像我们为mongo.input.uri和mongo.output.uri所做的那样。这些参数可以在 XML 中提供,也可以直接从代码中在MongoConfig实例上调用;这两种方法分别是setInputURI和setOutputURI。
现在我们将看一下mapper和reducer类的实现。我们将在这里复制类的重要部分以进行分析。有关整个实现,请参考克隆的项目:
public class TreasuryYieldMapper
extends Mapper<Object, BSONObject, IntWritable, DoubleWritable> {
@Override
public void map(final Object pKey,
final BSONObject pValue,
final Context pContext)
throws IOException, InterruptedException {
final int year = ((Date) pValue.get("_id")).getYear() + 1900;
double bid10Year = ((Number) pValue.get("bc10Year")).doubleValue();
pContext.write(new IntWritable(year), new DoubleWritable(bid10Year));
}
}
我们的 mapper 扩展了org.apache.hadoop.mapreduce.Mapper类。四个通用参数是键类、输入值类型、输出键类型和输出值类型。map 方法的主体从输入文档中读取_id值,即日期,并从中提取年份。然后,它从文档中获取bc10Year字段的双值,并简单地写入上下文键值对,其中键是年份,双值是上下文键值对的值。这里的实现不依赖于传递的pKey参数的值,可以使用该值作为键,而不是在实现中硬编码_id值。该值基本上是使用 XML 中的mongo.input.key属性或MongoConfig.setInputKey方法设置的相同字段。如果没有设置,_id是默认值。
让我们来看一下 reducer 的实现(删除了日志记录语句):
public class TreasuryYieldReducer extends Reducer<IntWritable, DoubleWritable, IntWritable, BSONWritable> {
@Override
public void reduce(final IntWritable pKey, final Iterable<DoubleWritable> pValues, final Context pContext)throws IOException, InterruptedException {
int count = 0;
double sum = 0;
for (final DoubleWritable value : pValues) {
sum += value.get();
count++;
}
final double avg = sum / count;
BasicBSONObject output = new BasicBSONObject();
output.put("count", count);
output.put("avg", avg);
output.put("sum", sum);
pContext.write(pKey, new BSONWritable(output));
}
}
这个类扩展自org.apache.hadoop.mapreduce.Reducer,有四个通用参数:输入键、输入值、输出键和输出值。reducer 的输入是 mapper 的输出,因此,如果你仔细观察,你会发现前两个通用参数的类型与我们之前看到的 mapper 的最后两个通用参数相同。第三和第四个参数是从 reduce 中发出的键和值的类型。值的类型是BSONDocument,因此我们有BSONWritable作为类型。
现在我们有了 reduce 方法,它有两个参数:第一个是键,与 map 函数发出的键相同,第二个参数是发出的相同键的值的java.lang.Iterable。这就是标准的 map reduce 函数的工作原理。例如,如果 map 函数给出以下键值对,(1950, 10), (1960, 20), (1950, 20), (1950, 30),那么 reduce 将使用两个唯一的键 1950 和 1960 进行调用,并且键 1950 的值将是Iterable,包括(10, 20, 30),而 1960 的值将是单个元素(20)的Iterable。reducer 的 reduce 函数简单地迭代双值的Iterable,找到这些数字的和与计数,并写入一个键值对,其中键与传入的键相同,输出值是BasicBSONObject,其中包括计算值的和、计数和平均值。
在克隆的 mongo-hadoop 连接器示例中,包括 Enron 数据集在内有一些很好的示例。如果你想玩一下,我建议你看看这些示例项目并运行它们。
更多内容…
我们在这里看到的是一个现成的示例,我们执行了它。没有什么比自己编写一个 MapReduce 作业来澄清我们的理解更好。在下一个示例中,我们将使用 Java 中的 Hadoop API 编写一个 MapReduce 作业,并看到它的运行情况。
另请参阅…
如果你想知道Writable接口是什么,为什么不应该使用普通的旧序列化,那么请参考这个 URL,由 Hadoop 的创建者解释:www.mail-archive.com/hadoop-user@lucene.apache.org/msg00378.html。
编写我们的第一个 Hadoop MapReduce 作业
在这个示例中,我们将使用 Hadoop MapReduce API 编写我们的第一个 MapReduce 作业,并使用 mongo-hadoop 连接器从 MongoDB 获取数据运行它。请参考第三章中的使用 Java 客户端在 Mongo 中执行 MapReduce示例,了解如何使用 Java 客户端实现 MapReduce、测试数据创建和问题陈述。
准备工作
请参考之前的使用 mongo-hadoop 连接器执行我们的第一个样本 MapReduce 作业食谱来设置 mongo-hadoop 连接器。此食谱的先决条件和第三章中的使用 Java 客户端在 Mongo 中执行 MapReduce食谱是我们此食谱所需的全部内容。这是一个 maven 项目,因此需要设置和安装 maven。请参考第一章中的从 Java 客户端连接到单节点食谱,在那里我们提供了在 Windows 上设置 maven 的步骤;该项目是在 Ubuntu Linux 上构建的,以下是您需要在操作系统 shell 中执行的命令:
$ sudo apt-get install maven
操作步骤如下...
-
我们有一个 Java
mongo-hadoop-mapreduce-test项目,可以从 Packt 网站下载。该项目旨在实现我们在第三章中实现的用例,即在 MongoDB 的 MapReduce 框架中使用 Python 和 Java 客户端调用 MapReduce 作业。 -
在项目根目录中的当前目录中的命令提示符下,执行以下命令:
$ mvn clean package
-
JAR 文件
mongo-hadoop-mapreduce-test-1.0.jar将被构建并保存在目标目录中。 -
假设 CSV 文件已经导入到
postalCodes集合中,请在仍然位于我们刚构建的mongo-hadoop-mapreduce-test项目根目录中的当前目录中执行以下命令:
~/hadoop-binaries/hadoop-2.4.0/bin/hadoop \
jar target/mongo-hadoop-mapreduce-test-1.0.jar \
com.packtpub.mongo.cookbook.TopStateMapReduceEntrypoint \
-Dmongo.input.split_size=8 \
-Dmongo.job.verbose=true \
-Dmongo.input.uri=mongodb://localhost:27017/test.postalCodes \
-Dmongo.output.uri=mongodb://localhost:27017/test.postalCodesHadoopmrOut
- MapReduce 作业完成后,通过在操作系统命令提示符上键入以下内容打开 mongo shell,并在 shell 中执行以下查询:
$ mongo
> db.postalCodesHadoopmrOut.find().sort({count:-1}).limit(5)
- 将输出与我们之前使用 mongo 的 map reduce 框架执行 MapReduce 作业时获得的输出进行比较(在第三章中,编程语言驱动程序)。
工作原理...
我们将类保持得非常简单,只包含我们需要的最少内容。我们的项目中只有三个类:TopStateMapReduceEntrypoint、TopStateReducer和TopStatesMapper,都在同一个com.packtpub.mongo.cookbook包中。mapper 的map函数只是将键值对写入上下文,其中键是州的名称,值是整数值 1。以下是来自mapper函数的代码片段:
context.write(new Text((String)value.get("state")), new IntWritable(1));
Reducer 获得的是相同的键,即州的列表和整数值,为 1。我们所做的就是将相同州的名称和可迭代的总和写入上下文。现在,由于在 Iterable 中没有 size 方法可以在常数时间内给出计数,我们只能在线性时间内将所有得到的 1 相加。以下是 reducer 方法中的代码:
int sum = 0;
for(IntWritable value : values) {
sum += value.get();
}
BSONObject object = new BasicBSONObject();
object.put("count", sum);
context.write(text, new BSONWritable(object));
我们将文本字符串写入键,将包含计数的 JSON 文档写入上下文。然后,mongo-hadoop 连接器负责将postalCodesHadoopmrOut文档写入我们拥有的输出集合,其中_id字段与发射的键相同。因此,当我们执行以下操作时,我们将获得数据库中拥有最多城市的前五个州:
> db. postalCodesHadoopmrOut.find().sort({count:-1}).limit(5)
{ "_id" : "Maharashtra", "count" : 6446 }
{ "_id" : "Kerala", "count" : 4684 }
{ "_id" : "Tamil Nadu", "count" : 3784 }
{ "_id" : "Andhra Pradesh", "count" : 3550 }
{ "_id" : "Karnataka", "count" : 3204 }
最后,主入口类的主方法如下:
Configuration conf = new Configuration();
MongoConfig config = new MongoConfig(conf);
config.setInputFormat(MongoInputFormat.class);
config.setMapperOutputKey(Text.class);
config.setMapperOutputValue(IntWritable.class);
config.setMapper(TopStatesMapper.class);
config.setOutputFormat(MongoOutputFormat.class);
config.setOutputKey(Text.class);
config.setOutputValue(BSONWritable.class);
config.setReducer(TopStateReducer.class);
ToolRunner.run(conf, new TopStateMapReduceEntrypoint(), args);
我们所做的就是使用com.mongodb.hadoop.MongoConfig实例将org.apache.hadoop.conf.Configuration对象包装起来,以设置各种属性,然后使用 ToolRunner 提交 MapReduce 作业以执行。
另请参阅
我们使用 Hadoop API 在 Hadoop 上执行了一个简单的 MapReduce 作业,从 MongoDB 获取数据,并将数据写入 MongoDB 集合。如果我们想要用不同的语言编写map和reduce函数怎么办?幸运的是,使用一个称为 Hadoop streaming 的概念是可能的,其中stdout用作程序和 Hadoop MapReduce 框架之间的通信手段。在下一个示例中,我们将演示如何使用 Python 来实现与本示例中相同的用例,使用 Hadoop streaming。
使用流式传输在 Hadoop 上运行 MapReduce 作业
在我们之前的示例中,我们使用 Hadoop 的 Java API 实现了一个简单的 MapReduce 作业。用例与我们在第三章的示例中使用 Python 和 Java 中的 Mongo 客户端 API 实现 MapReduce 相同。在这个示例中,我们将使用 Hadoop streaming 来实现 MapReduce 作业。
流式传输的概念是基于使用stdin和stdout进行通信。您可以在hadoop.apache.org/docs/r1.2.1/streaming.html上获取有关 Hadoop streaming 及其工作原理的更多信息。
准备工作…
请参考本章中的使用 mongo-hadoop 连接器执行我们的第一个示例 MapReduce 作业示例,了解如何为开发目的设置 Hadoop 并使用 Gradle 构建 mongo-hadoop 项目。就 Python 库而言,我们将从源代码安装所需的库;但是,如果您不希望从源代码构建,可以使用pip(Python 的软件包管理器)进行设置。我们还将看到如何使用pip设置 pymongo-hadoop。
参考第一章中的使用 Python 客户端连接到单个节点示例,了解如何为您的主机操作系统安装 PyMongo。
工作原理…
- 我们将首先从源代码构建 pymongo-hadoop。将项目克隆到本地文件系统后,在克隆项目的根目录中执行以下操作:
$ cd streaming/language_support/python
$ sudo python setup.py install
-
输入密码后,设置将继续在您的计算机上安装 pymongo-hadoop。
-
这就是我们需要从源代码构建 pymongo-hadoop 的全部内容。但是,如果您选择不从源代码构建,可以在操作系统 shell 中执行以下命令:
$ sudo pip install pymongo_hadoop
- 以任何方式安装 pymongo-hadoop 后,我们将在 Python 中实现我们的
mapper和reducer函数。mapper函数如下:
#!/usr/bin/env python
import sys
from pymongo_hadoop import BSONMapper
def mapper(documents):
print >> sys.stderr, 'Starting mapper'
for doc in documents:
yield {'_id' : doc['state'], 'count' : 1}
print >> sys.stderr, 'Mapper completed'
BSONMapper(mapper)
- 现在是
reducer函数,将如下所示:
#!/usr/bin/env python
import sys
from pymongo_hadoop import BSONReducer
def reducer(key, documents):
print >> sys.stderr, 'Invoked reducer for key "', key, '"'
count = 0
for doc in documents:
count += 1
return {'_id' : key, 'count' : count}
BSONReducer(reducer)
- 环境变量
$HADOOP_HOME和$HADOOP_CONNECTOR_HOME应该分别指向 Hadoop 和 mongo-hadoop 连接器项目的基本目录。现在,我们将在操作系统 shell 中使用以下命令调用MapReduce函数。书中提供的代码在 Packt 网站上有mapper,reducePython 脚本和 shell 脚本,将用于调用mapper和reducer函数:
$HADOOP_HOME/bin/hadoop jar \
$HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming* \
-libjars $HADOOP_CONNECTOR_HOME/streaming/build/libs/mongo-hadoop-streaming-1.2.1-SNAPSHOT-hadoop_2.4.jar \
-input /tmp/in \
-output /tmp/out \
-inputformat com.mongodb.hadoop.mapred.MongoInputFormat \
-outputformat com.mongodb.hadoop.mapred.MongoOutputFormat \
-io mongodb \
-jobconf mongo.input.uri=mongodb://127.0.0.1:27017/test.postalCodes \
-jobconf mongo.output.uri=mongodb://127.0.0.1:27017/test.pyMRStreamTest \
-jobconf stream.io.identifier.resolver.class=com.mongodb.hadoop.streaming.io.MongoIdentifierResolver \
-mapper mapper.py \
-reducer reducer.py
在执行此命令时,mapper.py和reducer.py文件位于当前目录中。
- 执行该命令时,应该需要一些时间来成功执行 MapReduce 作业,在操作系统命令提示符上键入以下命令打开 mongo shell,并从 shell 执行以下查询:
$ mongo
> db.pyMRStreamTest.find().sort({count:-1}).limit(5)
- 将输出与我们之前在第三章中使用 mongo 的 MapReduce 框架执行 MapReduce 作业时获得的输出进行比较,编程语言驱动程序。
如何做…
让我们看一下步骤 5 和 6,我们编写mapper和reducer函数。我们定义了一个接受所有文档列表的map函数。我们遍历这些文档,并产生文档,其中_id字段是键的名称,计数值字段的值为 1。产生的文档数量将与输入文档的总数相同。
最后,我们实例化了BSONMapper,它接受mapper函数作为参数。该函数返回一个生成器对象,然后该BSONMapper类使用它来向 MapReduce 框架提供值。我们需要记住的是,mapper函数需要返回一个生成器(在循环中调用yield时返回),然后实例化BSONMapper类,这是由pymongo_hadoop模块提供给我们的。如果你感兴趣,你可以选择查看我们本地文件系统中克隆的项目中的streaming/language_support/python/pymongo_hadoop/mapper.py文件的源代码,看看它是做什么的。这是一段小而简单易懂的代码。
对于reducer函数,我们得到了键和该键对应的文档列表作为值。键与map函数中发出的文档的_id字段的值相同。我们在这里简单地返回一个新文档,其中_id是州的名称,计数是该州的文档数。记住,我们返回一个文档,而不是像在 map 中那样发出一个文档。最后,我们实例化BSONReducer并传递reducer函数。在我们本地文件系统中克隆的项目中的streaming/language_support/python/pymongo_hadoop/reducer.py文件中有BSONReducer类的实现。
最后,我们在 shell 中调用命令来启动使用流处理的 MapReduce 作业。这里需要注意的几点是,我们需要两个 JAR 文件:一个在 Hadoop 分发的share/hadoop/tools/lib目录中,另一个在 mongo-hadoop 连接器中,位于streaming/build/libs/目录中。输入和输出格式分别是com.mongodb.hadoop.mapred.MongoInputFormat和com.mongodb.hadoop.mapred.MongoOutputFormat。
正如我们之前看到的,sysout和sysin构成了流处理的基础。所以,基本上,我们需要对我们的 BSON 对象进行编码以写入sysout,然后,我们应该能够读取sysin以将内容再次转换为 BSON 对象。为此,mongo-hadoop 连接器为我们提供了两个框架类,com.mongodb.hadoop.streaming.io.MongoInputWriter和com.mongodb.hadoop.streaming.io.MongoOutputReader,用于对 BSON 对象进行编码和解码。这些类分别扩展自org.apache.hadoop.streaming.io.InputWriter和org.apache.hadoop.streaming.io.OutputReader。
stream.io.identifier.resolver.class属性的值是com.mongodb.hadoop.streaming.io.MongoIdentifierResolver。这个类继承自org.apache.hadoop.streaming.io.IdentifierResolver,并且让我们有机会注册我们的org.apache.hadoop.streaming.io.InputWriter和org.apache.hadoop.streaming.io.OutputReader的实现到框架中。我们还使用我们自定义的IdentifierResolver注册输出键和输出值类。只要记住,如果你正在使用 mongo-hadoop 连接器进行流处理,一定要始终使用这个解析器。
我们最终执行了之前讨论过的mapper和reducer的 Python 函数。要记住的一件重要的事情是,不要从mapper和reducer函数中向sysout打印日志。sysout和sysin的 mapper 和 reducer 是通信的手段,向其中写入日志可能会产生不良行为。正如我们在示例中看到的,要么写入标准错误(stderr),要么写入日志文件。
注意
在 Unix 中使用多行命令时,可以使用\在下一行继续命令。但是,记住在\后面不要有空格。
在 Amazon EMR 上运行 MapReduce 作业
这个教程涉及在 AWS 上使用云来运行 MapReduce 作业。您需要一个 AWS 账户才能继续。在aws.amazon.com/注册 AWS。我们将看到如何在云上使用 Amazon Elastic Map Reduce (Amazon EMR)运行 MapReduce 作业。Amazon EMR 是亚马逊在云上提供的托管 MapReduce 服务。更多详情请参考aws.amazon.com/elasticmapreduce/。Amazon EMR 从 AWS S3 存储桶中获取数据、二进制文件/JAR 等,处理它们并将结果写回 S3 存储桶。Amazon Simple Storage Service (Amazon S3)是 AWS 提供的另一个用于云上数据存储的服务。更多关于 Amazon S3 的详情请参考aws.amazon.com/s3/。虽然我们将使用 mongo-hadoop 连接器,有趣的是我们不需要一个 MongoDB 实例在运行。我们将使用存储在 S3 存储桶中的 MongoDB 数据转储进行数据分析。MapReduce 程序将在输入的 BSON 转储上运行,并在输出存储桶中生成结果 BSON 转储。MapReduce 程序的日志将被写入另一个专门用于日志的存储桶。下图给出了我们的设置在高层次上的样子:
在 Amazon EMR 上运行 MapReduce 作业
准备工作
我们将使用与编写我们的第一个 Hadoop MapReduce 作业教程相同的 Java 示例。要了解更多关于mapper和reducer类实现的信息,您可以参考同一教程的它是如何工作的部分。我们有一个mongo-hadoop-emr-test项目,其中包含可以从 Packt 网站下载的代码,用于使用 AWS EMR API 在云上创建 MapReduce 作业。为了简化事情,我们将只上传一个 JAR 到 S3 存储桶来执行 MapReduce 作业。这个 JAR 将使用 BAT 文件在 Windows 上组装,使用 Unix 操作系统上的 shell 脚本。mongo-hadoop-emr-testJava 项目有一个mongo-hadoop-emr-binaries子目录,其中包含必要的二进制文件以及将它们组装成一个 JAR 的脚本。
已组装的mongo-hadoop-emr-assembly.jar文件也提供在子目录中。运行.bat或.sh文件将删除这个 JAR 并重新生成已组装的 JAR,这并不是必需的。已提供的已组装的 JAR 足够好,可以正常工作。Java 项目包含一个data子目录,其中包含一个postalCodes.bson文件。这是从包含postalCodes集合的数据库中生成的 BSON 转储。mongo 分发提供的mongodump实用程序用于提取这个转储。
如何操作...
- 这个练习的第一步是在 S3 上创建一个存储桶。您可以选择使用现有的存储桶;但是,对于这个教程,我创建了一个
com.packtpub.mongo.cookbook.emr-in存储桶。请记住,存储桶的名称必须在所有 S3 存储桶中是唯一的,您将无法创建一个具有相同名称的存储桶。您将不得不创建一个不同名称的存储桶,并在这个教程中使用它来代替com.packtpub.mongo.cookbook.emr-in。
提示
不要使用下划线(_)创建存储桶名称;而是使用连字符(-)。使用下划线创建存储桶名称不会失败;但是后来的 MapReduce 作业会失败,因为它不接受存储桶名称中的下划线。
-
我们将上传已组装的 JAR 文件和一个
.bson文件到新创建(或现有)的 S3 存储桶。要上传文件,我们将使用 AWS 网络控制台。点击上传按钮,选择已组装的 JAR 文件和postalCodes.bson文件上传到 S3 存储桶。上传后,存储桶的内容应该如下所示:![如何操作...]()
-
接下来的步骤是从 AWS 控制台启动 EMR 作业,而不需要编写一行代码。我们还将看到如何使用 AWS Java SDK 启动此作业。如果您希望从 AWS 控制台启动 EMR 作业,请按照步骤 4 到 9 进行。如果要使用 Java SDK 启动 EMR 作业,请按照步骤 10 和 11 进行。
-
我们将首先从 AWS 控制台启动一个 MapReduce 作业。访问
console.aws.amazon.com/elasticmapreduce/并点击创建集群按钮。在集群配置屏幕中,输入图中显示的细节,除了日志桶,您需要选择作为日志需要写入的桶。您还可以点击文本框旁边的文件夹图标,选择您的帐户中存在的桶作为日志桶。![操作步骤…]()
注意
终止保护选项设置为否,因为这是一个测试实例。如果出现任何错误,我们宁愿希望实例终止,以避免保持运行并产生费用。
-
在软件配置部分,选择Hadoop 版本为2.4.0,AMI 版本为3.1.0 (hadoop 2.4.0)。通过点击其名称旁边的叉号来移除额外的应用程序,如下图所示:
![操作步骤…]()
-
在硬件配置部分,选择EC2 实例类型为m1.medium。这是我们需要为 Hadoop 版本 2.4.0 选择的最低配置。从下图中可以看到所选择的从属和任务实例的数量为零:
![操作步骤…]()
-
在安全和访问部分,保留所有默认值。我们也不需要引导操作,所以也保持不变。
-
最后一步是为 MapReduce 作业设置步骤。在添加步骤下拉菜单中,选择自定义 JAR选项,然后选择自动终止选项为是,如下图所示:
![操作步骤…]()
现在点击配置和添加按钮并输入细节。
JAR S3 位置的值为s3://com.packtpub.mongo.cookbook.emr-in/mongo-hadoop-emr-assembly.jar。这是我输入桶中的位置;您需要根据自己的输入桶更改输入桶。JAR 文件的名称将保持不变。
在参数文本区域中输入以下参数;主类的名称在列表中排在第一位:
com.packtpub.mongo.cookbook.TopStateMapReduceEntrypoint
-Dmongo.job.input.format=com.mongodb.hadoop.BSONFileInputFormat
-Dmongo.job.mapper=com.packtpub.mongo.cookbook.TopStatesMapper
-Dmongo.job.reducer=com.packtpub.mongo.cookbook.TopStateReducer
-Dmongo.job.output=org.apache.hadoop.io.Text
-Dmongo.job.output.value=org.apache.hadoop.io.IntWritable
-Dmongo.job.output.value=org.apache.hadoop.io.IntWritable
-Dmongo.job.output.format=com.mongodb.hadoop.BSONFileOutputFormat
-Dmapred.input.dir=s3://com.packtpub.mongo.cookbook.emr-in/postalCodes.bson
-Dmapred.output.dir=s3://com.packtpub.mongo.cookbook.emr-out/
-
最后两个参数的值包含了我 MapReduce 样本使用的输入和输出桶;这个值将根据您自己的输入和输出桶而改变。失败时的操作值将为终止。在填写完所有这些细节后,点击保存:
![操作步骤…]()
-
现在点击创建集群按钮。这将需要一些时间来配置和启动集群。
-
在接下来的几步中,我们将使用 AWS Java API 在 EMR 上创建一个 MapReduce 作业。将提供的代码示例中的
EMRTest项目导入到您喜欢的 IDE 中。导入后,打开com.packtpub.mongo.cookbook.AWSElasticMapReduceEntrypoint类。 -
类中有五个常量需要更改。它们是您将用于示例的输入、输出和日志存储桶,以及 AWS 访问密钥和秘密密钥。访问密钥和秘密密钥在您使用 AWS SDK 时充当用户名和密码。相应地更改这些值并运行程序。成功执行后,它应该为您新启动的作业提供一个作业 ID。
-
无论您如何启动 EMR 作业,请访问 EMR 控制台
console.aws.amazon.com/elasticmapreduce/以查看您提交的 ID 的状态。您在启动的作业的第二列中可以看到作业 ID,它将与您执行 Java 程序时在控制台上打印的作业 ID 相同(如果您使用 Java 程序启动)。单击启动的作业的名称,这应该将您引导到作业详细信息页面。硬件配置将需要一些时间,然后最终,您的 MapReduce 步骤将运行。作业完成后,作业的状态应在作业详细信息屏幕上如下所示:![操作方法…]()
展开后,步骤部分应如下所示:

-
单击日志文件部分下方的 stderr 链接,以查看 MapReduce 作业的所有日志输出。
-
现在 MapReduce 作业已完成,我们的下一步是查看其结果。访问 S3 控制台
console.aws.amazon.com/s3并访问输出存储桶。在我的情况下,以下是输出存储桶的内容:![操作方法…]()
part-r-0000.bson文件是我们感兴趣的。这个文件包含了我们的 MapReduce 作业的结果。
- 将文件下载到本地文件系统,并使用 mongorestore 实用程序导入到本地运行的 mongo 实例中。请注意,以下命令的还原实用程序期望 mongod 实例正在运行并侦听端口
27017,并且当前目录中有part-r-0000.bson文件:
$ mongorestore part-r-00000.bson -d test -c mongoEMRResults
- 现在,使用 mongo shell 连接到
mongod实例并执行以下查询:
> db.mongoEMRResults.find().sort({count:-1}).limit(5)
对于查询,我们将看到以下结果:
{ "_id" : "Maharashtra", "count" : 6446 }
{ "_id" : "Kerala", "count" : 4684 }
{ "_id" : "Tamil Nadu", "count" : 3784 }
{ "_id" : "Andhra Pradesh", "count" : 3550 }
{ "_id" : "Karnataka", "count" : 3204 }
- 这是前五个结果的预期结果。如果我们比较在在 Java 客户端中执行 Mongo 的 MapReduce中得到的结果,来自第三章的编程语言驱动程序,使用 Mongo 的 MapReduce 框架和本章中的编写我们的第一个 Hadoop MapReduce 作业配方,我们可以看到结果是相同的。
工作原理…
Amazon EMR 是一项托管的 Hadoop 服务,负责硬件配置,并让您远离设置自己的集群的麻烦。与我们的 MapReduce 程序相关的概念已经在“编写我们的第一个 Hadoop MapReduce 作业”一文中进行了介绍,没有更多要提到的了。我们所做的一件事是将我们需要的 JAR 文件组装成一个大的 JAR 文件来执行我们的 MapReduce 作业。这种方法对于我们的小型 MapReduce 作业来说是可以的;对于需要大量第三方 JAR 文件的大型作业,我们将不得不采用一种方法,将 JAR 文件添加到 Hadoop 安装的lib目录中,并以与我们在本地执行的 MapReduce 作业相同的方式执行。我们与本地设置不同的另一件事是不使用mongid实例来获取数据和写入数据,而是使用 mongo 数据库中的 BSON 转储文件作为输入,并将输出写入 BSON 文件。然后将输出转储导入到本地 mongo 数据库,并对结果进行分析。将数据转储上传到 S3 存储桶并在云上使用云基础设施对已上传到 S3 的数据运行分析作业是一个不错的选择。EMR 集群从存储桶访问的数据不需要公共访问权限,因为 EMR 作业使用我们账户的凭据运行;我们可以访问我们自己的存储桶来读取和写入数据/日志。
另请参阅
尝试了这个简单的 MapReduce 作业之后,强烈建议您了解亚马逊 EMR 服务及其所有功能。EMR 的开发人员指南可以在docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/找到。
Enron 数据集中提供了 mongo-hadoop 连接器示例中的一个 MapReduce 作业。它可以在github.com/mongodb/mongo-hadoop/tree/master/examples/elastic-mapreduce找到。您也可以选择根据给定的说明在亚马逊 EMR 上实现此示例。
第九章:开源和专有工具
在本章中,我们将涵盖一些开源和专有工具。以下是本章中将要介绍的配方:
-
使用 spring-data-mongodb 进行开发
-
使用 JPA 访问 MongoDB
-
通过 REST 访问 MongoDB
-
为 MongoDB 安装基于 GUI 的客户端 MongoVUE
介绍
有大量的工具/框架可用于简化使用 MongoDB 的软件的开发/管理过程。我们将看一些这些可用的框架和工具。对于开发人员的生产力(在这种情况下是 Java 开发人员),我们将看一下 spring-data-mongodb,它是流行的 spring data 套件的一部分。
JPA 是一个广泛使用的 ORM 规范,特别是与关系数据库一起使用。(这是 ORM 框架的目标。)然而,有一些实现让我们可以将其与 NoSQL 存储(在这种情况下是 MongoDB)一起使用。我们将看一个提供这种实现的提供者,并用一个简单的用例来测试它。
我们将使用 spring-data-rest 来为客户端公开 MongoDB 的 CRUD 存储库,以便客户端调用底层 spring-data-mongo 存储库支持的各种操作。
在 shell 中查询数据库是可以的,但最好有一个良好的 GUI,使我们能够从 GUI 中执行所有与管理/开发相关的任务,而不是在 shell 中执行命令来执行这些活动。我们将在本章中看一个这样的工具。
使用 spring-data-mongodb 进行开发
从开发人员的角度来看,当程序需要与 MongoDB 实例交互时,他们需要使用特定平台的相应客户端 API。这样做的麻烦在于我们需要编写大量的样板代码,而且不一定是面向对象的。例如,我们有一个名为Person的类,具有各种属性,如name、age、address等。相应的 JSON 文档与这个person类的结构类似。
{
name:"…",
age:..,
address:{lineOne:"…", …}
}
然而,为了存储这个文档,我们需要将Person类转换为 DBObject,这是一个具有键值对的映射。真正需要的是让我们将这个Person类本身作为一个对象持久化到数据库中,而不必将其转换为 DBObject。
此外,一些操作,如按文档的特定字段搜索、保存实体、删除实体、按 ID 搜索等,都是非常常见的操作,我们往往会反复编写类似的样板代码。在这个配方中,我们将看到 spring-data-mongodb 如何解除我们这些繁琐和繁重的任务,以减少不仅开发工作量,还减少引入这些常见写函数中的错误的可能性。
准备工作
SpringDataMongoTest项目,存在于本章的捆绑包中,是一个 Maven 项目,必须导入到您选择的任何 IDE 中。所需的 maven 构件将自动下载。需要一个单独的 MongoDB 实例正在运行并监听端口27017。有关如何启动独立实例的说明,请参阅第一章中的安装单节点 MongoDB配方,安装和启动服务器。
对于聚合示例,我们将使用邮政编码数据。有关如何创建测试数据,请参阅第二章中的创建测试数据配方,命令行操作和索引。
如何做…
-
我们将首先探索 spring-data-mongodb 的存储库功能。从您的 IDE 中打开测试用例的
com.packtpub.mongo.cookbook.MongoCrudRepositoryTest类并执行它。如果一切顺利,MongoDB 服务器实例是可达的,测试用例将成功执行。 -
另一个测试用例
com.packtpub.mongo.cookbook.MongoCrudRepositoryTest2,用于探索 spring-data-mongodb 提供的存储库支持的更多功能。这个测试用例也应该成功执行。 -
我们将看到如何使用 spring-data-mongodb 的
MongoTemplate执行 CRUD 操作和其他常见操作。打开com.packtpub.mongo.cookbook.MongoTemplateTest类并执行它。 -
或者,如果不使用 IDE,可以在命令提示符中使用 maven 执行所有测试,当前目录在
SpringDataMongoTest项目的根目录中:
$ mvn clean test
它是如何工作的...
我们首先看一下在com.packtpub.mongo.cookbook.MongoCrudRepositoryTest中做了什么,我们在那里看到了 spring-data-mongodb 提供的存储库支持。以防你没有注意到,我们没有为存储库编写一行代码。实现所需代码的魔力是由 spring data 项目完成的。
让我们首先看一下 XML 配置文件的相关部分:
<mongo:repositories base-package="com.packtpub.mongo.cookbook" />
<mongo:mongo id="mongo" host="localhost" port="27017"/>
<mongo:db-factory id="factory" dbname="test" mongo-ref="mongo"/>
<mongo:template id="mongoTemplate" db-factory-ref="factory"/>
我们首先看一下最后三行,这些是 spring-data-mongodb 命名空间声明,用于实例化com.mongodb.Mongo,客户端的com.mongodb.DB实例的工厂,以及template实例,用于在 MongoDB 上执行各种操作。稍后我们将更详细地看一下org.springframework.data.mongodb.core.MongoTemplate。
第一行是所有 CRUD 存储库的基本包的命名空间声明。在这个包中,我们有一个接口,具有以下内容:
public interface PersonRepository extends PagingAndSortingRepository<Person, Integer>{
/**
*
* @param lastName
* @return
*/
Person findByLastName(String lastName);
}
PagingAndSortingRepository接口来自 spring data 核心项目的org.springframework.data.repository包,并在同一项目中扩展自CrudRepository。这些接口为我们提供了一些最常见的方法,例如按 ID/主键搜索、删除实体以及插入和更新实体。存储库需要一个对象,它将其映射到底层数据存储。spring data 项目支持大量的数据存储,不仅限于 SQL(使用 JDBC 和 JPA)或 MongoDB,还包括其他 NoSQL 存储,如 Redis 和 Hadoop,以及 Solr 和 Elasticsearch 等搜索引擎。在 spring-data-mongodb 的情况下,对象被映射到集合中的文档。
PagingAndSortingRepository<Person, Integer>的签名表示第一个是 CRUD 存储库构建的实体,第二个是主键/ID 字段的类型。
我们只添加了一个findByLastName方法,它接受一个字符串值作为姓氏的参数。这是一个特定于我们的存储库的有趣操作,甚至不是我们实现的,但它仍然会按预期工作。Person 是一个 POJO,我们用org.springframework.data.annotation.Id注解标记了id字段。这个类没有什么特别之处;它只有一些普通的 getter 和 setter。
有了所有这些细节,让我们通过回答一些你心中的问题来把这些点连接起来。首先,我们将看到我们的数据去了哪个服务器、数据库和集合。如果我们查看配置文件的 XML 定义,mongo:mongo,我们可以看到我们通过连接到 localhost 和端口27017来实例化com.mongodb.Mongo类。mongo:db-factory声明用于表示要使用的数据库是test。最后一个问题是:哪个集合?我们类的简单名称是Person。集合的名称是简单名称的第一个字符小写,因此Person对应到person,而BillingAddress之类的东西将对应到billingAddress集合。这些是默认值。但是,如果您需要覆盖此值,可以使用org.springframework.data.mongodb.core.mapping.Document注解注释您的类,并使用其 collection 属性来给出您选择的任何名称,正如我们将在后面的示例中看到的。
查看集合中的文档,只需执行com.packtpub.mongo.cookbook.MongoCrudRepositoryTest类中的一个测试用例saveAndQueryPerson方法。现在,连接到 mongo shell 中的 MongoDB 实例并执行以下查询:
> use test
> db.person.findOne({_id:1})
{
"_id" : 1,
"_class" : "com.packtpub.mongo.cookbook.domain.Person",
"firstName" : "Steve",
"lastName" : "Johnson",
"age" : 20,
"gender" : "Male"
…
}
正如我们在前面的结果中所看到的,文档的内容与我们使用 CRUD 存储库持久化的对象相似。文档中字段的名称与 Java 对象中相应属性的名称相同,有两个例外。使用@Id注释的字段现在是_id,与 Java 类中字段的名称无关,并且在文档中添加了一个额外的_class属性,其值是 Java 类本身的完全限定名称。这对应用程序没有任何用处,但是 spring-data-mongodb 用作元数据。
现在更有意义了,并且让我们了解 spring-data-mongodb 必须为所有基本的 CRUD 方法做些什么。我们执行的所有操作都将使用 spring-data-mongodb 项目中的MongoTemplate(MongoOperations,这是MongoTemplate实现的接口)类。它将使用主键,在使用Person实体类派生的集合上的_id字段上调用 find。save方法简单地调用MongoOperations上的save方法,而MongoOperations又调用com.mongodb.DBCollection类上的save方法。
我们仍然没有回答findByLastName方法是如何工作的。spring 如何知道要调用什么查询以返回数据?这些是以find、findBy、get或getBy开头的特殊类型的方法。在命名方法时需要遵循一些规则,存储库接口上的代理对象能够正确地将此方法转换为集合上的适当查询。例如,Person类的存储库中的findByLastName方法将在 person 文档的lastName字段上执行查询。因此,findByLastName(String lastName)方法将在数据库上触发db.person.find({'lastName': lastName })查询。根据方法定义的返回类型,它将返回来自数据库的结果中的List或第一个结果。我们在我们的查询中使用了findBy,但是任何以find开头,中间有任何文本,并以By结尾的都可以工作。例如,findPersonBy也与findBy相同。
要了解更多关于这些findBy方法,我们有另一个测试MongoCrudRepositoryTest2类。在您的 IDE 中打开这个类,可以与本文一起阅读。我们已经执行了这个测试用例;现在,让我们看看这些findBy方法的使用和它们的行为。这个接口中有七个findBy方法,其中一个方法是同一接口中另一个方法的变体。为了清楚地了解查询,我们将首先查看测试数据库中personTwo集合中的一个文档。在连接到运行在 localhost 上的 MongoDB 服务器的 mongo shell 中执行以下操作:
> use test
> db.personTwo.findOne({firstName:'Amit'})
{
"_id" : 2,
"_class" : "com.packtpub.mongo.cookbook.domain.Person2",
"firstName" : "Amit",
"lastName" : "Sharma",
"age" : 25,
"gender" : "Male",
"residentialAddress" : {
"addressLineOne" : "20, Central street",
"city" : "Mumbai",
"state" : "Maharashtra",
"country" : "India",
"zip" : "400101"
}
}
请注意,存储库使用Person2类;但是使用的集合的名称是personTwo。这是可能的,因为我们在Person2类的顶部使用了@Document(collection="personTwo")注解。
回到com.packtpub.mongo.cookbook.PersonRepositoryTwo存储库类中的七种方法,让我们逐一看看它们:
| 方法 | 描述 |
|---|---|
findByAgeGreaterThanEqual |
这个方法将在personTwo集合上触发一个查询,{'age':{'$gte':<age>}}。秘密在于方法的名称。如果我们把它分开,findBy后面告诉我们我们想要什么。age属性(首字母小写)是将在具有$gte运算符的文档上查询的字段,因为方法的名称中有GreaterThanEqual。用于比较的值将是传递的参数的值。结果是Person2实体的集合,因为我们会有多个匹配项。 |
findByAgeBetween |
这个方法将再次在年龄上进行查询,但将使用$gt和$lt的组合来找到匹配的结果。在这种情况下,查询将是{'age' : {'$gt' : from, '$lt' : to}}。重要的是要注意 from 和 to 两个值在范围内都是排他的。测试用例中有两种方法,findByAgeBetween和findByAgeBetween2。这些方法展示了对不同输入值的 between 查询的行为。 |
findByAgeGreaterThan |
这个方法是一个特殊的方法,它还会对结果进行排序,因为该方法有两个参数:第一个参数是年龄将要进行比较的值,第二个参数是org.springframework.data.domain.Sort类型的字段。有关更多详细信息,请参考 spring-data-mongodb 的 Javadocs。 |
findPeopleByLastNameLike |
这个方法用于通过匹配模式查找姓氏匹配的结果。用于匹配目的的是正则表达式。例如,在这种情况下,触发的查询将是{'lastName' : <lastName as regex>}。这个方法的名称以findPeopleBy开头,而不是findBy,它的工作方式与findBy相同。因此,当我们在所有描述中说findBy时,实际上是指find…By。提供的值作为参数将用于匹配姓氏。 |
findByResidentialAddressCountry |
这是一个有趣的方法。在这里,我们通过居住地址的国家进行搜索。实际上,这是Person类中residentialAddress字段中的Address类中的一个字段。查看personTwo集合中的文档,以了解查询应该是什么样子。当 spring data 找到名称为ResidentialAddressCountry时,它将尝试使用此字符串找到各种组合。例如,它可以查看Person类中的residentialAddressCountry字段,或者residential.addressCountry,residentialAddress.country或residential.address.country。如果没有冲突的值,如我们的情况下的residentialAddress。字段'country'是'Person2'文档的一部分,因此将在查询中使用。但是,如果存在冲突,则可以使用下划线来清楚地指定我们要查看的内容。在这种情况下,方法可以重命名为findByResidentialAddress_country,以清楚地指定我们期望的结果。测试用例findByCountry2方法演示了这一点。 |
findByFirstNameAndCountry |
这是一个有趣的方法。我们并不总是能够使用方法名来实现我们实际想要的功能。为了让 spring 自动实现查询,方法的名称可能会有点难以使用。例如,findByCountryOfResidence听起来比findByResidentialAddressCountry更好。然而,我们只能使用后者,因为这是 spring-data-mongodb 构造查询的方式。使用findByCountryOfResidence并没有提供如何构造查询给 spring data 的细节。但是,有一个解决方法。您可以选择使用@Query注解,并在方法调用时指定要执行的查询。以下是我们使用的注解:@Query("{'firstName':?0, 'residentialAddress.country': ?1}")我们将值写成一个将被执行并将函数的参数绑定到查询的查询,作为从零开始的编号参数。因此,方法的第一个参数将绑定到?0,第二个参数将绑定到?1,依此类推。 |
我们看到了findBy或getBy方法如何自动转换为 MongoDB 的查询。同样,我们有以下方法的前缀。countBy方法返回给定条件的长数字,该条件是从方法名称的其余部分派生的,类似于findBy。我们可以使用deleteBy或removeBy来根据派生条件删除文档。关于com.packtpub.mongo.cookbook.domain.Person2类的一点需要注意的是,它没有无参数构造函数或设置器来设置值。相反,spring 将使用反射来实例化此对象。
spring-data-mongodb 支持许多findBy方法,这里并未涵盖所有。有关更多详细信息,请参阅 spring-data-mongodb 参考手册。参考手册中提供了许多基于 XML 或 Java 的配置选项。这些 URL 将在本食谱的参见部分中提供。
我们还没有完成;我们还有另一个测试用例com.packtpub.mongo.cookbook.MongoTemplateTest,它使用org.springframework.data.mongodb.core.MongoTemplate执行各种操作。您可以打开测试用例类,看看执行了哪些操作以及调用了 MongoTemplate 的哪些方法。
让我们来看看 MongoTemplate 类的一些重要和经常使用的方法:
| 方法 | 描述 |
|---|---|
save |
该方法用于在 MongoDB 中保存(如果是新的则插入;否则更新)实体。该方法接受一个参数,即实体,并根据其名称或@Document注解找到目标集合。save 方法有一个重载版本,还接受第二个参数,即需要将数据实体持久化到的集合的名称。 |
remove |
这个方法用于从集合中删除文档。在这个类中有一些重载的方法。所有这些方法都接受要删除的实体或org.springframework.data.mongodb.core.query.Query实例,用于确定要删除的文档。第二个参数是要从中删除文档的集合的名称。当提供实体时,可以推导出集合的名称。如果提供了Query实例,我们必须给出集合的名称或实体类的名称,然后将用于推导集合的名称。 |
updateMulti |
这是用于一次更新多个文档的函数。第一个参数是用于匹配文档的查询。第二个参数是org.springframework.data.mongodb.core.query.Updat e实例。这是将在使用第一个Query对象选择的文档上执行的更新。下一个参数是实体类或集合名称,用于执行更新。有关该方法及其各种重载版本的更多详细信息,请参阅 Javadocs。 |
updateFirst |
这是updateMulti方法的相反操作。此操作将仅更新第一个匹配的文档。我们在单元测试用例中没有涵盖这个方法。 |
insert |
我们提到 save 方法可以执行插入和更新。模板中的 insert 方法调用底层 mongo 客户端的 insert 方法。如果要插入一个实体或文档,调用 insert 或 save 方法没有区别。然而,如我们在测试用例中看到的 insertMultiple 方法,我们创建了一个包含三个Person实例的列表,并将它们传递给 insert 方法。三个Person实例的所有三个文档将作为一个调用的一部分发送到服务器。无论何时插入失败的行为是由 Write Concern 的 continue on error 参数确定的。它将确定批量插入在第一次失败时是否失败,或者即使在报告最后一个错误时也会继续。URL docs.mongodb.org/manual/core/bulk-inserts/ 提供了有关批量插入和各种写关注参数的更多详细信息,可以改变行为。 |
findAndRemove/findAllAndRemove |
这两个操作都用于查找然后删除文档。第一个找到一个文档,然后返回被删除的文档。这个操作是原子的。然而,后者在返回所有被删除文档的实体列表之前找到并删除所有文档。 |
findAndModify |
这个方法在功能上类似于我们在 mongo 客户端库中拥有的findAndModify。它将原子地查找并修改文档。如果查询匹配多个文档,只有第一个匹配项将被更新。该方法的前两个参数是要执行的查询和更新。接下来的几个参数是要在其上执行操作的实体类或集合名称。此外,还有一个特殊的org.springframework.data.mongodb.core.FindAndModifyOptions类,它只对findAndModify操作有意义。这个实例告诉我们在操作执行后是否要查找新实例或旧实例,以及是否要执行 upsert。只有在不存在与匹配查询的文档时才相关。还有一个额外的布尔标志,告诉客户端这是否是一个findAndRemove操作。实际上,我们之前看到的findAndRemove操作只是一个方便的函数,它使用了这个删除标志来委托findAndModify。 |
在前面的表中,当谈到更新时,我们提到了Query和Update类。这些是 spring-data-mongodb 中的特殊便捷类,它们让我们使用易于理解且具有改进可读性的语法构建 MongoDB 查询。例如,在 mongo 中检查lastName是否为Johnson的查询是{'lastName':'Johnson'}。在 spring-data-mongodb 中,可以按照以下方式构建相同的查询:
new Query(Criteria.where("lastName").is("Johnson"))
与以 JSON 形式给出查询相比,这种语法看起来更整洁。让我们举另一个例子,我们想要在我们的数据库中找到所有 30 岁以下的女性。现在查询将构建如下:
new Query(Criteria.where("age").lt(30).and("gender").is("Female"))
同样,对于更新,我们希望根据一些条件为一些客户设置一个布尔标志youngCustomer为true。要在文档中设置此标志,MongoDB 格式如下:
{'$set' : {'youngCustomer' : true}}
在 spring-data-mongodb 中,可以通过以下方式实现:
new Update().set("youngCustomer", true)
请参考 Javadocs,了解在 spring-data-mongodb 中可用于构建查询和更新的所有可能方法。
这些方法绝不是MongoTemplate类中唯一可用的方法。还有许多其他方法用于地理空间索引、获取集合中文档数量的便捷方法、聚合和 MapReduce 支持等。有关更多详细信息和方法,请参考MongoTemplate的 Javadocs。
说到聚合,我们还有一个名为aggregationTest的测试用例方法,用于对集合执行聚合操作。我们在 MongoDB 中有一个postalCodes集合,其中包含各个城市的邮政编码详细信息。集合中的一个示例文档如下:
{
"_id" : ObjectId("539743b26412fd18f3510f1b"),
"postOfficeName" : "A S D Mello Road Fuller Marg",
"pincode" : 400001,
"districtsName" : "Mumbai",
"city" : "Mumbai",
"state" : "Maharashtra"
}
我们的聚合操作意图是找到集合中文档数量前五名的州。在 mongo 中,聚合管道如下所示:
[
{'$project':{'state':1, '_id':0}},
{'$group':{'_id':'$state', 'count':{'$sum':1}}}
{'$sort':{'count':-1}},
{'$limit':5}
]
在 spring-data-mongodb 中,我们使用MongoTemplate调用了聚合操作:
Aggregation aggregation = newAggregation(
project("state", "_id"),
group("state").count().as("count"),
sort(Direction.DESC, "count"),
limit(5)
);
AggregationResults<DBObject> results = mongoTemplate.aggregate(
aggregation,
"postalCodes",
DBObject.class);
关键在于创建org.springframework.data.mongodb.core.aggregation.Aggregation类的实例。newAggregation方法是从同一类中静态导入的,并接受varargs,用于不同的org.springframework.data.mongodb.core.aggregation.AggregationOperation实例,对应于链中的一个操作。Aggregation类有各种静态方法来创建AggregationOperation的实例。我们使用了其中一些,比如project、group、sort和limit。有关更多详细信息和可用方法,请参考 Javadocs。MongoTemplate中的aggregate方法接受三个参数。第一个是Aggregation类的实例,第二个是集合的名称,第三个是聚合结果的返回类型。有关更多详细信息,请参考聚合操作测试用例。
另请参阅
-
有关更多详细信息和 API 文档,请参考
docs.spring.io/spring-data/mongodb/docs/current/api/的 Javadocs。 -
spring-data-mongodb 项目的参考手册可以在
docs.spring.io/spring-data/data-mongodb/docs/current/reference/找到
使用 JPA 访问 MongoDB
在这个示例中,我们将使用一个 JPA 提供程序,它允许我们使用 JPA 实体来实现与 MongoDB 的对象到文档映射。
准备工作
启动独立的服务器实例,监听端口27017。这是一个使用 JPA 的 Java 项目。我们期望熟悉 JPA 及其注解,尽管我们将要查看的内容相当基础。如果您不熟悉 maven,可以参考第一章中的使用 Java 客户端连接单节点部分来设置 maven。从提供的捆绑包中下载DataNucleusMongoJPA项目。虽然我们将从命令提示符中执行测试用例,但您也可以将项目导入到您喜欢的 IDE 中查看源代码。
如何做…
- 转到
DataNucleusMongoJPA项目的根目录,并在 shell 中执行以下操作:
$ mvn clean test
-
这应该会下载构建和运行项目所需的必要工件,并成功执行测试用例。
-
一旦测试用例执行完毕,打开 mongo shell 并连接到本地实例。
-
在 shell 中执行以下查询:
> use test
> db.personJPA.find().pretty()
工作原理…
首先,让我们看一下在personJPA集合中创建的示例文档:
{
"_id" : NumberLong(2),
"residentialAddress" : {
"residentialAddress_zipCode" : "400101",
"residentialAddress_state" : "Maharashtra",
"residentialAddress_country" : "India",
"residentialAddress_city" : "Mumbai",
"residentialAddress_addressLineOne" : "20, Central street"
},
"lastName" : "Sharma",
"gender" : "Male",
"firstName" : "Amit",
"age" : 25
}
我们执行的步骤非常简单;让我们逐个查看使用的类。我们从com.packtpub.mongo.cookbook.domain.Person类开始。在类的顶部(包和导入之后),我们有以下内容:
@Entity
@Table(name="personJPA")
public class Person {
这表示Person类是一个实体,它将持久化到personJPA集合中。请注意,JPA 主要设计为对象关系映射(ORM)工具,因此使用的术语更多地是针对关系数据库。在 RDBMS 中,表与 MongoDB 中的集合是同义词。类的其余部分包含了人的属性,以及用@Column和@Id注释的列作为主键。这些都是简单的 JPA 注释。有趣的是看一下com.packtpub.mongo.cookbook.domain.ResidentialAddress类,它存储为Person类中的residentialAddress变量。如果我们看一下之前给出的人员文档,@Column注释中给出的所有值都是人员键的名称;还要注意Enum如何转换为字符串值。residentialAddress字段是Person类中的变量名,存储地址实例。如果我们看ResidentialAddress类,我们可以看到类名上方的@Embeddable注解。这再次是一个 JPA 注解,表示这个实例本身不是一个实体,而是嵌入在另一个Entity或Embeddable类中。请注意文档中字段的名称;在这种情况下,它们的格式如下:<person 类中的变量名>_<ResidentialAddress 类中的变量名的值>。
这里有一个问题。字段的名称太长,占用了不必要的空间。解决方案是在@Column注解中使用较短的值。例如,@Column(name="ln")注解代替@Column(name="lastName"),将在文档中创建一个名为ln的键。不幸的是,这在嵌入的ResidentialAddress类中不起作用;在这种情况下,您将不得不处理较短的变量名。现在我们已经看到了实体类,让我们看看persistence.xml:
<persistence-unit name="DataNucleusMongo">
<class>com.packtpub.mongo.cookbook.domain.Person</class>
<properties>
<property name="javax.persistence.jdbc.url" value="mongodb:localhost:27017/test"/>
</properties>
</persistence-unit>
这里只有一个名为DataNucleusMongo的持久性单元定义。有一个类节点,即我们将使用的实体。请注意,嵌入式地址类在这里没有提到,因为它不是一个独立的实体。在属性中,我们提到了要连接的数据存储的 URL。在这种情况下,我们连接到本地主机上的实例,端口27017,数据库为 test。
现在,让我们看一下查询和插入数据的类。这是我们的com.packtpub.mongo.cookbook.DataNucleusJPATest测试类。我们创建javax.persistence.EntityManagerFactory作为Persistence.createEntityManagerFactory("DataNucleusMongo")。这是一个线程安全的类,其实例在线程之间共享;字符串参数也与我们在persistence.xml中使用的持久化单元的名称相同。对javax.persistence.EntityManager的所有其他调用,以持久化或查询集合,都要求我们使用EntityManagerFactory创建一个实例——使用它,然后在操作完成后关闭它。所有执行的操作都符合 JPA 规范。测试用例类持久化实体并查询它们。
最后,让我们看一下pom.xml,特别是我们使用的增强器插件,如下所示:
<plugin>
<groupId>org.datanucleus</groupId>
<artifactId>datanucleus-maven-plugin</artifactId>
<version>4.0.0-release</version>
<configuration>
<log4jConfiguration>${basedir}/src/main/resources/log4j.properties</log4jConfiguration>
<verbose>true</verbose>
</configuration>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
我们编写的实体需要增强才能作为 JPA 实体使用数据核。前面的插件将附加到 process-class 阶段,然后调用插件的增强。
另请参阅
-
有多种方法可以使用数据核增强器增强 JPA 实体。请参考
www.datanucleus.org/products/datanucleus/jdo/enhancer.html以获取可能的选项。甚至有一个 Eclipse 插件,允许实体类被增强/仪器化以供数据核使用。 -
JPA 2.1 规范可以在
www.jcp.org/aboutJava/communityprocess/final/jsr338/index.html找到。
通过 REST 访问 MongoDB
在这个示例中,我们将看到如何使用 REST API 访问 MongoDB 并执行 CRUD 操作。我们将使用 spring-data-rest 进行 REST 访问,使用 spring-data-mongodb 执行 CRUD 操作。在继续进行这个示例之前,重要的是要知道如何使用 spring-data-mongodb 实现 CRUD 存储库。请参考本章中的使用 spring-data-mongodb 进行开发示例,了解如何使用这个框架。
一个人必须要问的问题是,为什么需要 REST API?有些情况下,有一个数据库被许多应用程序共享,并且可能是用不同的语言编写的。编写 JPA DAO 或使用 spring-data-mongodb 对于 Java 客户端来说已经足够好了,但对于其他语言的客户端来说就不够了。在应用程序本地拥有 API 甚至不能给我们一个集中访问数据库的方式。这就是 REST API 发挥作用的地方。我们可以在 Java 中开发服务器端数据访问层和 CRUD 存储库——具体来说是 spring-data-mongodb,然后通过 REST 接口将其暴露给任何语言编写的客户端来调用它们。我们不仅以平台无关的方式调用我们的 API,还提供了一个进入我们数据库的单一入口。
准备就绪
除了 spring-data-mongodb 示例的先决条件之外,这个示例还有一些其他要求。首先是从 Packt 网站下载SpringDataRestTest项目,并将其作为 maven 项目导入到您的 IDE 中。或者,如果您不希望导入到 IDE 中,您可以从命令提示符中运行服务请求,我们将在下一节中看到。没有特定的客户端应用程序用于通过 REST 执行 CRUD 操作。我将使用 Chrome 浏览器和 Advanced REST Client 浏览器的特殊插件来演示这些概念,以向服务器发送 HTTP POST 请求。这些工具可以在 Chrome 网络商店的开发者工具部分找到。
操作步骤...
-
如果您已将项目作为 maven 项目导入 IDE,请执行
com.packtpub.mongo.cookbook.rest.RestServer类,这是引导类,启动本地服务器,接受客户端连接。 -
如果要从命令提示符中作为 maven 项目执行该项目,转到项目的根目录并运行以下命令:
mvn spring-boot:run
- 如果一切顺利,服务器已经启动,命令提示符上将看到以下行:
[INFO] Attaching agents: []
- 无论以何种方式启动服务器,都在浏览器的地址栏中输入
http://localhost:8080/people,我们应该看到以下 JSON 响应。因为底层的人员集合是空的,所以会看到这个响应。
{
"_links" : {
"self" : {
"href" : "http://localhost:8080/people{?page,size,sort}",
"templated" : true
},
"search" : {
"href" : "http://localhost:8080/people/search"
}
},
"page" : {
"size" : 20,
"totalElements" : 0,
"totalPages" : 0,
"number" : 0
}
}
- 我们现在将使用 HTTP POST 请求将一个新文档插入到人员集合中,请求将被发送到
http://localhost:8080/people。我们将使用 Chrome 浏览器的 Advanced REST Client 扩展来向服务器发送 POST 请求。发送的文档是:
{"lastName":"Cruise", "firstName":"Tom", "age":52, "id":1}.
请求的内容类型是application/json。
以下图片显示了发送到服务器的 POST 请求和服务器的响应:

-
现在,我们将使用浏览器中的
_id字段来查询这个文档,这个字段在这种情况下是1。在浏览器的地址栏中输入http://localhost:8080/people/1。您应该看到我们在步骤 3 中插入的文档。 -
现在我们在集合中有一个文档了(您可以尝试为具有不同名称和更重要的是唯一 ID 的人插入更多文档),我们将使用姓氏查询文档。首先,在浏览器的地址栏中输入以下 URL 以查看所有可用的搜索选项:
http://localhost:8080/people/search。我们应该看到一个search方法,findByLastName,它接受一个命令行参数lastName。 -
要按姓氏搜索,我们的情况下是 Cruise,可以在浏览器的地址栏中输入以下 URL:
http://localhost:8080/people/search/findByLastName?lastName=Cruise。 -
现在我们将更新 ID 为
1的人的姓氏和年龄,目前是汤姆·克鲁斯。让我们把姓氏更新为汉克斯,年龄更新为58。为此,我们将使用 HTTP PATCH 请求,并且请求将被发送到http://localhost:8080/people/1,这个地址唯一标识了要更新的文档。HTTP PATCH 请求的主体是{"lastName":"Hanks", "age":58}。参考以下图片,查看我们发送的更新请求:![操作步骤…]()
-
为了验证我们的更新是否成功(我们知道它成功了,因为在 PATCH 请求之后我们得到了一个响应状态 204),再次在浏览器的地址栏中输入
http://localhost:8080/people/1。 -
最后,我们删除文档。这很简单,我们只需向
http://localhost:8080/people/1发送一个 DELETE 请求。一旦 DELETE 请求成功,从浏览器向http://localhost:8080/people/1发送一个 HTTP GET 请求,我们不应该得到任何文档作为返回。
工作原理…
我们不会在这个教程中再次重复 spring-data-mongodb 的概念,而是将看一些我们专门为 REST 接口添加的注释。第一个是在类名的顶部,如下所示:
@RepositoryRestResource(path="people")
public interface PersonRepository extends PagingAndSortingRepository<Person, Integer> {
这用于指示服务器可以使用 people 资源访问此 CRUD 存储库。这就是为什么我们总是在http://localhost:8080/people/上进行 HTTP GET 和 POST 请求的原因。
第二个注释在findByLastName方法中。我们有以下方法签名:
Person findByLastName(@Param("lastName") String lastName);
这里,方法的lastName参数使用了@Param注释,用于注释将在调用存储库上的此方法时传递的lastName参数的参数名称。如果我们看一下上一节的第 6 步,我们可以看到使用 HTTP GET 请求调用了findByLastName,并且 URL 的lastName参数的值被用作在调用存储库方法时传递的字符串值。
我们的示例非常简单,只使用一个参数进行搜索操作。我们可以为存储库方法使用多个参数,并在 HTTP 请求中使用相同数量的参数,这些参数将映射到存储库上的方法,以便调用 CRUD 存储库。对于某些类型,例如要发送的日期,请使用@DateTimeFormat注释,该注释将用于指定日期和时间格式。有关此注释及其用法的更多信息,请参阅 spring Javadocs docs.spring.io/spring/docs/current/javadoc-api/
这就是我们向 REST 接口发出的 GET 请求,以查询和搜索数据。我们最初通过向服务器发送 HTTP POST 请求来创建文档数据。要创建新文档,我们将始终发送 POST 请求,将要创建的文档作为请求的主体发送到标识 REST 端点的 URL,即http://localhost:8080/people/。发送到此集合的所有文档都将使用PersonRepository来持久化Person在相应的集合中。
我们的最后两个步骤是更新人员和删除人员。执行这些操作的 HTTP 请求类型分别为 PATCH 和 DELETE。在第 7 步中,我们更新了人员 Tom Cruise 的文档,并更新了他的姓和年龄。为了实现这一点,我们的 PATCH 请求被发送到标识特定人员实例的 URL,即http://localhost:8080/people/1。请注意,在创建新人员的情况下,我们的 POST 请求总是发送到http://localhost:8080/people,而不是发送到 PATCH 和 DELETE 请求,其中我们将 HTTP 请求发送到表示要更新或删除的特定人员的 URL。在更新的情况下,PATCH 请求的主体是 JSON,其提供的字段将替换目标文档中的相应字段以进行更新。所有其他字段将保持不变。在我们的情况下,目标文档的lastName和年龄被更新,而firstName保持不变。在删除的情况下,消息主体不为空,并且 DELETE 请求本身指示应删除发送请求的目标。
您还可以发送 PUT 请求,而不是 PATCH 请求到标识特定人员的 URL;在这种情况下,集合中的整个文档将被更新或替换为作为 PUT 请求的一部分提供的文档。
另请参阅
spring-data-rest 的主页位于projects.spring.io/spring-data-rest/,您可以在那里找到其 Git 存储库、参考手册和 Javadocs URL 的链接。
安装基于 GUI 的 MongoDB 客户端 MongoVUE
在这个示例中,我们将看到一个基于 GUI 的 MongoDB 客户端。在整本书中,我们一直使用 mongo shell 来执行我们需要的各种操作。它的优点如下:
-
它与 MongoDB 安装一起打包
-
由于轻量级,您不必担心它占用系统资源
-
在没有基于 GUI 的界面的服务器上,shell 是连接、查询和管理服务器实例的唯一选项
话虽如此,如果您不在服务器上并且想要连接到数据库实例进行查询、查看查询计划、管理等操作,最好有一个具有这些功能的 GUI,让您可以轻松完成任务。作为开发人员,我们总是使用基于 GUI 的厚客户端查询我们的关系数据库,那么为什么不为 MongoDB 呢?
在这个示例中,我们将看到如何安装 MongoDB 客户端 MongoVUE 的一些功能。该客户端仅适用于 Windows 机器。该产品既有付费版本(根据用户数量的不同级别进行许可),也有一些限制的免费版本。在这个示例中,我们将看看免费版本。
准备工作
对于这个示例,以下步骤是必要的:
-
启动 MongoDB 服务器的单个实例。接受连接的端口将是默认端口
27017。 -
在 mongod 服务器启动后,从命令提示符导入以下两个集合:
$ mongoimport --type json personTwo.json -c personTwo -d test –drop
$ mongoimport --type csv -c postalCodes -d test pincodes.csv --headerline –drop
如何操作...
-
从
www.mongovue.com/downloads/下载 MongoVUE 的安装程序 ZIP。下载后,只需点击几下,软件就会安装好。 -
打开安装的应用程序;由于这是免费版本,在前 14 天内我们将拥有所有功能,之后,一些功能将不可用。详情请参见
www.mongovue.com/purchase/。 -
我们要做的第一件事是添加数据库连接:
-
一旦打开以下窗口,点击(+)按钮添加新连接:
![如何操作...]()
-
打开后,我们将得到另一个窗口,在其中填写服务器连接详细信息。在新窗口中填写以下详细信息,然后单击测试。如果连接正常,这应该成功;最后,单击保存。
![如何操作...]()
-
添加后,连接到实例。
- 在左侧导航面板中,我们将看到添加的实例和其中的数据库,如下图所示:
![如何操作...]()
正如我们在上图中所看到的,将鼠标悬停在集合名称上会显示集合中的文档大小和计数。
-
让我们看看如何查询一个集合并获取所有文档。我们将使用
test中的postalCodes集合。右键单击集合名称,然后单击查看。我们将看到集合的内容显示为树形视图,我们可以展开并查看内容,表格视图,以表格网格显示内容,以及文本视图,以普通 JSON 文本显示内容。 -
让我们看看当我们查询具有嵌套文档的集合时会发生什么;
personTwo是一个具有以下示例文档的集合:
{
"_id" : 1,
"_class" : "com.packtpub.mongo.cookbook.domain.Person2",
"firstName" : "Steve",
"lastName" : "Johnson",
"age" : 30,
"gender" : "Male",
"residentialAddress" : {
"addressLineOne" : "20, Central street",
"city" : "Sydney",
"state" : "NSW",
"country" : "Australia"
}
}
当我们查询以查看集合中的所有文档时,我们会看到以下图像:

residentialAddress列显示值为嵌套文档,并显示其中的字段数。将鼠标悬停在上面会显示嵌套文档;或者,您可以单击该列以再次以网格形式显示此文档中的内容。显示嵌套文档后,您可以单击网格顶部返回一级。
- 让我们看看如何编写查询以检索所选文档:
-
右键单击postalCodes集合,然后单击查找。我们将在{查找}文本框和{排序}字段中输入以下查询,然后单击右侧的查找按钮:
![如何操作...]()
-
我们可以从选项卡中选择所需的视图类型,包括树形视图、表格视图或文本视图。查询计划也会显示。每次运行任何操作时,底部的 Learn shell 会显示实际执行的 Mongo 查询。在这种情况下,我们看到以下内容:
[ 11:17:07 PM ]
db.postalCodes.find({ "city" : /Mumbai/i }).limit(50);
db.postalCodes.find({ "city" : /Mumbai/i }).limit(50).explain();
- 查询计划也会显示每次查询,截至当前版本 1.6.9.0,没有办法禁用查询计划的显示。
-
在树形视图中,右键单击文档会给出更多选项,例如展开它,复制 JSON 内容,向该文档添加键,删除文档等。尝试使用右键从集合中删除文档,并尝试向文档添加任何其他键。您可以选择通过重新导入
postalCodes集合中的数据来恢复文档。 -
要在集合中插入文档,请执行以下操作。我们将在
personTwo集合中插入一个文档:
-
右键单击personTwo集合名称,然后单击插入/导入文档…,如下图所示:
![如何做…]()
-
将出现另一个弹出窗口,在那里您可以选择输入单个 JSON 文档或包含要导入的 JSON 文档的有效文本文件。我们通过导入单个文档导入了以下文档:
{
"_id" : 4,
"firstName" : "Jack",
"lastName" : "Jones",
"age" : 35,
"gender" : "Male"
}
- 成功导入文档后,查询集合;我们将查看新导入的文档以及旧文档。
- 让我们看看如何更新文档:
-
您可以右键单击左侧的集合名称,然后单击更新,或者在顶部选择更新选项。在任何一种情况下,我们将看到以下窗口。在这里,我们将更新在上一步中插入的人的年龄:
![如何做…]()
-
在此 GUI 中需要注意的一些事项是左侧的查询文本框,用于查找要更新的文档,以及右侧的更新 JSON,它将应用于所选的文档。
-
在更新之前,您可以选择点击计数按钮,以查看可以更新的文档数量(在本例中为一个)。点击查找将以树形式显示文档。在右侧,在更新 JSON 文本下方,我们可以通过点击更新 1或全部更新来选择更新一个文档和多个文档。
-
如果找不到给定查找条件的文档,可以选择Upsert操作。
-
前一屏幕右下角的单选按钮显示
getLastError操作的输出或更新后的结果,如果是后者,则将执行查询以查找已更新的文档。 -
但是,查找查询并不是绝对可靠的,可能会返回与真正更新的结果不同的结果,就像在查找文本框中一样。更新和查找操作不是原子的。
- 到目前为止,我们已经在小集合上进行了查询。随着集合大小的增加,执行完整集合扫描的查询是不可接受的,我们需要创建索引如下:
-
要按
lastName升序和年龄降序创建索引,我们将调用db.personTwo.ensureIndex({'lastName':1, 'age':-1})。 -
使用 MongoVUE,有一种方法可以通过右键单击屏幕左侧的集合名称并选择添加索引…来可视化创建相同的索引。
-
在新的弹出窗口中,输入索引的名称,并选择可视选项卡,如图所示。分别选择lastName和age字段,以升序和降序的方式:
![如何做…]()
-
填写这些细节后,点击创建。这应该通过触发
ensureIndex命令为我们创建索引。 -
您可以选择将索引设置为唯一和删除重复项(当选择唯一时将启用),甚至可以在后台创建大型、长时间运行的索引创建。
-
请注意可视选项卡旁边的Json选项卡。这是您可以输入
ensureIndex命令的地方,就像在 shell 中一样,以创建索引。
- 我们将看到如何删除索引:
-
简单地展开左侧的树(如第 9 步的屏幕截图所示)
-
展开集合后,我们将看到在其上创建的所有索引
-
除了
_id字段上的默认索引外,所有其他索引都可以被删除。 -
简单右键单击名称,选择删除索引以删除,或点击属性查看其属性
- 在了解了基本的 CRUD 操作和创建索引之后,让我们看看如何执行聚合操作:
-
在聚合索引的创建中没有可视化工具,只是一个文本区域,我们在其中输入我们的聚合管道
-
在以下示例中,我们对
postalCodes集合执行聚合,以找到在集合中出现次数最多的五个州 -
我们将输入以下聚合管道:
{'$project' : {'state':1, '_id':0}},
{'$group': {'_id':'$state', 'count':{'$sum':1}}},
{'$sort':{'count':-1}},
{'$limit':5}

- 一旦进入管道,点击聚合按钮以获取聚合结果
- 执行 MapReduce 甚至更酷。我们将执行的用例与前面的用例类似,但我们将看到如何使用 MongoVUE 实现 MapReduce 操作:
-
要执行 map reduce 作业,请在左侧菜单中右键单击集合名称,然后单击Map Reduce。
-
此选项位于我们在上一张图片中看到的Aggregation选项正上方。这为我们提供了一个相当整洁的 GUI,可以输入Map、Reduce、Finalize和In & Out,如下图所示:
![如何做...]()
-
Map函数就是以下内容:
function Map() {
emit(this.state, 1)
}
Reduce函数如下:
function Reduce(key, values) {
return Array.sum(values)
}
-
保持
Finalize方法未实现,并在In & Out部分填写以下细节:![如何做...]()
-
单击开始开始执行 MapReduce 作业。
-
我们将输出打印到
mongoVue_mr集合。使用以下查询查询mongoVue_mr集合:
db.mongoVue_mr.find().sort({value:-1}).limit(5)
-
检查结果是否与使用聚合获得的结果相匹配。
-
选择了 map reduce 的格式作为Reduce。有关更多选项及其行为,请访问
docs.mongodb.org/manual/reference/command/mapReduce/#mapreduce-out-cmd。
- 现在可以使用
MongoVUE监视服务器实例:
-
要监视一个实例,请点击顶部菜单中的工具 | 监视。
-
默认情况下,不会添加任何服务器,我们必须点击+添加服务器来添加服务器实例。
-
选择添加的本地实例或任何要监视的服务器,然后单击连接。
-
我们将看到相当多的监控细节。MongoVUE 使用
db.serverStatus命令来提供这些统计信息,并限制我们在繁忙的服务器实例上执行此命令的频率,我们可以在屏幕顶部选择刷新间隔,如下图所示:

工作原理...
我们在前面的部分中所涵盖的内容对于我们作为开发人员和管理员来执行大部分活动都是非常简单的。
还有更多...
有关管理和监控 MongoDB 实例的管理和监控的详细信息,请参阅第四章、管理和第六章、监控和备份。
另请参阅
- 请参阅
www.mongovue.com/tutorials/,了解有关 MongoVUE 的各种教程
注意
在编写本书时,MongoDB 计划发布一个名为Compass的类似数据可视化和操作产品。您应该查看www.mongodb.com/products/compass。
附录 A. 参考概念
本附录包含一些额外信息,将帮助您更好地理解配方。我们将尽可能详细地讨论写入关注和读取偏好。
写入关注及其重要性
写入关注是 MongoDB 服务器提供的关于客户端执行的写入操作的最低保证。客户端应用程序设置了各种级别的写入关注,以便从服务器获取在服务器端写入过程中达到某个阶段的保证。
对于保证的要求越强,从服务器获取响应的时间就越长(可能)。在写入关注中,我们并不总是需要从服务器获取关于写入操作完全成功的确认。对于一些不太关键的数据,比如日志,我们可能更感兴趣地通过连接发送更多的写入。另一方面,当我们试图更新敏感信息,比如客户详细信息时,我们希望确保写入成功(一致和持久);数据完整性至关重要,优先于写入速度。
写入关注的一个极其有用的特性是在特定情况下在写入操作的速度和数据一致性之间进行权衡。然而,这需要对设置特定写入关注的影响有深入的理解。下图从左到右运行,并显示了写入保证水平的增加:

随着我们从I到IV,执行的写入保证越来越强,但从客户端的角度来看,执行写入操作所需的时间也越来越长。所有写入关注都以 JSON 对象的形式表示,使用三个不同的键,即w、j和fsync。另外,还使用了一个名为wtimeout的键,用于提供写入操作的超时值。让我们详细看一下这三个键:
-
w:用于指示是否等待服务器的确认,是否报告由于数据问题而导致的写入错误,以及数据是否被复制到次要位置。其值通常是一个数字,还有一个特殊情况,值可以是majority,我们稍后会看到。 -
j:这与日志记录有关,其值可以是布尔值(true/false 或 1/0)。 -
fsync:这是一个布尔值,与写入是否等待数据刷新到磁盘有关。 -
wtimeout:指定写入操作的超时时间,如果服务器在提供的时间内没有在几秒内回复客户端,驱动程序将向客户端抛出异常。我们很快会详细了解该选项。
在我们划分到驱动程序的I部分中,我们有两种写入关注点,分别是{w:-1}和{w:0}。这两种写入关注点都很常见,它们既不等待服务器对写入操作的确认,也不会报告由于唯一索引违规而在服务器端引起的任何异常。客户端将收到一个ok响应,并且只有在以后查询数据库时发现数据丢失时才会发现写入失败。两者的区别在于它们在网络错误时的响应方式。当我们设置{w:-1}时,操作不会失败,并且用户将收到写入响应。但是,它将包含一个响应,指出网络错误阻止了写入操作的成功,并且不应尝试重新写入。另一方面,对于{w:0},如果发生网络错误,驱动程序可能选择重试操作,并且如果由于网络错误导致写入失败,则向客户端抛出异常。这两种写入关注点以牺牲数据一致性为代价,快速向调用客户端返回响应。这些写入关注点适用于日志记录等用例,其中偶尔的日志写入丢失是可以接受的。在较早版本的 MongoDB 中,如果调用客户端没有提及任何写入关注点,则{w:0}是默认的写入关注点。在撰写本书时,这已更改为默认的{w:1}选项,而{w:0}选项已被弃用。
在图表的II部分,位于驱动程序和服务器之间,我们讨论的写入关注点是{w:1}。驱动程序等待服务器对写入操作的确认完成。请注意,服务器的响应并不意味着写入操作已经持久化。这意味着更改刚刚更新到内存中,所有约束都已经检查,并且任何异常都将报告给客户端,与我们之前看到的两种写入关注点不同。这是一个相对安全的写入关注点模式,将会很快,但如果在数据从内存写入日志时发生崩溃,仍然有一些数据丢失的可能性。对于大多数用例来说,这是一个不错的选择。因此,这是默认的写入关注点模式。
接下来,我们来到图表的III部分,从服务器的入口点到日志。我们在这里寻找的写入关注点是{j:1}或{j:true}。这种写入关注点确保只有当写入操作写入日志时,才会向调用客户端返回响应。但是什么是日志呢?这是我们在第四章中深入了解的内容,但现在,我们只看一种机制,确保写入是持久的,数据在服务器崩溃时不会损坏。
最后,让我们来到图表的IV部分;我们讨论的写入关注点是{fsync:true}。这要求在向客户端发送响应之前将数据刷新到磁盘。在我看来,当启用日志记录时,这个操作实际上并没有增加任何价值,因为日志记录确保即使在服务器崩溃时也能保持数据持久性。只有在禁用日志记录时,此选项才能确保客户端接收到成功响应时写入操作成功。如果数据真的很重要,首先不应该禁用日志记录,因为它还确保磁盘上的数据不会损坏。
我们已经看到了单节点服务器的一些基本写入关注点,或者仅适用于复制集中的主节点的写入关注点。
注意
讨论一个有趣的事情是,如果我们有一个写关注,比如{w:0, j:true}?我们不等待服务器的确认,同时确保写入已经被记录到日志中。在这种情况下,日志标志优先,并且客户端等待写操作的确认。应该避免设置这种模棱两可的写关注,以避免不愉快的惊喜。
现在,我们将讨论涉及副本集辅助节点的写关注。让我们看一下下面的图表:

任何w值大于一的写关注都表示在发送响应之前,辅助节点也需要确认。如前图所示,当主节点接收写操作时,它将该操作传播到所有辅助节点。一旦它从预定数量的辅助节点收到响应,它就向客户端确认写操作已成功。例如,当我们有一个写关注{w:3}时,这意味着只有当集群中的三个节点确认写操作时,客户端才会收到响应。这三个节点包括主节点。因此,现在只有两个辅助节点需要对成功的写操作做出响应。
然而,为写关注提供一个数字存在问题。我们需要知道集群中节点的数量,并相应地设置w的值。较低的值将向复制数据的少数节点发送确认。值太高可能会不必要地减慢向客户端的响应,或者在某些情况下可能根本不发送响应。假设您有一个三节点副本集,我们的写关注是{w:4},服务器将在数据复制到三个不存在的辅助节点时才发送确认,因为我们只有两个辅助节点。因此,客户端需要很长时间才能从服务器那里得知写操作的情况。解决这个问题有几种方法:
-
使用
wtimeout键并指定写关注的超时时间。这将确保写操作不会阻塞超过wtimeout字段指定的时间(以毫秒为单位)。例如,{w:3, wtimeout:10000}确保写操作不会阻塞超过 10 秒(10,000 毫秒),之后将向客户端抛出异常。在 Java 的情况下,将抛出WriteConcernException,根本原因消息将说明超时的原因。请注意,此异常不会回滚写操作。它只是通知客户端操作在指定的时间内未完成。它可能在客户端收到超时异常后的一段时间内在服务器端完成。由应用程序来处理异常并以编程方式采取纠正措施。超时异常的消息传达了一些有趣的细节,我们将在执行写关注的测试程序时看到。 -
在副本集的情况下,指定
w的更好方法是将值指定为majority。这种写关注会自动识别副本集中的节点数,并在数据复制到大多数节点时向客户端发送确认。例如,如果写关注是{w:"majority"},并且副本集中的节点数为三,则majority将是2。而在以后,当我们将节点数更改为五时,majority将是3个节点。当写关注的值给定为majority时,自动计算形成大多数所需的节点数。
现在,让我们将我们讨论的概念付诸实践,并执行一个测试程序,演示我们刚刚看到的一些概念。
建立副本集
要设置副本集,您应该知道如何启动具有三个节点的基本副本集。参考第一章 安装和启动服务器中的作为副本集的一部分启动多个实例配方。这个配方是基于那个配方构建的,因为在启动副本集时需要额外的配置,我们将在下一节中讨论。请注意,此处使用的副本与您之前使用的副本在配置上有轻微变化。
在这里,我们将使用一个 Java 程序来演示各种写入关注点及其行为。在第一章 安装和启动服务器中的使用 Java 客户端连接单个节点配方中,直到设置 Maven 之前,应该被访问。如果您来自非 Java 背景,这可能有点不方便。
注意
Java 项目名为Mongo Java可在该书的网站上下载。如果设置完成,只需执行以下命令即可测试该项目:
mvn compile exec:java -Dexec.mainClass=com.packtpub.mongo.cookbook.FirstMongoClient
该项目的代码可在该书的网站上下载。下载名为WriteConcernTest的项目,并将其保存在本地驱动器上以备执行。
所以,让我们开始吧:
- 为副本集准备以下配置文件。这与我们在第一章 安装和启动服务器中的作为副本集的一部分启动多个实例配方中看到的配置文件相同,我们在那里设置了副本集,只有一个区别,
slaveDelay:5,priority:0:
cfg = {
_id:'repSetTest',
members:[
{_id:0, host:'localhost:27000'},
{_id:1, host:'localhost:27001'},
{_id:2, host:'localhost:27002', slaveDelay:5, priority:0}
]
}
-
使用此配置启动一个三节点副本集,其中一个节点监听端口
27000。其他节点可以是您选择的任何端口,但如果可能的话,请坚持使用27001和27002(如果决定使用不同的端口号,我们需要相应更新配置)。还要记得在启动副本集时,将副本集的名称设置为replSetTest,并将其作为replSet命令行选项。在继续下一步之前,请给副本集一些时间来启动。 -
此时,具有前述规格的副本集应该已经启动并运行。我们现在将执行 Java 中提供的测试代码,以观察不同写入关注点的一些有趣事实和行为。请注意,此程序还尝试连接到没有 Mongo 进程监听连接的端口。选择的端口是
20000;在运行代码之前,请确保没有服务器正在运行并监听端口20000。 -
转到
WriteConcernTest项目的根目录并执行以下命令:
mvn compile exec:java -Dexec.mainClass=com.packtpub.mongo.cookbook.WriteConcernTests
这需要一些时间才能完全执行,具体取决于您的硬件配置。在我的机器上大约花了 35 到 40 秒的时间,我的机器上有一个 7200 转的传统硬盘。
在我们继续分析日志之前,让我们看看添加到配置文件中设置副本的这两个附加字段是什么。slaveDelay字段表示特定的副本(在本例中监听端口27002的副本)将比主节点滞后 5 秒。也就是说,当前在该副本节点上复制的数据是 5 秒前添加到主节点上的数据。其次,该节点永远不能成为主节点,因此必须添加priority字段并赋值为0。我们已经在第四章 管理中详细介绍了这一点。
现在让我们分析前述命令执行的输出。这里不需要查看提供的 Java 类;控制台上的输出就足够了。输出控制台的一些相关部分如下:
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ mongo-cookbook-wctest ---
Trying to connect to server running on port 20000
Trying to write data in the collection with write concern {w:-1}
Error returned in the WriteResult is NETWORK ERROR
Trying to write data in the collection with write concern {w:0}
Caught MongoException.Network trying to write to collection, message is Write operation to server localhost/127.0.0.1:20000 failed on database test
Connected to replica set with one node listening on port 27000 locally
Inserting duplicate keys with {w:0}
No exception caught while inserting data with duplicate _id
Now inserting the same data with {w:1}
Caught Duplicate Exception, exception message is { "serverUsed" : "localhost/127.0.0.1:27000" , "err" : "E11000 duplicate key error index: test.writeConcernTest.$_id_ dup key: { : \"a\" }" , "code" : 11000 , "n" : 0 , "lastOp" : { "$ts" :1386009990 , "$inc" : 2} , "connectionId" : 157 , "ok" : 1.0}
Average running time with WriteConcern {w:1, fsync:false, j:false} is 0 ms
Average running time with WriteConcern {w:2, fsync:false, j:false} is 12 ms
Average running time with WriteConcern {w:1, fsync:false, j:true} is 40 ms
Average running time with WriteConcern {w:1, fsync:true, j:false} is 44 ms
Average running time with WriteConcern {w:3, fsync:false, j:false} is 5128 ms
Caught WriteConcern exception for {w:5}, with following message { "serverUsed" : "localhost/127.0.0.1:27000" , "n" : 0 , "lastOp" : { "$ts" : 1386009991 , "$inc" : 18} , "connectionId" : 157 , "wtimeout" : true , "waited" : 1004 , "writtenTo" : [ { "_id" : 0 , "host" : "localhost:27000"} , { "_id" : 1 , "host" : "localhost:27001"}] , "err" : "timeout" , "ok" : 1.0}
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 36.671s
[INFO] Finished at: Tue Dec 03 00:16:57 IST 2013
[INFO] Final Memory: 13M/33M
[INFO] ------------------------------------------------------------------------
日志中的第一条语句说明我们尝试连接到一个监听端口20000的 Mongo 进程。由于不应该有 Mongo 服务器在此端口上运行并监听客户端连接,因此我们所有对该服务器的写操作都不应成功,现在我们有机会看看当我们使用写关注{w:-1}和{w:0}并向这个不存在的服务器写入时会发生什么。
输出中的下两行显示,当我们有写关注{w:-1}时,我们确实得到了写入结果,但其中包含了设置为指示网络错误的错误标志。但是,没有抛出异常。在写关注{w:0}的情况下,我们在客户端应用程序中对任何网络错误都会得到异常。当然,在这种情况下,所有其他确保严格保证的写关注也会抛出异常。
现在我们来到连接到副本集的代码部分,其中一个节点正在监听端口27000(如果没有,代码将在控制台上显示错误并终止)。现在,我们尝试向集合中插入一个具有重复_id字段({'_id':'a'})的文档,一次使用写关注{w:0},一次使用{w:1}。正如我们在控制台中看到的,前者({w:0})没有抛出异常,从客户端的角度来看插入成功进行了,而后者({w:1})向客户端抛出了异常,指示重复键。异常包含了关于服务器主机名和端口的大量信息,在异常发生时:唯一约束失败的字段;客户端连接 ID;错误代码;以及导致异常的不唯一值。事实是,即使使用{w:0}作为写关注进行插入,它也失败了。但是,由于驱动程序没有等待服务器的确认,它从未被通知插入失败。
继续前进,我们现在尝试计算写操作完成所需的时间。这里显示的时间是执行相同操作的给定写关注五次所需时间的平均值。请注意,这些时间将在程序的不同执行实例上变化,这种方法只是为了给我们的研究提供一些粗略的估计。我们可以从输出中得出结论,写关注{w:1}所需的时间少于{w:2}(要求从一个辅助节点获得确认),而{w:2}所需的时间少于{j:true},而{j:true}又少于{fsync:true}。输出的下一行告诉我们,当写关注为{w:3}时,写操作完成所需的平均时间大约为 5 秒。你猜为什么会这样吗?为什么会花这么长时间?原因是,当w为3时,我们只有在两个辅助节点确认写操作时才向客户端发送确认。在我们的情况下,一个节点比主节点延迟约 5 秒,因此只有在 5 秒后才能确认写操作,因此客户端大约在 5 秒后从服务器收到响应。
让我们在这里做一个快速练习。当我们的写关注为{w:'majority'}时,你认为大约的响应时间会是多少?这里的提示是,对于一个三个节点的副本集,两个是大多数。
最后我们看到了超时异常。超时是使用文档的wtimeout字段设置的,以毫秒为单位。在我们的情况下,我们设置了 1000 毫秒的超时,即 1 秒,并且在将响应发送回客户端之前从副本集中获得确认的节点数为 5(四个从实例)。因此,我们的写关注是{w:5, wtimeout:1000}。由于我们的最大节点数为三个,所以将w设置为5的操作将等待很长时间,直到集群中添加了另外两个从实例。设置超时后,客户端返回并向客户端抛出错误,传达一些有趣的细节。以下是作为异常消息发送的 JSON:
{ "serverUsed" : "localhost/127.0.0.1:27000" , "n" : 0 , "lastOp" : { "$ts" : 1386015030 , "$inc" : 1} , "connectionId" : 507 , "wtimeout" : true , "waited" : 1000 , "writtenTo" : [ { "_id" : 0 , "host" : "localhost:27000"} , { "_id" : 1 , "host" : "localhost:27001"}] , "err" : "timeout" , "ok" : 1.0}
让我们看看有趣的字段。我们从n字段开始。这表示更新的文档数量。在这种情况下,它是一个插入而不是更新,所以保持为0。wtimeout和waited字段告诉我们事务是否超时以及客户端等待响应的时间;在这种情况下是 1000 毫秒。最有趣的字段是writtenTo。在这种情况下,插入在超时时成功在副本集的这两个节点上,并且因此在数组中看到。第三个节点的slaveDelay值为 5 秒,因此数据仍未写入。这证明超时不会回滚插入,它确实成功进行。实际上,即使操作超时,具有slaveDelay的节点也将在 5 秒后拥有数据,这是有道理的,因为它保持主节点和从节点同步。应用程序有责任检测此类超时并处理它们。
查询的读取偏好
在前一节中,我们看到了写关注是什么以及它如何影响写操作(插入、更新和删除)。在本节中,我们将看到读取偏好是什么以及它如何影响查询操作。我们将讨论如何在单独的配方中使用读取偏好,以使用特定的编程语言驱动程序。
当连接到单个节点时,默认情况下允许查询操作连接到主节点,如果连接到从节点,则需要明确声明可以通过在 shell 中执行rs.slaveOk()来从从实例查询。
然而,考虑从应用程序连接到 Mongo 副本集。它将连接到副本集,而不是从应用程序连接到单个实例。根据应用程序的性质,它可能总是想要连接到主节点;总是连接到从节点;更喜欢连接到主节点,但在某些情况下连接到从节点也可以,反之亦然,最后,它可能连接到地理位置靠近它的实例(嗯,大部分时间)。
因此,读取偏好在连接到副本集而不是单个实例时起着重要作用。在下表中,我们将看到各种可用的读取偏好以及它们在查询副本集方面的行为。共有五种,名称不言自明:
| 读取偏好 | 描述 |
|---|---|
primary |
这是默认模式,它允许查询仅在主实例上执行。这是唯一保证最新数据的模式,因为所有写操作都必须通过主实例进行。然而,如果没有主实例可用,读操作将失败,这在主机宕机并持续到选择新的主机时会发生一段时间。 |
primaryPreferred |
这与前面的主读取偏好相同,只是在故障切换期间,当没有主机可用时,它将从从节点读取数据,这些时候可能不会读取到最新数据。 |
secondary |
这与默认的 primary 读取偏好完全相反。此模式确保读取操作永远不会转到 primary,而总是选择 secondary。在这种模式下,读取不一致的数据的机会最大,因为它没有更新到最新的写操作。但是,对于不面向最终用户并且用于某些实例获取每小时统计和分析作业的应用程序来说,这是可以接受的(事实上是首选),其中数据的准确性最不重要,但不会增加对 primary 实例的负载是关键的。如果没有 secondary 实例可用或可达,只有 primary 实例,读取操作将失败。 |
secondaryPreferred |
这与前面的 secondary 读取偏好类似,除了如果没有 secondary 可用,读取操作将转到 primary 实例。 |
nearest |
与所有先前的读取偏好不同,这可以连接到 primary 或 secondary。这种读取偏好的主要目标是客户端和副本集实例之间的最小延迟。在大多数情况下,由于网络延迟和客户端与所有实例之间的相似网络,所选择的实例将是地理上接近的实例。 |
与写关注可以与分片标签结合使用类似,读取偏好也可以与分片标签一起使用。由于标签的概念已经在第四章中介绍过,您可以参考它以获取更多详细信息。
我们刚刚看到了不同类型的读取偏好(除了使用标签的那些),但问题是,我们如何使用它们?本书中涵盖了 Python 和 Java 客户端,并将看到如何在它们各自的示例中使用它们。我们可以在各个级别设置读取偏好:在客户端级别、集合级别和查询级别,查询级别指定的读取偏好将覆盖先前设置的任何其他读取偏好。
让我们看看最近的读取偏好意味着什么。从概念上讲,它可以被可视化为以下图表:

Mongo 副本集设置了一个 secondary,它永远不会成为 primary,在一个单独的数据中心,另一个数据中心有两个(一个 primary 和一个 secondary)。在两个数据中心都部署了相同的应用程序,使用 primary 读取偏好,将始终连接到数据中心 I中的 primary 实例。这意味着,对于数据中心 II中的应用程序,流量将通过公共网络,这将具有较高的延迟。但是,如果应用程序可以接受略有陈旧的数据,它可以将读取偏好设置为最近,这将自动让数据中心 I中的应用程序连接到数据中心 I中的实例,并允许数据中心 II中的应用程序连接到数据中心 II中的 secondary 实例。
但接下来的问题是,驱动程序如何知道哪一个是最近的?术语“地理上接近”是误导的;实际上是具有最小网络延迟的那个。我们查询的实例可能在地理上比副本集中的另一个实例更远,但它可能被选择,只是因为它具有可接受的响应时间。通常,更好的响应时间意味着地理上更接近。
以下部分是为那些对驱动程序内部细节感兴趣的人准备的,关于最近节点是如何选择的。如果您只对概念感兴趣而不关心内部细节,可以放心地跳过其余内容。
了解内部情况
让我们看一下来自 Java 客户端(用于此目的的驱动程序为 2.11.3)的一些代码片段,并对其进行一些解释。如果我们查看com.mongodb.TaggableReadPreference.NearestReadPreference.getNode方法,我们会看到以下实现:
@Override
ReplicaSetStatus.ReplicaSetNode getNode(ReplicaSetStatus.ReplicaSet set) {
if (_tags.isEmpty())
return set.getAMember();
for (DBObject curTagSet : _tags) {
List<ReplicaSetStatus.Tag> tagList = getTagListFromDBObject(curTagSet);
ReplicaSetStatus.ReplicaSetNode node = set.getAMember(tagList);
if (node != null) {
return node;
}
}
return null;
}
目前,如果我们忽略指定标签的内容,它所做的就是执行set.getAMember()。
这个方法的名称告诉我们,有一组副本集成员,我们随机返回其中一个。那么是什么决定了集合是否包含成员?如果我们再深入一点研究这个方法,我们会在com.mongodb.ReplicaSetStatus.ReplicaSet类中看到以下代码行:
public ReplicaSetNode getAMember() {
checkStatus();
if (acceptableMembers.isEmpty()) {
return null;
}
return acceptableMembers.get(random.nextInt(acceptableMembers.size()));
}
好的,它所做的就是从内部维护的副本集节点列表中选择一个。现在,随机选择可以是一个 secondary,即使可以选择一个 primary(因为它存在于列表中)。因此,我们现在可以说当最近的节点被选择为读取偏好时,即使主节点在候选者列表中,也可能不会被随机选择。
现在的问题是,acceptableMembers列表是如何初始化的?我们看到它是在com.mongodb.ReplicaSetStatus.ReplicaSet类的构造函数中完成的,如下所示:
this.acceptableMembers =Collections.unmodifiableList(calculateGoodMembers(all, calculateBestPingTime(all, true),acceptableLatencyMS, true));
calculateBestPingTime行只是找到所有 ping 时间中的最佳时间(稍后我们将看到这个 ping 时间是什么)。
值得一提的另一个参数是acceptableLatencyMS。这在com.mongodb.ReplicaSetStatus.Updater中初始化(实际上是一个不断更新副本集状态的后台线程),acceptableLatencyMS的值初始化如下:
slaveAcceptableLatencyMS = Integer.parseInt(System.getProperty("com.mongodb.slaveAcceptableLatencyMS", "15"));
正如我们所见,这段代码搜索名为com.mongodb.slaveAcceptableLatencyMS的系统变量,如果找不到,则初始化为值15,即 15 毫秒。
这个com.mongodb.ReplicaSetStatus.Updater类还有一个run方法,定期更新副本集的统计信息。不深入研究,我们可以看到它调用updateAll,最终到达com.mongodb.ConnectionStatus.UpdatableNode中的update方法。
long start = System.nanoTime();
CommandResult res = _port.runCommand(_mongo.getDB("admin"), isMasterCmd);
long end = System.nanoTime()
它所做的就是执行{isMaster:1}命令并记录响应时间(以纳秒为单位)。这个响应时间转换为毫秒并存储为 ping 时间。所以,回到com.mongodb.ReplicaSetStatus.ReplicaSet类中,calculateGoodMembers所做的就是找到并添加副本集中不超过acceptableLatencyMS毫秒的成员,这些成员的 ping 时间不超过副本集中找到的最佳 ping 时间。
例如,在一个有三个节点的副本集中,客户端到三个节点(节点 1、节点 2 和节点 3)的 ping 时间分别为 2 毫秒、5 毫秒和 150 毫秒。正如我们所见,最佳时间是 2 毫秒,因此节点 1 进入了良好成员的集合中。现在,从剩下的节点中,所有延迟不超过最佳时间的acceptableLatencyMS的节点也是候选者,即2 + 15 毫秒 = 17 毫秒,因为 15 毫秒是默认值。因此,节点 2 也是一个候选者,剩下的是节点 3。现在我们有两个节点在良好成员的列表中(从延迟的角度来看是好的)。
现在,将我们在前面的图表中看到的所有内容整合起来,最小的响应时间将来自同一数据中心中的一个实例(从这两个数据中心的编程语言驱动程序的角度来看),因为其他数据中心中的实例可能由于公共网络延迟而无法在 15 毫秒(默认可接受值)内响应。因此,数据中心 I中的可接受节点将是该数据中心中的两个副本集节点,其中一个将被随机选择,而对于数据中心 II,只有一个实例存在,也是唯一的选择。因此,它将由在该数据中心运行的应用程序选择。











































浙公网安备 33010602011771号