其他 Redis 相关技术沉淀文章
- Redis 源码分析(一) :sds
- Redis 源码分析(二) :ADList
- Redis 源码分析(三) :dict
- Redis 源码分析(四) :intset
- Redis 源码分析(五) :ziplist
- Redis 源码分析(六) :quciklist
- Redis 源码分析(七) :skiplist
- Redis 高可用解决方案总结
基础
1.1 Redis 常用的数据结构
Redis支持五种数据类型:string
(字符串),hash
(哈希),list
(列表),set
(集合)及zset
(sorted set
:有序集合)。
- Sting 、SDS(embstr、raw)
- List qucklist (ziplist、linklist)
- Set (dict、 intset)
- Zset (ziplist 、 skiplist)
- Hash (ziplist、 dict)
- BitMap
- Hyperloglog
- Geo编码
1.2 Redis为什么速度快?
- 基于内存实现
- 数据都存储在内存里,减少了一些不必要的`I/O 操作,操作速率很快。
- 高效的数据结构
- 底层多种数据结构支持不同的数据类型,支持
Redis
存储不同的数据; - 不同数据结构的设计,使得数据存储时间复杂度降到最低。
- 底层多种数据结构支持不同的数据类型,支持
- 合理的数据编码
- 根据字符串的长度及元素的个数适配不同的编码格式。
- 合适的线程模型
I/O
多路复用模型同时监听客户端连接;- 单线程在执行过程中不需要进行上下文切换,减少了耗时。
1.3 为什么 Redis 选择单线程模型
Redis
选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络I/O
操作上;
而Redis
引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对Redis
主线程阻塞的时间,提高执行的效率。
1.4 缓存雪崩、缓存击穿、缓存穿透?
缓存雪崩:
- 大量的KEY过期时间过于集中,导致瞬时很多缓存失效,由此可能导致数据库压力陡然升高。
- 解决方案: 将失效时间随机打乱,如在系统启动预热时设定一定程度上离散的过期时间。
缓存击穿:
- 缓存中某一个KEY过期失效,如果此时有大量请求过来无法命中缓存的KEY,缓存层像被凿开了一个口子一样流入大量数据库查询请求。也算是一种惊群效应。
- 解决方案:双重校验方式从数据库中读取数据到缓存。双重校验:第一层查询缓存失败后,进入临界区,保证同时只有一个请求线程读取数据库,进入临界区后再次尝试缓存,仍然没有命中则查询数据库。
缓存穿透:
- 外部请求不断查询一个系统中不存在的数据,服务无法命中缓存转而每次尝试从数据库中查询。
- 解决方案:
- 对查询结果为空key设置值为null的缓存,牺牲缓存空间换响应时间。
- 把所有非法的key映射到一个bitmap中,通过bitmap拦截。《布隆过滤器》原理
1.5 Redis如何删除过期key?
主动删除:redis
默认每隔一定时间检查已过期key
进行删除, 或者内存不足时触发主动删除机制
Redis
默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的key
,而是采用了一种简单的贪心策略。
- 从过期字典中随机
20
个key
; - 删除这
20
个key
中已经过期的key
; - 如果过期的
key
比率超过1/4
,那就重复步骤1
; - 同时,为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过
25ms
惰性删除:在有请求读写key
时再检查key
是否过期,过期则删除
1.6 Redis内存淘汰策略
noeviction
:不删除策略,内存达到上限时直接返回错误信息allkeys-random
: 针对所有的key
,随机删除一部分allkeys-lru
: 针对所有的key
,优先删除最少使用的volatile-random
: 针对设置了过期时间的key
,随机删除一部分volatile-ttl
: 针对设置了过期时间的key
,优先删除最快过期的key
key
增加一个额外24bit
字段(RedisObject
)存时间戳,每次随机抽样5
个。
上一节提到处理 key
过期方式分为集中处理和懒惰处理,LRU
淘汰不一样,它的处理方式只有懒惰处理。
当 Redis
执行写操作时,发现内存超出maxmemory
,就会执行一次LRU
淘汰算法。
这个算法也很简单,就是随机采样出 5(可以配置) 个 key
,然后淘汰掉最旧的 key
,如果淘汰后内存还是超出 maxmemory
,那就继续随机采样淘汰,直到内存低于maxmemory
为止。
struct RedisObject { // 一共占用16字节
int4 type; // 4bits 类型
int4 encoding; // 4bits 存储格式
int24 lru; // 24bits 记录LRU信息
int32 refcount; // 4bytes
void *ptr; // 8bytes,64-bit system
} robj;
- 不同的对象具有不同的类型
type(4bit)
,同一个类型的type
会有不同的存储形式encoding(4bit)
。 - 为了记录对象的
LRU
信息,使用了24
个bit
的lru
来记录LRU
信息。 - 每个对象都有个引用计数
refcount
,当引用计数为零时,对象就会被销毁,内存被回收。ptr
指针将指向对象内容 (body
) 的具体存储位置。 - 一个
RedisObject
对象头共需要占据16
字节的存储空间。
1.7 Redis如何持久化
AOF
AOF(Append Only File )
: 记录每次redis
的写命令,如对一个key
更新10
次,记录10
条写指令。可以设置每秒一次或者每个写动作发生后追加。会定期Compact
之前的文件
- 优点: 持久化频率高,异常down机时数据丢失少。
- 缺点: 文件大,恢复时相对耗时。
RDB
RDB
:快照持久化。在特点时间点保存全量的数据信息。
- 优点: 恢复时直接将快照文件加载到内存,速度快。
- 缺点: 因为全量数据量大,持久化频率一般设置较低。异常关机时会丢失上次持久化到关机时刻的变更数据。
可以通过SAVE
或者BGSAVE
来生成RDB
文件。
SAVE
命令会阻塞redis
进程,直到RDB
文件生成完毕,在进程阻塞期间,redis
不能处理任何命令请求,这显然是不合适的。
BGSAVE
则是会fork
出一个子进程,然后由子进程去负责生成RDB
文件,父进程还可以继续处理命令请求,不会阻塞进程。
1.8 知道什么是热key吗?热key问题怎么解决?
热key
是指在一个分布式系统(如分布式缓存)中被高频访问和操作的key
。热key
可能会导致访问热点,从而导致单个服务节点的负载过高,系统性能受限甚至过载。针对热key
问题,可以采取以下解决方法:
键值分片:将键值对按照
key
的范围或哈希值进行分片,使得热key
分散到不同的服务节点上。这样可以避免单个节点的过载,并实现负载均衡。热 key local 缓存:提前加载
热key
数据到内存中,如果redis
宕机,走内存查询
1.9 Redis大Key危害?怎么解决
Redis
使用过程中经常会有各种大key
的情况, 比如单个简单的key
存储的value
很大。
由于redis
是单线程运行的,如果一次操作的value
很大会对整个redis
的响应时间造成负面影响,导致IO
网络拥塞。
解决方案:将整存整取的大对象,分拆为多个小对象。可以尝试将对象分拆成几个key-value
。
1.10 了解Redis事务机制吗
redis
通过MULTI
、EXEC
、WATCH
等命令来实现事务机制,事务执行过程将一系列多个命令按照顺序一次性执行,并且在执行期间,事务不会被中断,也不会去执行客户端的其他请求,直到所有命令执行完毕。事务的执行过程如下:
- 服务端收到客户端请求,事务以
MULTI
开始 - 如果客户端正处于事务状态,则会把事务放入队列同时返回给客户端
QUEUED
,反之则直接执行这个命令 - 当收到客户端
EXEC
命令时,WATCH
命令监视整个事务中的key
是否有被修改,如果有则返回空回复到客户端表示失败,否则redis
会遍历整个事务队列,执行队列中保存的所有命令,最后返回结果给客户端
语法错误:语法错误指命令不存在或者命令参数的个数不对。只要有一个命令有语法错误,执行EXEC
命令后Redis
就会直接返回错误,连语法正确的命令也不会执行。
运行错误:运行错误指在命令执行时出现的错误,比如使用散列类型的命令操作集合类型的键,这种错误在实际执行之前Redis是无法发现的,所以在事务里这样的命令是会被Redis接受并执行的。如果事务里的一条命令出现了运行错误,事务里其他的命令依然会继续执行(包括出错命令之后的命令)
使用WATCH检测balance,事务期间balance数据未变动,事务执行成功
WATCH命令用于在事务开始之前监视任意数量的键: 当调用EXEC命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败。
WATCH的机制本身是一个CAS的机制,被监视的key会被保存到一个链表中,如果某个key被修改,那么REDIS_DIRTY_CAS标志将会被打开,这时服务器会拒绝执行事务。
1.11 SDS
redis 3.2
之后,针对不同长度的字符串引入了不同的SDS
数据结构,并且强制内存对齐1
,将内存对齐交给统一的内存分配函数,从而达到节省内存的目的SDS
的字符串长度通过sds->len
来控制,不受限于C
语言字符串\0
,可以存储二进制数据,并且将获取字符串长度的时间复杂度降到了O(1)
SDS
的头和buf
字节数组的内存是连续的,可以通过寻址方式获取SDS
的指针以及flags
值SDS
的拼接扩展有一个内存预分配策略,用空间减少每次拼接的内存重分配可能性SDS
的缩短并不会真正释放掉对应空闲空间SDS
分配内存都会多分配1
个字节用来在buf
的末尾追加一个\0
,在部分场景下可以和C语言字符串保证同样的行为甚至复用部分string.h
的函数
len
记录当前字节数组的长度(不包括\0
),使得获取字符串长度的时间复杂度由O(N)
变为了O(1)
alloc
记录了当前字节数组总共分配的内存大小(不包括\0
)flags
记录了当前字节数组的属性、用来标识到底是sdshdr8
还是sdshdr16
等buf
保存了字符串真正的值以及末尾的一个\0
1.12 ADList
ADList(A generic doubly linked list)
是redis
自定义的一种双向链表,广泛运用于redisClients
、 redisServer
、发布订阅、慢查询、监视器等。(注:3.0及以前还会被运用于list
结构中,在3.2以后被quicklist
取代)。
- 双端:链表节点带有
prev
和next
指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(N
) - 无环:表头节点的
prev
指针和表尾节点的next
都指向NULL
,对立案表的访问时以NULL
为截止 - 表头和表尾:因为链表带有
head
指针和tail
指针,程序获取链表头结点和尾节点的时间复杂度为O(1)
- 长度计数器:链表中存有记录链表长度的属性
len
- 多态:链表节点使用
void*
指针来保存节点值,并且可以通过list
结构的dup
、free
、match
三个属性为节点值设置类型特定函数。
1.13 dict
dict
(dictionary
字典),通常的存储结构是Key-Value
形式的,通过Hash
函数对key
求Hash
值来确定Value
的位置,因此也叫Hash
表,是一种用来解决算法中查找问题的数据结构,默认的算法复杂度接近O(1)
。
需要注意的是创建初始化一个dict
时并没有为buckets
分配空间,table
是赋值为null
的。只有在往dict
里添加dictEntry
节点时才会为buckets
分配空间,真正意义上创建一张hash
表。
什么是Rehash
随着操作的不断执行,hash
表保存的键值对会逐渐的增多或者减少,这时就会暴露一些问题。如果hash
表很大,但是键值对太少,也就是hash
表的负载(dictht->used/dictht->size
)太小,就会有大量的内存浪费;如果hash
表的负载太大,就会影响字典的查找效率。这时候就需要进行rehash
将hash
表的负载控制在一个合理的范围。
(扩容、缩容)Rehash的方式
- 主动
Rehash
,一毫秒执行一次 - 被动
Rehash
,字典的增删改查(CRUD
)调用dictAdd,dicFind,dictDelete,dictGetRandomKey
等函数时,会调用_dictRehashStep
,迁移buckets
中的一个非空bucket
dictht
的负载因子,就是used
与size
的比值,也称装载因子(load factor
)。这个比值越大,哈希值冲突概率越高。当比值[默认]超过5
,会强制进行rehash
。
当哈希表的负载因子小于0.1
时, 程序自动开始对哈希表执行收缩操作。
serverCron->tryResizeHashTables->dictResize->dictExpand
serverCron函数是个心跳函数,调用tryResizeHashTables段为:
1.14 intset
intset
是Redis
内存数据结构之一,用来实现Redis
的Set
结构(当集合元素不大于设定值并且元素都是整数时,就会用intset
作为set
的底层数据结构),它的特点有:
- 元素类型只能为数字。
- 元素有三种类型:
int16_t
、int32_t
、int64_t
。 - 元素有序,不可重复。
intset
和sds
一样,内存连续,就像数组一样。intset
实质就是一个有序数组,内存连续,无重复- 可以看到添加删除元素都比较耗时,查找元素是O(logN)时间复杂度,不适合大规模的数据
- 有三种编码方式,通过升级的方式进行编码切换
- 不支持降级
- 数据使用小端存储
intset
的编码是由最大的一个数决定的,如果有一个数是int64
,那么整个inset
的编码都是int64
。length
是inset
的整数个数,contents
整数数组
1.15 ziplist
ziplist
是redis
节省内存的典型例子之一,这个数据结构通过特殊的编码方式将数据存储在连续的内存中。在3.2
之前是list
的基础数据结构之一,在3.2
之后被quicklist
替代。但是仍然是zset
底层实现之一。
ziplist
是redis
为了节省内存,提升存储效率自定义的一种紧凑的数据结构ziplist
保存着尾节点的偏移量,可以方便的拿到头尾节点- 每一个
entry
都保存着前一个entry
的长度,可以很方便的从尾遍历 - 每个
entry
中都可以保存一个字节数组或整数,不同类型和大小的数据有不同的编码方式 - 添加和删除节点可能会引发连锁更新,极端情况下会更新整个
ziplist
,但是概率很小
1.16 quicklist
quicklist
是redis
在ziplist
和adlist
两种数据结构的基础上融合而成的一个实用的复杂数据结构quicklist
在3.2
之后取代adlist
和ziplist
作为list
的基础数据类型quicklist
的大部分api
都是直接复用ziplist
quicklist
的单个节点最大存储默认为8kb
quicklist
提供了基于lzf
算法的压缩api
,通过将不常用的中间节点数据压缩达到节省内存的目的quicklist
将双向链表和ziplist
两者的优点结合起来,在时间和空间上做了一个均衡,能较大程度上提高Redis
的效率。push
和pop
等操作操作的时间复杂度也都达到了最优。
1.17 skiplist
skiplist
本质上也是一种查找结构,用于解决算法中的查找问题(Searching
),即根据给定的key
,快速查到它所在的位置(或者对应的value
)。
// redis 5.0.2的客户端代码,redis 3.2.x版本最大Level是32
#define ZSKIPLIST_MAXLEVEL 64 /* Should be enough for 2^64 elements */
实际上,Redis
中sorted set
的实现是这样的:
- 当数据较少时,
sorted set
是由一个ziplist
来实现的。 - 当数据多的时候,
sorted set
是由一个dict
加一个skiplist
来实现的。简单来讲,dict
用来查询数据到分数的对应关系,而skiplist
用来根据分数查询数据(可能是范围查找)。
现在我们集中精力来看一下sorted set
与skiplist
的关系:
zscore
的查询,不是由skiplist
来提供的,而是由那个dict
来提供的。- 为了支持排名(
rank
),Redis
里对skiplist
做了扩展,使得根据排名能够快速查到数据,或者根据分数查到数据之后,也同时很容易获得排名。而且,根据排名的查找,时间复杂度也为O(log n)
。 zrevrange
的查询,是根据排名查数据,由扩展后的skiplist
来提供。zrevrank
是先在dict
中由数据查到分数,再拿分数到skiplist
中去查找,查到后也同时获得了排名。
总结起来,Redis
中的skiplist
跟前面介绍的经典的skiplist
相比,有如下不同:
- 分数(
score
)允许重复,即skiplist
的key
允许重复。这在最开始介绍的经典skiplist
中是不允许的。 - 在比较时,不仅比较分数(相当于
skiplist
的key
),还比较数据本身。在Redis
的skiplist
实现中,数据本身的内容唯一标识这份数据,而不是由key
来唯一标识。另外,当多个元素分数相同的时候,还需要根据数据内容来进字典排序。 - 第
1
层链表不是一个单向链表,而是一个双向链表。这是为了方便以倒序方式获取一个范围内的元素。 - 在
skiplist
中可以很方便地计算出每个元素的排名(rank
)。
1.18 Redis为什么用skiplist而不用平衡树?
- 内存占用,一个节点平均
1.3
个指针 - 对范围查找的支持,更方便。
- 实现比红黑树跟简单
1.19 skiplist与平衡树、哈希表的比较
skiplist
和各种平衡树(如AVL
、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key
的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。- 在做范围查找的时候,平衡树比
skiplist
操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist
上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。 - 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而
skiplist
的插入和删除只需要修改相邻节点的指针,操作简单又快速。 - 从内存占用上来说,
skiplist
比平衡树更灵活一些。一般来说,平衡树每个节点包含2
个指针(分别指向左右子树),而skiplist
每个节点包含的指针数目平均为1/(1-p)
,具体取决于参数p
的大小。如果像Redis
里的实现一样,取p=1/4
,那么平均每个节点包含1.33
个指针,比平衡树更有优势。 - 查找单个
key
,skiplist
和平衡树的时间复杂度都为O(log n)
,大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1)
,性能更高一些。所以我们平常使用的各种Map
或dictionary
结构,大都是基于哈希表实现的。 - 从算法实现难度上来比较,
skiplist
比平衡树要简单得多。
1.20 Redis的高可用
主从架构
主从模式是最简单的实现高可用的方案,核心就是主从同步。主从同步的原理如下:
- slave发送sync命令到master
- master收到sync之后,执行bgsave,生成RDB全量文件
- master把slave的写命令记录到缓存
- bgsave执行完毕之后,发送RDB文件到slave,slave执行
- master发送缓存中的写命令到slave,slave执行
增量复制
当slave节点与master全量同步后,master节点上数据再次发生更新,就会触发增量复制。
断点续传(continue replication)
断点续传或者说是断点恢复复制,也就是说 slave 因为某种原因与master断开连接了一段时间,然后又与master发生重连。
- 故障恢复复杂,如果没有RedisHA系统(需要开发),当主库节点出现故障时,需要手动将一个从节点晋升为主节点,同时需要通知业务方变更配置,并且需要让其它从库节点去复制新主库节点,整个过程需要人为干预,比较繁琐;
- 主库的写能力受到单机的限制,可以考虑分片;
哨兵
哨兵可以同时监视多个主从服务器,并且在被监视的master下线时,自动将某个slave提升为master,然后由新的master继续接收命令。整个过程如下:
- 初始化sentinel,将普通的redis代码替换成sentinel专用代码
- 初始化masters字典和服务器信息,服务器信息主要保存ip:port,并记录实例的地址和ID
- 创建和master的两个连接,命令连接和订阅连接,并且订阅sentinel:hello频道
- 每隔10秒向master发送info命令,获取master和它下面所有slave的当前信息
- 当发现master有新的slave之后,sentinel和新的slave同样建立两个连接,同时每个10秒发送info命令,更新master信息
- sentinel每隔1秒向所有服务器发送ping命令,如果某台服务器在配置的响应时间内连续返回无效回复,将会被标记为下线状态
- 选举出领头sentinel,领头sentinel需要半数以上的sentinel同意
- 领头sentinel从已下线的的master所有slave中挑选一个,将其转换为master
- 让所有的slave改为从新的master复制数据
- 将原来的master设置为新的master的从服务器,当原来master重新回复连接时,就变成了新master的从服务器
sentinel会每隔1秒向所有实例(包括主从服务器和其他sentinel)发送ping命令,并且根据回复判断是否已经下线,这种方式叫做主观下线。当判断为主观下线时,就会向其他监视的sentinel询问,如果超过半数的投票认为已经是下线状态,则会标记为客观下线状态,同时触发故障转移。
redis集群
如果说依靠哨兵可以实现redis的高可用,如果还想在支持高并发同时容纳海量的数据,那就需要redis集群。redis集群是redis提供的分布式数据存储方案,集群通过数据分片sharding来进行数据的共享,同时提供复制和故障转移的功能。
一个redis集群由多个节点node组成,而多个node之间通过cluster meet命令来进行连接,节点的握手过程:
- 节点A收到客户端的cluster meet命令
- A根据收到的IP地址和端口号,向B发送一条meet消息
- 节点B收到meet消息返回pong
- A知道B收到了meet消息,返回一条ping消息,握手成功
- 最后,节点A将会通过gossip协议把节点B的信息传播给集群中的其他节点,其他节点也将和B进行握手
redis通过集群分片的形式来保存数据,整个集群数据库被分为16384个slot,集群中的每个节点可以处理0-16384个slot,当数据库16384个slot都有节点在处理时,集群处于上线状态,反之只要有一个slot没有得到处理都会处理下线状态。通过cluster addslots命令可以将slot指派给对应节点处理。
slot是一个位数组,数组的长度是16384/8=2048,而数组的每一位用1表示被节点处理,0表示不处理,如图所示的话表示A节点处理0-7的slot。
当客户端向节点发送命令,如果刚好找到slot属于当前节点,那么节点就执行命令,反之,则会返回一个MOVED命令到客户端指引客户端转向正确的节点。(MOVED过程是自动的)
如果增加或者移出节点,对于slot的重新分配也是非常方便的,redis提供了工具帮助实现slot的迁移,整个过程是完全在线的,不需要停止服务。
故障转移
如果节点A向节点B发送ping消息,节点B没有在规定的时间内响应pong,那么节点A会标记节点B为pfail疑似下线状态,同时把B的状态通过消息的形式发送给其他节点,如果超过半数以上的节点都标记B为pfail状态,B就会被标记为fail下线状态,此时将会发生故障转移,优先从复制数据较多的从节点选择一个成为主节点,并且接管下线节点的slot,整个过程和哨兵非常类似,都是基于Raft协议做选举。
1.21 Redis常见的几种缓存策略
- Cache-Aside
- Read-Through
- Write-Through
- Write-Behind
Read-Through
和Cache-Aside
很相似,不同点在于程序不需要再去管理从哪去读数据(缓存还是数据库)。相反它会直接从缓存中读数据,该场景下是缓存去决定从哪查询数据。当我们比较两者的时候这是一个优势因为它会让程序代码变得更简洁。
Write-Through
下所有的写操作都经过缓存,每次我们向缓存中写数据的时候,缓存会把数据持久化到对应的数据库中去,且这两个操作都在一个事务中完成。因此,只有两次都写成功了才是最终写成功了。这的确带来了一些写延迟但是它保证了数据一致性。
Write-Behind
和Write-Through
在“程序只和缓存交互且只能通过缓存写数据”这一点上很相似。不同点在于Write-Through
会把数据立即写入数据库中,而Write-Behind
会在一段时间之后(或是被其他方式触发)把数据一起写入数据库,这个异步写操作是Write-Behind
的最大特点。
1.22 内存回收机制
Redis
并不总是可以将空闲内存立即归还给操作系统。
如果当前Redis
内存有10G
,当你删除了1GB
的key
后,再去观察内存,你会发现内存变化不会太大。原因是操作系统回收内存是以页为单位,如果这个页上只要有一个key
还在使用,那么它就不能被回收。Redis
虽然删除了1GB
的key
,但是这些key
分散到了很多页面中,每个页面都还有其它 key 存在,这就导致了内存不会立即被回收。
不过,如果你执行flushdb
,然后再观察内存会发现内存确实被回收了。原因是所有的key
都干掉了,大部分之前使用的页面都完全干净了,会立即被操作系统回收。Redis
虽然无法保证立即回收已经删除的key
的内存,但是它会重用那些尚未回收的空闲内存。
1.23 容器型数据结构的通用规则
list/set/hash/zset
这四种数据结构是容器型数据结构,它们共享下面两条通用规则:
create if not exists
,如果容器不存在,那就创建一个,再进行操作。比如rpush
操作刚开始是没有列表的,Redis
就会自动创建一个,然后再rpush
进去新元素。drop if no elements
,如果容器里元素没有了,那么立即删除元素,释放内存。这意味着lpop
操作到最后一
个元素,列表就消失了。
1.23 Scan
keys
算法是遍历算法,复杂度是O(n)
,如果实例中有千万级以上的key
,这个指令就会导致Redis
服务卡顿,所有读写Redis
的其它的指令都会被延后甚至会超时报错,因为Redis
是单线程程序,顺序执行所有指令,其它指令必须等到当前的keys
指令执行完了才可以继续。
面对这两个显著的缺点该怎么办呢?
Redis
为了解决这个问题,它在2.8
版本中加入了大海捞针的指令——scan
。scan
相比keys
具备有以下特点:
- 复杂度虽然也是
O(n)
,但是它是通过游标分步进行的,不会阻塞线程; - 提供
limit
参数,可以控制每次返回结果的最大条数,limit
只是一个hint
,返回的结果可多可少; - 同
keys
一样,它也提供模式匹配功能; - 服务器不需要为游标保存状态,游标的唯一状态就是
scan
返回给客户端的游标整数; - 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
- 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
- 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;
scan
的遍历顺序非常特别。它不是从第一维数组的第0
位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。