前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis详解(1)——为什么我们一定要了解Redis

Redis详解(1)——为什么我们一定要了解Redis

原创
作者头像
eedalong
修改2019-12-02 15:04:12
1.1K0
修改2019-12-02 15:04:12
举报

一、前言

从我第一次使用Redis来帮助加速算法运行速度开始,就把Redis应用在了各个项目中,每次带来的体验都非常得好,python多进程+Redis的使用帮助我把单进程运行十几个小时的程序加速到了只需要10分钟左右,也帮助我把本来需要运行十几分钟的项目加速到了几十秒就能运行结束,同时我也喜欢Redis项目本身的小巧和精致。所以在这里计划写些关于Redis的介绍,计划总共写两篇,第一篇主要介绍Redis的整体的一些设计和思想,第二篇会主要介绍Redis集群的一些研究,希望能帮助大家熟悉认识Redis,并鼓励在你的项目中能尝试使用Redis。本篇主要会涉及到如下内容:

  • Redis是什么
  • 为什么Redis速度能够这么快
  • Redis支持写入的数据结构都有哪些及其底层实现方式是什么
  • 内存资源稀缺,能够存储的键值数目有限,当Redis键值存不下时,该如何淘汰掉已有的键
  • Redis进程在内存中存储数据,如果Redis进程崩溃了,进程中的数据会丢失,那么Redis如何利用持久化来保证数据的安全性
  • Redis的python程序实例及一些常用的高效使用手段

二、Redis是什么

Redis的全称是REmote DIctionary Server,是一个高效的内存键值数据库,相比较我们常规使用的Mysql、MongoDB等数据库,Redis的最大特点在于数据读写全部在内存中进行,进而带来极大的效率优势。相比较其他的内存键值存储系统如Memcached, Redis支持更多的数据结构,提升了使用的易用性。同时Redis采用典型的CS架构, 并且有着非常丰富的不同语言客户端支持,本篇文章的最后也会向大家介绍同步和异步模式下的两个python语言的Redis客户端使用。

Redis采用CS架构
Redis采用CS架构

三、Redis为什么这么快

Redis最大的好处就是快,Redis为什么能做到这么快呢?主要的原因有三点

  • 数据读写都在内存中完成。从下图中我们可以看出,即使使用SSD,内存的读写速度要比外存的数据的读写速度快1000倍左右,如果你的电脑还没装上SSD,还是机械硬盘,那内存的读写速度比硬盘的读写速度就要快100000倍,那么基于内存的数据库的读写速度优势自然就是巨大的。

不同存储层次的访问速度对比
不同存储层次的访问速度对比
  • 单线程请求处理,这个主要是实现上的选择。也许同学会有疑惑,为什么不采用多线程进行并行读写呢?这里主要的原因仍然是Redis基于内存读写,多线程并行对数据读取的确能带来好处,但是同样带来了数据写入时锁的开销以及线程切换的开销。再大量的写入情况下,过多的锁带来的时间消耗比多线程带来的多核利用优势更大。
  • I/O多路复用技术。I/O多路复用我们又称之为事件驱动,Redis基于epoll等技术实现了网络部分响应的同步非阻塞I/O模式。Redis的I/O主要集中在了读写socket上,同步阻塞下,向客户端发送数据的时候,Redis需要一直等到对应客户端的socket可写才会去写,直到写完了再服务下一个请求,使用epoll等系统调用,把socket是否可读写的状态监控交给了操作系统,即Redis只会在操作系统告知其可读或者可写的socket文件的时候采取读写,进而节省了等IO的时间。关于epoll的具体介绍可以参考这一篇文章

以上三点是Redis为什么这么快的原因,内存读写是最主要的,其他两个技术选型对此也有所帮助。

四、Redis支持的数据结构

我们要把数据存到内存里面,怎么存呢?理论上来讲,内存KV数据存储其实只需要支持字符串数据存取就能支持所有的数据类型存储了,至于列表、字典的存储,我们只需要将数据进行序列化就行。缺点就是用户每次要修改数据都要获得所有的数据,修改结束之后还得把所有的数据再传回去,这样不但增加了每次网络的传输数据体积,而且使用体验也不是很好,因为需要用户自己来解析数据,事实上这就是Memcached的做法。Redis为了提高易用性,支持了更加丰富的数据结构,最常用的便是String、List、Hash、Set、Sorted Set五种。接下来我们一一介绍五种数据结构,主要介绍其特点和底层实现,这样我们就好估计每种数据结构的操作时间复杂度。

