Redis核心技术与实战(极客时间) - 图1

image.png
从这张对比图中,我们可以看到,从SimpleKV演进到Redis,有以下几个重要变化:
Redis主要通过网络框架进行访问,而不再是动态库了,这也使得Redis可以作为一个基础性的网络服务进
行访问,扩大了Redis的应用范围。
Redis数据模型中的value类型很丰富,因此也带来了更多的操作接口,例如面向列表的LPUSH/LPOP,面
向集合的SADD/SREM等。在下节课,我将和你聊聊这些value模型背后的数据结构和操作效率,以及它们
对Redis性能的影响。
Redis的持久化模块能支持两种方式:日志(AOF)和快照(RDB),这两种持久化方式具有不同的优劣
势,影响到Redis的访问性能和可靠性。
SimpleKV是个简单的单机键值数据库,但是,Redis支持高可靠集群和高可扩展集群,因此,Redis中包
含了相应的集群功能支撑模块。

image.png

image.png
哈希桶中的entry元素中保存了key和value指针,分别指向了实际的键和值,这
样一来,即使值是一个集合,也可以通过*value指针被查找到。

因为这个哈希表保存了所有的键值对,所以,我也把它称为全局哈希表 。哈希表的最大好处很明显,就是让
我们可以用O(1)的时间复杂度来快速查找到键值对⸺我们只需要计算键的哈希值,就可以知道它所对应的
哈希桶位置,然后就可以访问相应的entry元素。
你看,这个查找过程主要依赖于哈希计算,和数据量的多少并没有直接关系。也就是说,不管哈希表里有10
万个键还是100万个键,我们只需要一次计算就能找到相应的键。
但是,如果你只是了解了哈希表的O(1)复杂度和快速查找特性,那么,当你往Redis中写入大量数据后,
可能发现操作有时候会突然变慢了。这其实是因为你忽略了一个潜在的⻛险点,那就是哈希表的冲突问题和
rehash可能带来的操作阻塞。

哈希表冲突问题(哈希桶的数量肯定小于Key的数量)
image.png
什么是哈希rehash?为什么要hash rehash
如果哈希表里写入的数据越来越多,哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过⻓,进而导致这个链上的元素查找耗时⻓,效率降低。
所以,Redis会对哈希表做rehash操作。rehash也就是增加现有的哈希桶数量,让逐渐增多的entry元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

其实,为了使rehash操作更高效,Redis默认使用了两个全局哈希表:哈希表1和哈希表2。一开始,当你刚插入数据时,默认使用哈希表1,此时的哈希表2并没有被分配空间。随着数据逐步增多,Redis开始执行rehash,这个过程分为三步:
1.给哈希表2分配更大的空间,例如是当前哈希表1大小的两倍;
2.把哈希表1中的数据重新映射并拷⻉到哈希表2中;
3.释放哈希表1的空间。

但是会带来一个问题,在第二部分的时候会进行大量的数据拷贝,如果一次性全部拷贝,会阻塞Redis线程,无法服务其他请求,Redis无法读取.为了解决这个问题,redis采用了渐进式rehash——既Redis仍然正常处理客戶端请求,每处理一个请求时,从哈希表1中的第一个索引位置开始,顺带着将这个索引位置上的所有entries拷⻉到哈希表2中;等处理下一个请求时,再顺带拷⻉哈希表1中的下一个索引位置的entries。

image.png

压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在
表头有三个字段zlbytes、zltail和zllen,分别表示列表⻓度、列表尾的偏移量和列表中的entry个数;压缩列
表在表尾还有一个zlend,表示列表结束。

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的⻓度直接定位,
复杂度是O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是O(N)了。
image.png

image.png

集合操作

单元素操作是基础;
范围操作非常耗时;
统计操作通常高效;
例外情况只有几个。

