本文主要为《左手MongoDB,右手Redis:从入门到商业实战》 学习笔记。
第1篇 基础知识
非关系型数据库的分类及特点非关系型数据库主要分为以下几类。
- 键值数据库主要代表是Redis、Flare。这类数据库具有极高的读写性能,用于处理大量数据的高访问负载比较合适。
- 文档型数据库主要代表是MongoDB、CouchDB。这类数据库满足了海量数据的存储和访问需求,同时对字段要求不严格,可以随意地增加、删除、修改字段,且不需要预先定义表结构,所以适用于各种网络应用。
- 列存储数据库主要代表是Cassandra、Hbase。这类数据库查找速度快,可扩展性强,适合用作分布式文件存储系统。
- 图数据库主要代表是InfoGrid、Neo4J。这类数据库利用“图结构”的相关算法,适合用于构建社交网络和推荐系统的关系图谱。
MongoDB适合储存大量关联性不强的数据。MongoDB中的数据以“库”—“集合”—“文档”—“字段”结构进行储存。
MongoDB不需要预先定义表结构,数据的字段可以任意变动,并发写入速度也远远超过传统关系型数据库。
Redis有多种数据结构,适合多种不同的应用场景:做缓存,做队列,去重,实现积分板,实现“发布/订阅”功能。第2篇 快速入门
MongoDB
在Windows中安装安装MongoDB社区版 https://www.mongodb.com/try/download/community ,不要勾选“Install MongoDB Compass” ,这个选择后会去下载和Compass,很花费时间的!
输入以下代码启动MongoDB:mongod.exe --config mongod.conf
MongoDB的图形化管理软件——Robo 3T
Robo 3T是一个跨平台的MongoDB管理工具,采用图形界面查询或者修改MongoDB。Robo 3T的下载地址为:https://robomongo.org/download。
- 插入单条数据
插入单条数据的命令为“insertOne”。
批量插入数据的命令是“insertMany”。
无论是插入一条数据还是插入多条数据,每一条数据被插入 MongoDB 后都会被自动添加一个字段“_id”。“_id”读作“Object Id”,它是由时间、机器码、进程pid和自增计数器构成的。“_id”始终递增,但绝不重复。 - 查询特定数据
如要查询某个或者某些具体字段,
“find”的参数相当于一个字典。字典的 Key 就是字段名,字典的值就是要查询的值。如果字典有多个Key,则这些字段需同时满足。 - 查询范围值数据
如要查询的字段值能够比较大小,则查询时可以限定值的范围。使用大于等于操作符“$gte”。
● gte 大于等于(Great Than and Equal)
● gt 大于(Great Than)
● lt 小于(Less Than)
● lte 小于等于(Less Than and Equal)
● ne 不等于(Not Equal) - 限定返回哪些字段
“find”命令可以接收两个参数:第1个参数用于过滤不同的记录,第2个参数用于修改返回的字段。如果省略第2个参数,则MongoDB会返回所有的字段。 - 修饰返回结果
(1)满足要求的数据有多少条——count()命令。
(2)限定返回结果——“limit()”命令。
(3)对查询结果进行排序——“sort()”命令。
其中,字段的值为-1表示倒序,为1表示正序。 - 修改数据
● updateOne:只更新第1条满足要求的数据。
● updateMany:更新所有满足要求的数据。 - 删除数据
慎用删除功能。一般工程上会使用“假删除”,即:在文档里面增加一个字段“deleted”,如果值为0则表示没有删除,如果值为1则表示已经被删除了。 - 去重操作
去重操作用到的命令为“distinct()”。
distinct()可以接收两个参数:
● 第1个参数为字段名,表示对哪一个字段进行去重。
● 第2个参数就是查询命令“find()”的第1个参数。distinct命令的第2个参数可以省略。 - 使用Python操作MongoDB
使用Python操作MongoDB需要使用一个第三方库——PyMongo。Redis
Redis 没有官方的 Windows 版本。下载Windows版本Redis 推荐 https://github.com/tporadowski/redis/releases 。
在大多数情况下,Redis的生产环境都会被部署在Linux中。在Linux中安装Redis主要有三种方法:
(1)使用包管理器安装。
(2)从源代码编译安装。
(3)使用Docker安装。
Redis 的在线练习环境。网址为:http://try.redis.io。字符串(Strings)
字符串(Strings)是Redis的基本数据结构之一。它由Key和Value两部分组成。在工程上,Redis的字符串常用来记录简单的映射关系。
提示:
(1)字符串只应用在小量级的数据记录中。如果数据量超过百万级别,那么使用字符串来保存简单的映射关系将会浪费大量内存。此时需要使用Redis的另一种数据结构——Hash。储存相同量级的数据,Hash 结构消耗的内存只有字符串结构的1/4,但查询速度却不会比字符串差。
(2)如果Redis中有大量Key,那么执行“keys *”命令会对Redis性能造成短暂影响,甚至导致Redis失去响应。因此,绝对不应该在不清楚当前有多少Key的情况下冒然列出当前所有的Key。列表(Lists)
列表(Lists)是Redis中的另一种基本数据结构。
列表分左右两个方向,所以可以从左右两侧插入。“插入(Insert)”可以理解为“推入(Push)”。又由于“左(Left)”的首字母为“L”,“右(Right)”的首字母为“R”,所以,从列表左侧插入数据的命令为“LPush”,从列表右侧插入数据的命令为“RPush”。
Redis的命令是不区分大小写的,所以一般用小写“lpush”和“rpush”便于辨认。
向列表中插入数据的命令为:12lpush key value1 value2 value3rpush key value1 value2 value3
其中,Key的命名要求与字符串一样,可以是数字、字母下划线和中文,但不建议使用中文。Value可以有1个或者多个。如有多个value,应使用空格将它们隔开。如果一个value内部本身就有空格,那么就使用引号包起来。
由于一个列表里面可以存放非常多的数据,因此,可以使用命令“llen”来查看列表的长度。
与Python的列表一样,Redis的列表也是有“索引”的。可以使用命令“lrange”来根据索引查看数据。
索引从最左边开始编号,从0到“列表长度-1”。Redis的列表也支持“负索引”,索引“-1”表示最右边的数据,“-2”表示右数第2个数据,以此类推。
如果要查看列表的所有数据,可以使用命令:lrange key 0 -1
。
先查看列表的长度,如确定数据量很小,则列出所有的值;如果数据量很大,则可以使用索引查看头几条数据与末尾几条数据。
从左边弹出数据使用的命令为“lpop”,从右边弹出数据使用的命令为“rpop”。需要注意的是,在弹出数据的同时,被弹出的这个数据也会被从列表中删除。
Redis的列表,可以根据数据的索引修改数据,使用的命令是“lset”,命令格式为:lset key index 新的值
在工程上,Redis列表一般用来作为一个队列,存放一批可以使用相同逻辑处理的数据。
集合(Sets)
集合(Sets)是Redis的基本数据结构之一。
Redis中的集合与列表一样可以存放很多数据,但不同之处在于:集合里面的数据不能重复,也没有顺序。由于没有顺序,所以自然没有方向,不存在“左右侧”之说。
在工程中,Redis的集合一般有两种用途:
(1)根据集合内数据不重复的特性实现去重并记录信息。
(2)利用多个集合计算交集、并集和差集。
- 插入数据
集合的首字母为“S”,“添加”的英文为“Add”,所以向集合中添加数据的命令为“sadd”,命令格式如下:sadd key value1 value2 value3
● 数据插入命令执行的先后顺序无关紧要。
● 在一条命令中,数据位于“value1”还是“value3”也无关紧要。 - 查询集合里面元素的数量。
查询集合数据量的命令是scard。其中,首字母“s”是“集合(Sets)”的首字母,“card”不是英文单词“卡片(Card)”,而是“基数(Cardinality)”的缩写。 - 从集合中获取数据。
从集合中获取数据使用的命令为“spop”。由于集合里面的数据没有顺序,所以spop命令会随机获取集合中的数据,无法预测会获取哪一条数据。
“spop”命令的格式如下:spop key count
其中,如果“count”省略,则表示随机获取1条数据。
● 如果“count”为其他大于1的整数,则会获取多条数据;
● 如果“count”对应的整数超过了集合总数据的条数,则获取集合中的所有数据
获取一条数据后,这一条数据就被会被从集合中删除。 - 获取集合中的所有数据。
如果要获取所有数据,则可以使用以下命令:smembers key
提示:
smembers命令不会删除数据。但是如果集合里的数据量极大,就应该慎重使用“获取所有数据”,因为这样会导致系统的I/O资源瞬间被耗尽。 - 判断集合中是否包含某个元素。
sadd命令在遇到数据已经存在时,会返回“0”,如果数据不存则把数据插入再返回“1”。所以,这一条命令可以通过返回的数字来判断数据是否存在。
如果不想把数据插入集合,只是单纯想检查数据是否在集合中,那就要使用“sismember”命令。“sismember”命令的使用格式如下:sismember key value
如果数据存在,则返回“1”;如果数据不存在,则返回“0”。 - 删除数据
如果要从集合中删除特定的数据,可以使用命令“srem”,格式为:srem key value1 value2 value3
- 集合的交集
有两个集合A和B。交集是指,既属于A,又属于B的数据构成的集合。
在Redis中,求集合交集使用的命令为“sinter”,命令格式如下:sinter key1 key2 key3
如果有多个key,那么就是求所有key对应的集合的交集。 - 集合的并集
集合的“并集”是指,只属于集合A的数据与只属于集合B的数据,以及既属于A又属于B的数据构成的集合,集合A与集合B都有的数据需要去重。
求集合并集使用的命令为“sunion”,命令格式如下:sunion key1 key2 key3
如果有超过两个key,那么就是求所有集合的并集。 - 集合的差集
集合的“差集”是指,只属于一个集合,不属于其他集合的数据构成的集合。
求集合差集使用的命令是“sdiff”,命令格式如下:sdiff key1 key2 key3
意思是,求所有只在key1对应的集合中有,在key2、key3……集合中没有的数据构成的集合。第3篇 高级应用
MongoDB的高级语法
- AND和OR操作
随着查询的条件越来越复杂,MongoDB查询语句中的括号会越来越多,因此要养成先把括号闭合,再填写里面内容的习惯。这样才不容易漏掉括号的后半部分,也不会把大括号中括号小括号的后半部分顺序搞错。
OR操作与显式AND操作的格式完全一样,只需要把关键字“$and”换成“$or”即可。OR操作会自动按顺序去检查每一个条件,直到某一个查询条件找到至少一条数据为止。
MongoDB在执行OR操作时会遵循一个“短路原则”:只要前面的条件满足了,那后面的条件就直接跳过。OR操作一定是显式的,不存在隐式的OR操作。 - 查询子文档或数组中的数据
如要查询嵌套字段,则需要使用点号来指定具体的字段名,格式如下:嵌入式文档名.嵌套字段名
● 返回嵌套字段中的特定内容:
如需要在返回的查询结果中只显示嵌入式文档中的部分内容,也可以使用点号来实现。
● 根据数组的长度查询数据也非常简单,使用关键字“$size”。
“$size”只能查询具体某一个长度的数组,不能查询长度大于或小于某个值的数组。
● 根据数组索引查询数据
数组和列表一样,也有一个索引。通过这个索引能够定位到数组中的具体某个数据。
索引是从0开始的,索引为“0”表示数组中的第1个数据,索引为“1”表示数组中的第2个数据。根据索引查询某个值,也需要使用点号。 - MongoDB的聚合查询
MongoDB 自带了一个聚合(Aggregation)功能。使用聚合功能,可以直接让MongoDB来处理数据。
聚合操作的命令为“aggregate”,聚合操作可以有0个、1个或者多个阶段。
如果聚合有至少一个阶段,那么每一个阶段都是一个字典。不同的阶段负责不同的事情,每一个阶段有一个关键字。有专门负责筛选数据的阶段“$match”,有专门负责字段相关的阶段“$project”,有专门负责数据分组的阶段“$group”等。
一般情况下,并非所有的数据都需要被处理,因此大多数时候聚合的第一个阶段是数据筛选。就像“find()”一样,把某些满足条件的数据选出来以便后面做进一步处理。
在“$project”中,如果一个字段的值不是“0”或“1”,而是一个普通的字符串,那么最后的结果就是直接输出这个普通字符串,无论数据集中原本是否有这个字段。
由于特殊字段的值和“$project”的自身语法冲突了,导致所有以“$”开头的普通字符串和数字都不能添加。要解决这个问题,就需要使用另一个关键字“$literal”
分组操作对应的关键字为“$group”,它的作用是根据给出的字段Key,把所有Key的值相同的记录放在一起进行运算。这些运行包括常见的“求和($sum)”“计算平均数($avg)”“最大值($max)”“最小值($min)”等。
分组操作虽然也能实现去重操作,但是它返回的数据格式与“distinct”函数是不一样的。“distinct”函数返回的是数组,而分组操作返回的是3条记录。
提示:
原则上,“$sum”和“$avg”的值对应的字段的值应该都是数字。如果强行使用值为非数字的字段,那么“$sum”会返回0,“$avg”会返回“null”。而字符串是可以比较大小的,所以,“$max”与“$min”可以正常应用到字符串型的字段。
其中,“$sum”的值还可以使用数字“1”,这样查询语句就变成了统计每一个分组内有多少条记录。
关键字“$last”表示取最后一条记录。在 MongoDB 中,老数据先插入,新数据后插入,所以每一组的最后一条就是最新插入的数据。
在英语中,“last”的反义词是“first”,所以关键字“$first”的意思是取第一条,即是最早插入的数据。
拆分数组阶段使用的关键字为“$unwind”,它的作用是把一条包含数组的记录拆分为很多条记录,每条记录拥有数组中的一个元素。
所谓的联集合查询,相当于SQL中的联表查询。在某些情况下,一些相关的数据需要保存到多个集合中,然后使用某一个字段来进行关联。
联集合查询的关键字为“$lookup”,其中的“主集合”与“被查集合”需要搞清楚。MongoDB的优化和安全建议
MongoDB默认没有密码,且只允许本地访问。如果开放外网访问,就一定要设置密码,否则会有安全隐患。提高MongoDB读写性能
- 批量插入一次性数据
如果已经明确知道 Redis 中的数据就是全部数据,虽然多,但是不会继续增加新的数据,那么为了安全起见,是小批量插入。每从 Redis 中读取1000条数据就插入一次数据库。这样做的好处是,即使电脑断电,最多丢失1000条数据。当然,这里需要根据系统能够容忍的最大丢失数据条数来设置。 - 批量插入持续性数据
分批次批量插入持续性数据。
在本次发现Redis为空的情况下,暂停0.1秒,这样做可以显著降低CPU的占用。 - 用插入数据代替更新数据
更新操作(特别是逐条更新)比较费时间,因为它实际上包含“查询”和“修改”两个步骤。与“插入”不一样,某些情况下数据的“更新”没有办法实现批量操作,必需逐条更新。
对于必需逐条更新大量数据的情况,也可以使用插入代替更新来提高性能。
基本逻辑是:把数据插入到另一个集合中,然后删除原来的集合,再把新集合改名为原来的集合。 - 使用“索引”提高查询速度
在一个集合的数据量到达千万量级以后,查询速度会变得非常缓慢,这时就需要使用索引来加快查询速度。索引是一种特殊的数据结构,它使用了能够快速遍历的形式记录了集合中数据的位置。如果不使用索引,则每一次查询数据 MongoDB 都会遍历整个集合;而如果使用了索引,则MongoDB会直接根据索引快速找到需要的内容。
索引是以空间换时间。集合中的数据越多,索引占用的硬盘空间就越多。所以,只对必要的字段添加索引,不要对所有字段都添加索引。_id
默认自带索引,不需要添加。 - 引入Redis,以降低MongoDB的读取频率
(1)读取MongoDB的数据并存入Redis集合中。
(2)使用Redis集合的“sadd”命令,在判断数据是否存在的同时添加新的数据。
即使字段有了索引,但如果程序频繁读取MongoDB,还是会影响性能。
由于Redis的读写速度远远快于MongoDB,因此使用Redis可避免频繁读取MongoDB从而大大提高程序性能。 - 增添适当冗余信息,以提高查询速度
所谓的冗余信息,也就是“多余的信息”,即根据其他已有信息可以推算出来的信息。但有时多余的信息对提高查询性能反而有帮助。提高MongoDB的安全性
- 配置权限管理机制
为了增强MongoDB的安全性,需要配置基于角色的访问控制(Role-Based Access Control, RBAC)机制。
RBAC机制涉及三个关键定义:角色(Roles)、特权(Privileges)和用户(Users)。
管理员用户的作用是创建其他用户。管理员用户本身不能对数据库进行控制。
管理员(admin账号)能创建其他用户,看似权限非常大,但它不能访问任何一个数据库。所以,如果有必要,还需要创建一个能对所有数据库都有全部权限的用户。
能力越大责任越大,请慎重考虑是否有必要添加root用户。
一旦允许 MongoDB 接收外网访问,那一定要设置用户名和密码,同时最好配置防火墙,指定只允许哪些来源的IP可以访问MongoDB端口。Redis的高级数据结构
哈希表(Hash Table)
哈希表(Hash Table)是一种数据结构,它实现了“键-值”(Key-Value)的映射。
在Redis中,使用哈希表可以保存大量数据,且无论有多少数据,查询时间始终保持不变。Redis的一个哈希表里面可以储存232 ─1(约等于43亿)个键值对。
在Redis中,Key中的冒号就是普通的字符,用来分割前缀和后缀,没有什么特殊意义。
使用哈希表不仅可以减少Redis的个数,还能优化储存空间。Redis官方就特别说明,哈希表对存储结构进行过特殊的优化,储存相同的内容,占用的内存比字符串要小很多。 - 添加数据
向哈希表中添加数据,使用的方法名为hset或者hmset。
● hset一次只能添加一个键值对。
● hmset一次可以添加多个键值对。 - 读取数据
使用4个不同的命令(hkeys、hget、hmget和hgetall)从哈希表中读取数据
hkeys用于获取所有字段的字段名,返回的数据是包含bytes型数据的列表。
● hget:获取一个字段的值。
● hmget:一次性获取多个字段的值。
● hgetall:获取一个哈希表中的所有字段名和值。 - 判断一个哈希表中是否有某个字段。
如果要判断一个哈希表中是否有某个字段,有两个方法:
(1)获取这个字段的值,如果值为None,则这个字段就是不存在的。
(2)使用hexists方法。 - 查看一个哈希表中有多少个字段。
使用hlen方法。有序集合(Sorted Set)
有序集合(Sorted Set)是Redis的一个数据结构。
有序集合里面的数据跟集合一样,也是不能重复的,但是每一个元素又关联了一个分数(Score),根据这个分数可以对元素进行排序。分数可以重复。
有序集合还能直接修改某一个值的分数,从而直接改变排序。
- 向有序集合添加数据
向有序集合添加数据,使用的方法为“zadd”。它的格式有两种:
方法一:client.zadd(’有序集合名’, 值1, 评分1, 值2, 评分2, 值n, 评分n)
方法二:client.zadd(’有序集合’, 值1=评分1, 值2=评分2, 值3=评分3)
这两种方式的效果是一样的,但是第1种的值可以使用变量,而第2种的值不能使用变量。 - 修改评分
修改评分使用的方法名为“zincrby”,格式如下:
client.zincrby(’有序集合名’, 值,改变量) - 对有序集合元素基于评分范围进行排序
● zrangebyscore根据评分按照从小到大的顺序排序。
● zrevrangebyscore根据评分按照从大到小的顺序排序。
使用格式如下:
client.zrangebyscore(’有序集合名’, 评分上限, 评分下限, 结果切片起始位置, 结果数量, withscores=False)
client.zrevrangebyscore(’有序集合名’, 评分上限, 评分下限, 结果切片起始位置, 结果数量, withscores=False)
其中,评分上限、评分下限用于确定排序的范围。
结果切片起始位置、结果数量这两个参数可以同时省略,省略表示返回排序后的所有数据。
如果withscores设置为False,则返回的结果直接是排序好的值。
如果withscores设置为True,则返回的列表里面的元素是元组。元组的第1个元素是值,第2个元素是评分。 - 对有序集合基于位置进行排序
● zrange对评分按照从小到大的顺序排序。
● zrevrange对评分按照从大到小的顺序排序。
用法如下:
client.zrange(’有序集合名’, 开始位置(含), 结束位置(含), desc=False, withscores=False)
client.zrevrange(’有序集合名’, 开始位置(含), 结束位置(含), withscores=False)
这两个方法,根据0开始的索引找到需要排序的元素范围,然后对这个范围内的数据进行排序。
提示:
如果使用zrange方法,同时desc=True,那在底层会自动调用zrevrange方法。因此,如果使用zrange,开始位置为“0”,结束位置为“4”,参数desc=True,则它的作用是取最大的5个元素。 - 根据值查询排名,根据值查询评分
(1)使用zrank和zrevrank方法,可以查询一个值在有序列表中的排名。
(2)使用zscore可以查询一个值的评分。 - 其他常用方法
(1)查询有序集合里面一共有多少个值,使用的方法名为“zcard”。
(2)查询在某个评分范围内的值有多少,使用的方法名为zcount。Redis的安全管理
● 设置密码并开放外网访问
● 禁用危险命令
Redis中默认开启了一些非常高权限的命令。
通过修改Redis的配置文件,可以对一些危险命令进行改名或者禁用,从而降低安全风险。如果把命令重命名为空字符串,表示禁用这个命令。对于一些比较危险但可能会用到的命令,可以把它改名;对于一些特别危险的命令,可以禁用。MongoDB的常见陷阱
默认超时时间
游标(Cursor)可以理解为一个标记,用来指向本次查询的数据位置。每读取一次数据,游标就向下移动一条数据。
PyMongo 的 find()方法返回的就是一个游标对象,当使用 for 循环对游标进行迭代时, PyMongo 才会读取数据。游标对象有一个方法叫作“batch_size”,它的作用是限制PyMongo每一次连接MongoDB批量读取多少条数据。
还有一种办法——设置游标永久有效。collection.find()可以设置一个参数:no_cursor_timeout。如果把这个参数设置为True,则游标就不会过期
提示:
第二种做法应谨慎使用。因为一旦设置游标永不超时,那么使用完成以后必须手动关闭游标,否则它将会一直占用MongoDB的资源(即使Python程序已经关闭了,被占用的资源也不会自动释放),直到重启MongoDB。硬盘空间的使用
由于 MongoDB 储存空间优化是数据库工程师的工作,不是本书需要考虑的内容,因此这里介绍一个通用又简单的解决办法:
(1)把新的数据写入新的集合中。
(2)老数据里需要留下的部分也重新插入新的集合。
(3)删除老集合。
(4)重建索引。
使用Redis的注意事项
- 在一台服务器上运行Redis的多个实例
由于Redis服务的启动命令为:redis-server 配置文件路径
所以,只要有多个配置文件,每个配置文件里面保证端口号、日志路径、pid文件路径、数据文件路径不同,就可以通过多次运行此命令来启动多个Redis实例。 - 使用Redis自带的16个数据库
一个Redis实例,实际上自带了16个命名空间互相隔离的数据库。
Redis的一个实例自带了16个数据库,编号为0~15。在终端里可以使用以下命令进入不同的数据库。如果省略“-n”参数,表示使用“0”号数据库。redis-cli –n 数据库编号
默认数据库的数量是16,可以通过修改Redis的配置文件来增加可用的数据库的个数。
● 单实例多数据库的弊端:
由于Redis是单线程的数据库,所以,一个实例里的多个数据库的Key可以同名,且互不冲突。但是,一旦其中一个数据库卡住(例如对几百万个Key执行“keys *”命令),那么其他数据库也不能正常使用。一旦对某一个数据库进行了一个比较耗时的操作,那么对其他数据库的操作都会受到影响。一个Redis实例的所有数据库都只能共享CPU的一个核。
而如果通过多个配置文件启动多个 Redis 实例,则不会存在这种问题,即使一个实例卡死了,其他的实例仍能正常工作。 - 尽可能为每个Key设置过期时间
尽可能为每个Key设置合理的过期时间,这样即使忘记清理,到时间以后Redis也会自动把它删除,从而有效释放内存空间。
字符串有一个ex参数,表示过期时间。而对于其他数据结构,可以使用expire方法来设置过期时间