首页

第8章 MongoDB的优化和安全建议

关灯 护眼    字体:

上一章 目录 下一章




无论SQL数据库还是NoSQL数据库,都有一些通用的技巧可以大大提高读写性能。作为NoSQL的MongoDB有自己的一些特性。将这些特性应用到生产环境中时,需要提高警惕,以防导致不必要的麻烦。

MongoDB默认没有密码,且只允许本地访问。如果开放外网访问,就一定要设置密码,否则会有安全隐患。



8.1  提高MongoDB读写性能


使用一些简单的技巧,就可以大大提高  MongoDB  的读写性能。本节将介绍其中几种常见的技巧。



8.1.1  实例26:“批量插入”与“逐条插入”数据,比较性能差异


实例描述

在  MongoDB  中,分别用“逐条插入”与“批量插入”两种方式插入相同的数据,比较两者的时间差。

使用Python向MongoDB中插入一条数据,只需要3行代码,见代码8-1。

代码8-1  插入一条数据到MongoDB

从Python执行完成第3行代码,到数据存到数据库中,这个过程可能只需要几毫秒。但是在这几毫秒中,网络传输的时间占了非常大的比例。

I/O(Input/Ouput,输入/输出)操作总是最耗费时间的,无论是硬盘的I/O操作还是网络I/O操作。

●  如果写到本地的MongoDB,数据会在网卡中转一圈再存入硬盘。

●  如果写到远程的MongoDB,数据会先从本地网卡出去,然后经过网线,在电磁波、光信号、电信号之间进行转换,中间通过一层一层的交换机路由器,甚至海底光缆,绕地球一圈再进入目标服务器的网卡最后存入数据库。

这就像是扔砖头,分10次,每次只扔一块砖头的时间,肯定远远大于把10块砖一次扔出去的时间。如果你能够一次扔10块砖头,为什么你要一块一块地扔呢。

现在的宽带技术,上下行速度动辄每秒几百兆字节。如果使用  MongoDB  插入数据还在逐条插入,每一条几个字节,那可真是白白浪费了网络带宽。

下面通过实际数据来对比逐条插入数据和批量插入数据的性能差异。

1.生成初始数据

为了对比结果的公平性,首先生成一个CSV文件,这个文件中的数据将用于测试逐条插入与批量插入功能。

运行generate_people_info.py,会在当前文件夹下面生成一个people_info.csv文件,如图8-1所示。

图8-1  生成初始数据

people_info.csv一共有119  810行,除去第1行标题行和最后1行空白行,一共有119  808条数据将会被插入MongoDB中。

提示:

由于“age”“salary”“phone”这3个字段使用了随机数,所以每一次重新生成的数据都不同。读者自行生成的people_info.csv应该和图中有所差异,这是正常情况。读者只需要保证逐条插入和批量插入使用的数据相同即可。

2.逐行插入数据

编写一段Python代码,读取CSV文件并逐条插入到MongoDB中。代码如下:

代码8-2  计算逐条插入数据的时间

其中,主要代码说明如下。

●  第5~7行代码:使用Python自带的CSV模块读取CSV文件,并将其转换为包含字典的列表。其中每一个字典为CSV中的一行数据。

●  第9行代码:初始化MongoDB并连接到chatper_8库下面的one_by_one集合。

●  第11行代码:记录开始时间戳。

●  第12、13行代码:使用for循环把数据逐条插入到MongoDB中。

●  第14、15行代码:记录结束时间戳,并打印出时间差。

运行效果如图8-2所示。插入119  808条数据,共耗时44秒。

图8-2  逐条插入119808条数据,耗时44秒

3.批量插入数据

编写一段Python代码,测试批量插入数据的性能,见代码8-3。

代码8-3  计算批量插入数据的时间

其中,主要代码说明如下。

●  第5~7行代码:使用Python自带的CSV模块读取CSV文件,并将其转换为包含字典的列表。其中每一个字典为CSV中的一行数据。

●  第9行代码:初始化MongoDB,并连接到chatper_8库下面的batch集合。