String

String和我们常规理解的字符串基本一致,主要存储序列化后的字符串,支持写入原生字符串也支持写入数字类型。String的存取复杂度均为O(1)。主要支持的操作如下表

命令

含义

SET

设置键值

GET

获得给定键的值

DEL

删除给定的键

List

List即为列表,List在Redis底层采用的是双向链表实现的,所以我们会发现Redis的List操作命令有左右之分,比如LPUSH、RPUSH,实际上就是双端列表左右两端的存取。对于列表的端点插入和查询时间复杂度为O(1), 对于中间某个index的位置的值的获取以及对于index处于[start, end]的连续多个值的读取就是O(n)的复杂度(n为列表的长度),在我们的项目中,我们用List来存储疾病列表,来帮助实现用户搜索疾病时的即时自动补全。列表的主要命令如下:

命令

含义

LPUSH/RPUSH

向列表的左端/右端插入数据

LPOP/RPOP

从列表的左端/右端删除数据

LRANGE/RANGE

去除从左/右端开始计数的位置处于给定[start, end]之间的所有value

LINDEX/RINDEX

删除从左/右端开始计数的第INDEX个值

Hash

Hash可以理解为我们常规使用的字典数据结构,Redis采用散列表来实现Hash, 一个Hash结构里面可以存在很多的key和value,Hash是Redis比较推荐使用的一种数据结构,据说内存使用会更好,具体我还没有研究。在我们的项目里,我们主要用Hash保存用户的token信息来帮助快速验证用户是否已登录。Hash中的键值存取效率可以认为是O(1),Hash结构操作的主要命令如下表

命令

含义

HSET

向Hash中添加k:v

HGET

获取Hash中的给定key的值

HKEYS

获取Hash中所有的key

Set

Set是集合,满足集合确定性、无序性、唯一性三个性质,可以用来进行元素的去重操作。集合的底层实现仍然采用散列表,所以单个元素的存取可以认为是O(1)的时间复杂度,同时Redis支持对不同的集合的交并等计算,集合的操作命令主要如下

命令

含义

SADD

向集合中添加元素

SISMEMBER

判断键是否在集合中

SMEMBERS

获取集合中所有的键

SREM

删除集合中的给定的键

Sorted Set

Sorted Set是有序集合,满足集合唯一性的要求,同时也满足有序的性质。向Sorted Set中插入元素的时候需要同时指定一个Score,用于作为排序的标准,如下面的这条命令,我们向知乎热榜这个有序集合中插入一个文章的题目及其阅读量,通过有知乎热榜这个有序结合我们可以方便的得到每天排名靠前的文章题目及其对应的阅读量。 Sorted Set的底层实现采用的是Skip List, 所以其单个元素的存取效率可以近似认为是O(logn)的。有序集合的操作命令主要如下:

代码语言:javascript
复制
ZADD 知乎热榜 2000 如何看待xxxxx

命令

含义

示例

ZADD

向有序集合中添加元素

ZADD 知乎热榜 2000 如何看待xxxx

ZREM

删除集合中的元素

ZREM 知乎热榜 如何看待xxxx

ZRANGE

获取有序集合中排名处于[start, end]之间的值

ZRANGE 知乎热榜 0 10

ZSCORE

获取集合中给定键的score

ZSCORE 知乎热榜 如何看待xxxx

五、Redis键淘汰策略

前文提过,Redis的所有数据是存储在内存中的,但是内存本身就是稀缺资源,我们常规使用的笔记本内存只有8G或者16G,而且这个内存是给所有的进程使用的,Redis作为我们运行的其中一个进程我们一般会限制Redis的使用内存上限,比如2G,否则Redis就会把可用内存耗光。2G实际上能存储的键值是有限的,那么如果用户把Redis的存储存满了该怎么办呢?就像我们把家里的冰箱都装满了,再想装东西就得扔掉一部分不吃的东西或者过期的东西一样,Redis也会选择淘汰掉一些键来为新的键提供空间。同时Redis支持用户给键值设置过期时间,如果检查到某些键过期了,就删除掉键来空余出空间。为了方便管理,Redis把所有设置了过期时间的键放到一个单独的列表里面进行维护。这里我们主要介绍三类策略:

不淘汰策略