第一,单元素操作,是指每一种集合类型对单个数据实现的增删改查操作 。例如,Hash类型的HGET、
HSET和HDEL,Set类型的SADD、SREM、SRANDMEMBER等。这些操作的复杂度由集合采用的数据结构决
定,例如,HGET、HSET和HDEL是对哈希表做操作,所以它们的复杂度都是O(1);Set类型用哈希表作为底
层数据结构时,它的SADD、SREM、SRANDMEMBER复杂度也是O(1)。
这里,有个地方你需要注意一下,集合类型支持同时对多个元素进行增删改查,例如Hash类型的HMGET和
HMSET,Set类型的SADD也支持同时增加多个元素。此时,这些操作的复杂度,就是由单个元素操作复杂
度和元素个数决定的。例如,HMSET增加M个元素时,复杂度就从O(1)变成O(M)了。

第二,范围操作,是指集合类型中的遍历操作,可以返回集合中的所有数据 ,比如Hash类型的HGETALL和
Set类型的SMEMBERS,或者返回一个范围内的部分数据,比如List类型的LRANGE和ZSet类型的ZRANGE。
这类操作的复杂度一般是O(N),比较耗时,我们应该尽量避免 。
不过,Redis从2.8版本开始提供了SCAN系列操作(包括HSCAN,SSCAN和ZSCAN),这类操作实现了渐进
式遍历,每次只返回有限数量的数据。这样一来,相比于HGETALL、SMEMBERS这类操作来说,就避免了
一次性返回所有元素而导致的Redis阻塞。

第三,统计操作,是指集合类型对集合中所有元素个数的记录 ,例如LLEN和SCARD。这类操作复杂度只有
O(1),这是因为当集合类型采用压缩列表、双向链表、整数数组这些数据结构时,这些结构中专⻔记录了元
素的个数统计,因此可以高效地完成相关操作。

第四,这样一来,对于List类型的LPOP、RPOP、LPUSH、RPUSH这四个操作来说,它们是在列表的头尾增删元
素,这就可以通过偏移量直接定位,所以它们的复杂度也只有O(1),可以实现快速操作。

Redis是单线程,主要是指Redis的网络IO和键值对读写是由一个线程来完成的,这也是Redis对外提供键值存储服务的主要流程 。但Redis的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

image.png

在socket模型中,不同操作调用后会返回不同的套接字类型。socket()方法会返回主动套接字,然后调用
listen()方法,将主动套接字转化为监听套接字,此时,可以监听来自客戶端的连接请求。最后,调用
accept()方法接收到达的客戶端连接,并返回已连接套接字。

image.png

Linux中的IO多路复用机制是指一个线程处理多个IO流,就是我们经常听到的select/epoll机制。简单来说,
在Redis只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字 。

image.png

为了在请求到达时能通知到Redis线程,select/epoll提供了基于事件的回调机制 ,即针对不同事件的发生,
调用相应的处理函数 。

这些事件会被放进一个事件队列,Redis单线程对该事件队列不断进行处理。这样一来,Redis无需一直轮询
是否有请求实际发生,这就可以避免造成CPU资源浪费。同时,Redis在对事件队列中的事件进行处理时,
会调用相应的处理函数,这就实现了基于事件的回调。因为Redis一直在对事件队列进行处理,所以能及时
响应客戶端请求,提升Redis的响应性能。

Redis单线程处理IO请求性能瓶颈主要包括2个方面:
1、任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前
面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
a、操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会
产生耗时;
b、使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lran
gekey0-1一次查询全量数据;
c、大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求
时,耗时都在删除过期key,耗时变⻓;
d、淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些k
ey,也会造成耗时变⻓;
e、AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖
慢Redis的性能;
f、主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,
实例越大,阻塞时间越久;
2、并发量非常大时,单线程读写客戶端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客戶
端数据依旧是同步IO,只能单线程依次读取客戶端的数据,无法利用到CPU多核。
针对问题1,一方面需要业务人员去规避,一方面Redis在4.0推出了lazy-free机制,把bigkey释放内存的
耗时操作放在了异步线程中执行,降低对主线程的影响。
针对问题2,Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客戶端数据,进一
步提升server性能,当然,只是针对客戶端的读写是并行的,每个命令的真正操作依旧是单线程的。[23
赞]