●  第11行代码:记录开始时间戳。

●  第12行代码:使用insert_many()方法直接把包含字典的列表插入数据库。

●  第13、14行代码:记录结束时间戳,并打印时间差。

运行效果如图8-3所示。批量插入119  808条数据,只用了不到2.7秒。

图8-3  批量插入119808条数据,耗时2.7秒

4.如何正确批量插入数据

仅仅是使用本地的MongoDB数据库,批量插入数据的性能就远远超过逐条插入数据性能。如果使用的是远程数据库,那么网络I/O导致的时间消耗会比这个差异大很多倍。

既然批量插入数据库的性能这么好,那如何正确地使用批量插入功能?下面这一段代码想实现的功能是:从Redis里面读数据,再插入到MongoDB中。请读者看看这段代码有什么问题。

代码8-4  一段有多种崩溃可能的批量插入数据的示例代码

其中,主要代码说明如下。

●  第6行代码:初始化Redis连接。

●  第7行代码:初始化MongoDB连接。

●  第10行代码:开启一个永远运行的循环。

●  第11行代码:在Redis中名为people_info的列表左侧获取一条数据,并将数据赋值给people_info_json变量。

●  第12~14行代码:如果people_info_json不为空,则使用JSON模块把它转换为字典,然后将其添加到people_info_list列表中。

●  第15、16行代码:如果people_info_json为空,则说明Redis数据已经读完,跳出循环。

●  第17行代码:把people_info_list中的数据批量插入MongoDB。

这段代码会有什么问题呢?这里随便列出几条。

(1)如果Redis中的数据量非常大,全部转换为字典以后超过了系统内存,会怎么样?

(2)如果Redis中的数据临时暂停添加,过一会儿再添加,会怎么样?

(3)假设Redis中有100  000  000条数据,读取到第99  999  999条数据时,突然电脑断电了,会怎么样?

……

5.批量插入一次性数据

如果已经明确知道  Redis  中的数据就是全部数据,虽然多,但是不会继续增加新的数据,那么代码可以修改为如下:

代码8-5  以1000条数据为一组分批次批量插入数据

其中,关键的修改在第15~17行和21行。

●  15~17行,虽然还是批量插入数据,但为了安全起见,是小批量插入。每从  Redis  中读取1000条数据就插入一次数据库。这样做的好处是,即使电脑断电,最多丢失1000条数据。当然,这里需要根据系统能够容忍的最大丢失数据条数来设置。

●  第21行,再一次判断people_info_list是否为空。如果不为空,则再插入一次。这是因为:总数据量如果不是1000的整数倍,那么最后一轮凑不够1000条数据,在循环中无法插入,所以结束循环以后还需要再插入一次。但是insert_many是不能接收空列表的,所以只有在people_info不为空时才能插入。

6.批量插入持续性数据

如果Redis中的数据是持续性数据,则会有新数据源源不断被加入到Redis中,每次添加之间的时间间隔从几毫秒到几小时不等。代码可以修改为如下。

代码8-6  分批次批量插入持续性数据

其中,主要的修改点如下。

●  第11行代码:增加了一个计数变量,通过第25行代码实现每获取一次Redis中的数据就让变量加1。

●  第21行代码:在Redis为空的情况下,如果people_info_list中有数据,不论有多少数据,只要请求Redis的次数为1000的倍数,那么就批量插入数据库。这样做的好处是,保证people_info_list中的数据最多等待100秒就会被插入数据库。这里使用了“%”实现取余操作,“get_count  %  1000”的结果为get_count除以1000的余数。如果结果为0,则表示get_count正好是1000的整数倍。

●  第24行代码:在本次发现Redis为空的情况下,暂停0.1秒,这样做可以显著降低CPU的占用。



8.1.2  实例27:“插入”与“更新”数据,比较性能差异


更新操作(特别是逐条更新)比较费时间,因为它实际上包含“查询”和“修改”两个步骤。与“插入”不一样,某些情况下数据的“更新”没有办法实现批量操作,必需逐条更新。