第一条淘汰键的策略就是不淘汰哈哈,其实是表明Redis不主动清除键,清除键的操作全部交给用户来决定,如果用户始终不清除键,当Redis被写满了后,用户在往里面写Redis就会报错,拒绝写入数据。这种策略叫noeviction。

随机淘汰

随机抽样淘汰即Redis随机选取一些键然后进行删除,这样带来的问题是用户也不知道哪些键被删除了,可能用户吃着火锅唱着歌,回头一看,自己的数据没了!那显然是很糟糕的,但Redis提供了这样一个选项,用不用那自然是用户的选择问题了。根据随机抽样的集合不同又细分为两个策略,从所有的键中随机抽取就是allkeys-random, 从只设置了过期时间的键集合中进行抽取,就是volatile-random

LRU策略

LRU策略就是淘汰掉最不常用的键,每次用户访问某个键的时候,Redis就会记录这个键的访问时间,如果一个键距离上次访问已经太久没有被访问到了,那么Redis就认为这个键用户用不上了,就会把键清除掉。按照标准的LRU算法,我们应该统计所有键中最不常用的键,然后淘汰掉他,但是Redis是单线程响应用户请求的,不能每次都遍历所有的键来进行检查,否则就会严重的影响到服务的响应。所以Redis采用一种随机抽样的方法。即每次随机抽取K个键值,然后淘汰掉这K个键中上次访问时间最早的的那个键。同样,针对随机收取的集合不同又细分为两个策略,从所有的键中进行抽取,就是allkeys-lru策略,从只设置了过期时间的键集合中进行抽取,就是volatile-lru策略。volatile-lru策略是比较推荐的一种策略。关于LRU的策略,Redis的源码实现如下,我加了注释,还比较易懂

代码语言:javascript
复制
......
            /* 如果选择了volatile-lru 或者 allkeys-lru 策略 */
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
            {
                
                /*每次随机抽取maxmeory_samples个元素进行检查淘汰,默认设置为3*/
                for (k = 0; k < server.maxmemory_samples; k++) {
                    sds thiskey;
                    long thisval;
                    robj *o;
                    /*随机抽取一个键*/
                    de = dictGetRandomKey(dict);
                    thiskey = dictGetKey(de);
                    /*如果用户设置的是volatile-lru,则从设置了有效期的集合中进行抽样*/
                    if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
                        de = dictFind(db->dict, thiskey);
                    o = dictGetVal(de);
                    thisval = estimateObjectIdleTime(o);

                    /* 找到距离上次访问过去时间最久的键*/
                    if (bestkey == NULL || thisval > bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }
            }
            ......

六、Redis持久化策略

Redis是把数据存储在自己进程的内存中,但是如果Redis进程挂了或者说电脑断电了,那么存储的数据就全部丢失了。为了保证数据的安全性,就需要把数据从内存的数据备份到硬盘上,这就是持久化操作。这样即使内存中的数据丢失了,那么也可以从硬盘上把数据恢复出来。Redis提供两种持久化策略:RDB持久化和AOF持久化,不要被这两个名字吓到,RDB,AOF只是两种持久化文件的后缀名,并不是什么神奇的策略。都比较容易懂,下面一一介绍。

RDB持久化

RDB持久化就是快照持久化,即定期把内存中的数据全部拷贝保存到文件中。我们前面提到Redis是单线程响应用户需求的,如果把持久化这样涉及到大量IO的操作也放到这个线程中,会严重影响服务的响应。于是Redis采用fork一个子进程出来进行持久化。但是我们都知道,fork出来的子进程会拷贝父进程所有的数据,这样理论上当Redis要持久化2G的内存数据的时候,子进程也会占据几乎2G的内存,那么Redis相关的进程内存占用就会达到4G左右,这在数据比较小的时候还不严重,但是比如你的电脑内存是8G, 目前备份的Redis的数据本身体积是5G,那么按照上面的计算备份一定是无法进行的,所幸在Unix类操作系统上面,做了如下的优化:在刚开始的时候父子进程共享相同的内存,直到父进程或者子进程进行内存的写入后,对被写入的内存共享才结束。这样就会减少Redis持久化时对内存的消耗。

AOF持久化

AOF(AppendOnlyFile)持久化就是Redis把每次的用户写操作日志append到一个文件中,如果数据丢失了,那么按照AOF文件的操作顺序再进行操作一遍就可以恢复数据,而且这样每次我们都只需要写一小部分数据到文件中。但是这样会带来一个什么问题呢?由于程序一直在运行,所以不停的会往AOF文件中添加写的操作日志,这样终有一天AOF文件体积会大到不可想象。所以就又有一个操作叫AOF重写用于删除掉冗余的命令,比如用户对同一个key执行100遍SET操作,如果不AOF重写,那么AOF文件中就会有100条SET记录,数据恢复的时候也需要操作100次SET,但实际上只有最后一条SET命令是真正有意义的,所以AOF重写就会把前99条SET命令删除掉,只保留最后一条SET命令,这样不仅文件内存储的内容就变少了,Redis恢复数据的速度也加快了。

除了上面两条策略,Redis还支持主从备份,这又是一块比较大的内容,限于篇幅,我们将主从备份放到第二篇的Redis集群中介绍。

七、talk is cheap, show me the code

redis-py和aredis

又到了喜闻乐见的代码部分了。这部分主要介绍两个python的Redis客户端,redis-pyaredis前者是同步redis客户端,后者是异步redis客户端。aredis就是在redis-py的基础上利用了协程的技术来重写了接口,试图省去客户端等待服务器结果的时间。如果你是本地机器使用Redis,那么使用前者就能很好的满足你的需求,如果你使用的远端的Redis服务器而且网络还比较差的话,aredis也许会有些帮助。我之前尝试使用aredis客户端与本地运行的Redis服务器搭配使用,发现性能下降了很多,主要的原因就是因为本地Redis服务器网络延迟几乎为0,但过多的协程切换反而带来了高昂的开销。我使用redis-py客户端,处理完需要288s, 用aredis客户端处理完需要340s,后来我重写了客户端的一些接口,把一些协程的接口改成了普通的函数接口,减少了协程数目,运行结束为330s,快了10s。

代码语言:python
复制
# 这里的代码直接从redis-py的documents中粘过来的
>>> import redis
>>> r = redis.Redis(host='localhost', port=6379, db=0)
>>> r.set('foo', 'bar')
True
>>> r.get('foo')
'bar'
代码语言:python
复制
# aredis的操作
import aredis
import asyncio
loop = asyncio.get_event_loop()
r = aredis.StrictRedis(loop=loop)
async def set_key(key, value):
    await r.set(key, value)
    return 

流水线

Redis客户端和服务器的请求响应过程如下图所示,客户端发送一个命令,等待服务器返回结果之后再提交下一个命令。如果网络情况比较差,我们就会需要花许多的时间来等待服务器的响应。一种解决方案就是利用上文提到的aredis,可以在等待响应的同时切换协程做点其他的计算。另一种解决方案就是把所有的命令打包一起发送,然后等服务器计算完了之后把结果一起返回来,这就是流水线的概念。

逐个命令提交(图片出处:https://blog.csdn.net/w1lgy/article/details/84455579)
逐个命令提交(图片出处:https://blog.csdn.net/w1lgy/article/details/84455579)
使用pipeline,进行多个命令一起提交(图片出处:https://blog.csdn.net/w1lgy/article/details/84455579)
使用pipeline,进行多个命令一起提交(图片出处:https://blog.csdn.net/w1lgy/article/details/84455579)

代码如下:

代码语言:python
复制
import redis
r = redis.Redis()
# 使用一个pipeline
pipeline = r.pipeline()
pipeline.set("thu", "No.1")
pipeline.set("xxu", "No.2")
# 把所有的命令打包发送给服务器
pipeline.execute()

八、结语

本篇从Redis的单线程运行、支持的数据结构、到键驱逐策略以及持久化策略几个方面进行介绍,试图给读者一个Redis的全貌,这样使用的时候能对命令有更加清晰的了解,而不只是拘泥于客户端提供的接口。鼓励大家能尝试在自己的项目中使用Redis,相信我,它会给你从未有过的船新体验hh。下篇会主要研究Redis集群的相关内容,如果感兴趣的话,可以考虑订阅下我的专栏hh~。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、Redis是什么
  • 三、Redis为什么这么快
  • 四、Redis支持的数据结构
    • String
      • List
        • Hash
          • Set
            • Sorted Set
            • 五、Redis键淘汰策略
              • 不淘汰策略
                • 随机淘汰
                  • LRU策略
                  • 六、Redis持久化策略
                    • RDB持久化
                      • AOF持久化
                      • 七、talk is cheap, show me the code
                        • redis-py和aredis
                          • 流水线
                          • 八、结语
                          相关产品与服务
                          对象存储
                          对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档