实例描述

对于  one_by_one  数据集,现在要把每一条记录的“salary”字段的值,在原有的基础上增加100。使用下面两种方式更新:

(1)逐条更新数据。

(2)把数据读入Python,更新以后批量插入新的集合中。

1.逐条更新数据

下面以更新8.1.1小节生成的one_by_one集合为例。例如,要把每一条记录的“salary”字段的值在原有的基础上增加100。然而,在one_by_one集合中,“salary”这个字段的类型是字符串,不是整型,如图8-4所示。

图8-4  salary字段的类型为字符串

逐条更新数据的Python代码如下:

代码8-7  测试逐条更新数据的耗时



其中,主要代码说明如下。

●  第7行代码:读取所有数据,并只输出“_id”字段(默认输出)和“salary”字段。

●  第8行代码:把“salary”字段转换为整型数据。

●  第10行代码:根据“_id”字段把新的“salary”字段更新到数据库中。

代码运行效果如图8-5所示,逐条更新119  808条数据耗时68.7秒,比逐条插入数据的时间还长。

图8-5  逐条更新119808条数据耗时68.7秒

2.用插入数据代替更新数据

对于必需逐条更新大量数据的情况,也可以使用插入代替更新来提高性能。

基本逻辑是:把数据插入到另一个集合中,然后删除原来的集合,再把新集合改名为原来的集合。

示例代码如下:

代码8-8  测试使用插入数据代替更新数据的耗时

其中,主要代码说明如下。

●  第6~8行代码:初始化两个连接,分别指向batch集合和update_by_insert集合。

●  第14行代码:把更新以后的数据添加到新的列表中。

●  第15行:把新的列表批量插入数据库。

运行效果如图8-6所示。更新119  808条数据并插入新的集合中,耗时3秒。

图8-6  使用插入代替更新,耗时3秒

更新完成以后,删除原来的batch集合,再把新的集合update_by_insert改名为“batch”,就变相完成了数据的批量更新。



8.1.3  实例28:使用“索引”提高查询速度


实例描述

为one_by_one集合的“salary”字段增加索引,从而提高查询速度。

在一个集合的数据量到达千万量级以后,查询速度会变得非常缓慢,这时就需要使用索引来加快查询速度。

索引是一种特殊的数据结构,它使用了能够快速遍历的形式记录了集合中数据的位置。

如果不使用索引,则每一次查询数据  MongoDB  都会遍历整个集合;而如果使用了索引,则MongoDB会直接根据索引快速找到需要的内容。

1.原理比较

举例:在集合one_by_one中,要查询所有“salary”字段大于10000的记录。

●  如果没有对  salary  添加索引,那么  MongoDB  就会一条一条地检查,如果“salary”大于10000就记录下来。直到把所有记录遍历完,然后输出所有满足“salary”大于10000的记录。

●  如果为“salary”添加了索引,那么MongoDB在创建索引的过程中就会对“salary”的值进行排序,索引默认是升序。有了索引后,MongoDB的查询会先从索引中寻找,于是就能大大提高速度。

2.创建索引

对一个集合中的一个字段创建索引非常的简单,代码如下:

代码8-9  创建索引

其中第5行代码,对“salary”字段创建索引。background参数可以为True或者为False。

●  如果为False,那在创建索引时,这个集合就不能被查询也不能被写入,但是速度快。

●  如果设置为True,那么创建索引的速度会慢一些,但是不影响其他程序读写这个集合。

对一个字段添加索引以后,千万量级的数据在一秒内就可以查询出结果。

对一个字段,索引只需要添加一次,之后插入的新数据MongoDB都会自动处理。

索引是以空间换时间。集合中的数据越多,索引占用的硬盘空间就越多。所以,只对必要的字段添加索引,不要对所有字段都添加索引。

_id默认自带索引,不需要添加。



8.1.4  实例29:引入Redis,以降低MongoDB的读取频率


实例描述

使用Redis,以降低MongoDB的查询频率,从而提高新闻爬虫的爬取效率。

(1)读取MongoDB的数据并存入Redis集合中。

(2)使用Redis集合的“sadd”命令,在判断数据是否存在的同时添加新的数据。

即使字段有了索引,但如果程序频繁读取MongoDB,还是会影响性能。

1.何时需要降低MongoDB的读取频率

假设,需要实现一个新闻网站的爬虫,让它会去各个新闻网站爬取新闻,然后存入MongoDB中。为了不存入重复的新闻,爬虫需要根据新闻标题来判断新闻是否已经在数据库中了。

如果每一条新闻标题去查询  MongoDB  看是否已经重复,这显然会严重影响性能。为了防止频繁读MongoDB,则可以引入Redis以降低MongoDB的读取频率。

2.具体方法

假设新闻保存在chapter_8库中的news集合中。一开始news集合里面已经有不少新闻了。

当爬虫启动时,先读取一次news中的全部新闻标题,并把它们放在Redis中名为news_title的集合中。接下来,就不需要读取MongoDB了。

爬虫每爬取到一条新的新闻,就先使用“sadd”命令将其添加到Redis的集合中:

●  如果返回1,则表示以前没有这条新闻,将其插入到MongoDB中。

●  如果返回0,则表示以前已经有这条新闻了,直接丢弃。

示例代码片段如下:

代码8-10  使用Redis判断是否需要插入数据

其中,主要代码说明如下。

●  第2行代码:获取所有新闻标题。

●  第3行代码:把新闻标题全部添加到Redis中名为“news_title”的集合中。

●  第7行代码:添加并判断新闻标题是否已经在news_title集合中。如果已经存在,则返回0;如果不存在,则返回1,并将其添加进入Redis集合中。

由于Redis的读写速度远远快于MongoDB,因此使用Redis可避免频繁读取MongoDB从而大大提高程序性能。



8.1.5  实例30:增添适当冗余信息,以提高查询速度


实例描述

对于one_by_one数据集,快速查询“age”字段小于10,“salary”字段大于10000的数据。在查询的过程中,需要解决“age”和“salary”字段都是字符串的问题。

在插入数据时,提前根据“age”与“salary”字段的值添加一个额外的字段“special_person”,这个字段的值用来记录这个人是不是满足要求。

所谓的冗余信息,也就是“多余的信息”,即根据其他已有信息可以推算出来的信息。但有时多余的信息对提高查询性能反而有帮助。

1.提出问题

还是以one_by_one中的数据为例。假设定义一个身份“特殊人员”,这种身份需要满足的条件是:age小于10,salary大于10000。

问题是,age和salary两个字段的值都是字符串,而且字符串的长度不一样,无法正确用数字的方式比较大小。

例如字符串“5”就大于“10”,因为第一个字符“5”大于“1”,这就导致查询age小于字符串10时会漏掉字符串2~9。

这种情况下,要查询所有的“特殊人员”,无论是把数据全部读出来然后用Python将其转换为整数来判断,还是使用第7章讲到的聚合查询,都非常麻烦。

提示:

如何比较字符串型数字的大小?

我们知道,数字9显然是小于数字100的,但在字符串型的数字中却不是这么一回事。因为字符串比较大小是从左到右逐一比较的。例如字符串9和字符串100,首先比较9和1,发现9大于1,那么就判定字符串9大于字符串100。

如果非要比较字符串型的数字怎么办呢?那就要让字符串型的数字保持相同的长度,长度不足的左侧补0。那么字符串9实际上应该是009。009和100比大小,显然0小于1,所以009小于100。

2.解决问题

如果在插入数据库时就添加一个字段“special_person”,满足条件就是True,不满足条件就是False。那查询时就简单了,直接查询所有special_person字段为True的数据即可,如图8-7所示。

这里添加的special_person就属于一种冗余信息。因为根据age和salary已经可以判断一个人是不是“特殊人员”,添加special_person这个字段看起来多此一举。但是在查询时,它所带来的便利是显而易见的。

图8-7  使用冗余信息查询数据


上一章 目录 下一章