华为技术专家深度解析Redis惰性删除原理

JavaEdge 发表于 2021/12/26 01:46:02 2021/12/26
【摘要】 Lazy Free会影响缓存替换吗?Redis缓存淘汰是为了在Redis server内存使用量超过阈值时,筛选一些冷数据,从Redis server中删除。我们在前两节课,LRU和LFU在最后淘汰数据时,都会删除被淘汰数据。但它们在删除淘汰数据时,会根据如下配置项决定是否启用Lazy Free(惰性删除)惰性删除,Redis 4.0后功能,使用后台线程执行删除数据的任务,避免了删除操作阻...

Lazy Free会影响缓存替换吗?

Redis缓存淘汰是为了在Redis server内存使用量超过阈值时,筛选一些冷数据,从Redis server中删除。我们在前两节课,LRU和LFU在最后淘汰数据时,都会删除被淘汰数据。

但它们在删除淘汰数据时,会根据如下配置项决定是否启用Lazy Free(惰性删除)

惰性删除,Redis 4.0后功能,使用后台线程执行删除数据的任务,避免了删除操作阻塞主线程。但后台线程异步删除数据能及时释放内存吗?它会影响到Redis缓存的正常使用吗?

1 配置惰性删除

当Redis server希望启动惰性删除时,需在redis.conf设置惰性删除相关配置项,包括如下场景:

默认值都是no。所以,要在缓存淘汰时启用,就要将lazyfree-lazy-eviction置yes。Redis server在启动过程中进行配置参数初始化时,会根据redis.conf,设置全局变量server的lazyfree_lazy_eviction成员变量。

若看到对server.lazyfree_lazy_eviction变量值进行条件判断,那就是Redis根据lazyfree-lazy-eviction配置项,决定是否执行惰性删除。

2 被淘汰数据的删除过程

getMaxmemoryState负责执行数据淘汰,筛选出被淘汰的键值对后,就要开始删除被淘汰的数据:

  1. 为被淘汰的key创建一个SDS对象,然后调用propagateExpire:

Redis server可能针对缓存淘汰场景启用惰性删除,propagateExpire会根据全局变量server.lazyfree_lazy_eviction决定删除操作对应命令:

  • lazyfree_lazy_eviction=1(启用缓存淘汰时的惰性删除),则删除操作对应UNLINK命令

  • 否则,就是DEL命令

因为这些命令经常使用,所以Redis为这些命令创建共享对象,即sharedObjectsStruct结构体,并用一个全局变量shared表示

![image-20211225010625978](/Users/apple/Library/Application Support/typora-user-images/image-20211225010625978.png)

![image-20211225011045661](/Users/apple/Library/Application Support/typora-user-images/image-20211225011045661.png)

在该结构体中包含了指向共享对象的指针,这其中就包括了unlink和del命令对象。

然后,propagateExpire在为删除操作创建命令对象时,就使用了shared变量中的unlink或del对象:

接着,propagateExpire会判断Redis server是否启用AOF日志:

  • 若启用,则propagateExpire会调用feedAppendOnlyFile,把被淘汰key的删除操作记录到AOF文件,保证后续使用AOF文件进行Redis数据库恢复时,可以和恢复前保持一致。通过实现。
    • 然后,propagateExpire调用propagate,把删除操作同步给从节点,以保证主从节点的数据一致。propagate流程:

接下来,performEvictions就会开始执行删除。

  1. performEvictions根据server是否启用了惰性删除,分别执行:
  • Case1:若server启用惰性删除,performEvictions调用dbAsyncDelete异步删除
  • Case2:若server未启用惰性删除,performEvictions调用dbSyncDelete同步删除

performEvictions在调用删除函数前,都会调用zmalloc_used_memory计算当前使用内存量。然后,它在调用删除函数后,会再次调用zmalloc_used_memory函数计算此时的内存使用量,并计算删除操作导致的内存使用量差值,这个差值就是通过删除操作而被释放的内存量。

performEvictions最后把这部分释放的内存量和已释放内存量相加,得到最新内存释放量:

所以performEvictions在选定被删除的KV对后,可通过异步或同步操作来完成数据的实际删除。那数据异步删除和同步删除到底如何执行的?

3 数据删除操作

删除操作包含两步:

  1. 将被淘汰的KV对从哈希表剔除,这哈希表既可能是设置了过期key的哈希表,也可能是全局哈希表
  2. 释放被淘汰KV对所占用的内存空间

若这俩操作一起做,就是同步删除;只做1,而2由后台线程执行,就是异步删除

Redis使用dictGenericDelete实现了这俩操作。

**首先,dictGenericDelete函数会先在哈希表中查找要删除的key。**它会计算被删除key的哈希值,然后根据哈希值找到key所在的哈希桶。

因为不同key的哈希值可能相同,而Redis的哈希表是采用了链式哈希(你可以回顾下第3讲中介绍的链式哈希),所以即使我们根据一个key的哈希值,定位到了它所在的哈希桶,我们也仍然需要在这个哈希桶中去比对查找,这个key是否真的存在。

也正是由于这个原因,dictGenericDelete函数紧接着就会在哈希桶中,进一步比对查找要删除的key。如果找到了,它就先把这个key从哈希表中去除,也就是把这个key从哈希桶的链表中去除。

然后,dictGenericDelete会根据入参nofree,决定是否实际释放K和V的内存空间:

dictGenericDelete根据nofree决定执行同步or异步删除。

dictDelete V.S dictUnlink

给dictGenericDelete传递的nofree参数值是0 or 1:

  • nofree=0:同步删除

  • nofree=1,异步删除

好了,到这里,我们就了解了同步删除和异步删除的基本代码实现。下面我们就再来看下,在刚才介绍的performEvictions函数中,它在删除键值对时,所调用的dbAsyncDelete和dbSyncDelete这两个函数,是如何使用dictDelete和dictUnlink来实际删除被淘汰数据的。

基于异步删除的数据淘汰

由dbAsyncDelete执行:

  1. 调用dictDelete

  2. 调用dictUnlink:

被淘汰的KV对只是在全局哈希表中被移除,其占用内存空间还是没有实际释放。所以dbAsyncDelete会调用lazyfreeGetFreeEffort,计算释放被淘汰KV对内存空间的开销

lazyfreeGetFreeEffort

  • 若开销较小,dbAsyncDelete直接在主I/O线程中进行同步删除
  • 否则,dbAsyncDelete创建惰性删除任务,并交给后台线程完成

虽dbAsyncDelete说是执行惰性删除,但在实际执行过程中,会使用lazyfreeGetFreeEffort评估删除开销。

lazyfreeGetFreeEffort根据要删除的KV对的类型计算删除开销:

  • 若KV对类型属于List、Hash、Set和Sorted Set这四种集合类型中的一种,且未使用紧凑型内存结构,则该KV对的删除开销就等于集合中的元素个数
  • 否则,删除开销等于1

举个例子,如下代码就展示了azyfreeGetFreeEffort计算List和Set类型键值对的删除开销:KV对是Set类型,同时它是使用哈希表结构而不是整数集合来保存数据的话,那么它的删除开销就是Set中的元素个数。

这样,当dbAsyncDelete通过lazyfreeGetFreeEffort计得被淘汰KV对的删除开销后:

  1. 把删除开销和宏定义LAZYFREE_THRESHOLD(默认64)比较。

    当被淘汰KV对为包含超过64个元素的集合类型时,dbAsyncDelete才会调用bioCreateBackgroundJob实际创建后台任务执行惰性删除。

若被淘汰KV对不是集合类型或是集合类型但包含元素个数≤64个,则dbAsyncDelete调用dictFreeUnlinkedEntry释放KV对所占的内存空间。

dbAsyncDelete使用后台任务或主IO线程释放内存空间:

基于异步删除的数据淘汰过程,实际上会根据要删除的KV对包含的元素个数,决定是使用后台线程还是主线程执行删除操作。

  • 主线程如何知道后台线程释放的内存空间,已满足待释放空间的大小?performEvictions在调用dbAsyncDelete或dbSyncDelete前后,都会统计已使用内存量,并计算调用删除函数前后的差值,这就能知晓已释放的内存空间大小。

此外,performEvictions在调用dbAsyncDelete后,会再主动检测当前内存使用量,是否已满足最大内存容量要求。一旦满足,performEvictions就会停止淘汰数据的执行流程。

使用后台线程删除被淘汰数据过程中,主线程是否仍可处理外部请求?

可以。主线程决定淘汰这个 key 后,会先把这个 key 从「全局哈希表」剔除,然后评估释放内存代价,如符合条件,则丢到「后台线程」执行「释放内存」操作。

之后就可继续处理客户端请求,尽管后台线程还未完成释放内存,但因 key 已被全局哈希表剔除,所以主线程已查询不到该 key,对客户端无影响。

同步删除的数据淘汰

dbSyncDelete实现。

  1. 首先,调用dictDelete,在过期key的哈希表中删除被淘汰的KV对
  2. 再次调用dictDelete,在全局哈希表中删除被淘汰的KV对

dictDelete调用dictGenericDelete同步释放KV对的内存空间时,最终分别调用dictFreeKey、dictFreeVal和zfree释放K、V和KV对所对应的哈希项这三者所占内存空间。

它们根据操作的哈希表类型,调用相应valDestructor和keyDestructor函数释放内存:

为方便能找到最终进行内存释放操作的函数,以全局哈希表为例,看当操作全局哈希表时,KV对的dictFreeVal和dictFreeKey两个宏定义对应的函数。

全局哈希表是在initServer中创建:

dbDictType是个dictType类型结构体:

dbDictType作为全局哈希表,保存:

  • SDS类型的key

  • 多种数据类型的value

所以,dbDictType类型哈希表的K和V释放函数,分别是dictSdsDestructor和dictObjectDestructor:

dictSdsDestructor直接调用sdsfree,释放SDS字符串占用的内存空间。dictObjectDestructor会调用decrRefCount,执行释放操作:

decrRefCount会判断待释放对象的引用计数。

  • 只有当引用计数为1,才会根据待释放对象的类型,调用具体类型的释放函数来释放内存空间
  • 否则,decrRefCount只是把待释放对象的引用计数减1

若待释放对象的引用计数为1,且String类型,则decrRefCount调用freeStringObject执行最终的内存释放操作。

若对象是List类型,则decrRefCount调用freeListObject最终释放内存:

基于同步删除的数据淘汰过程,就是通过dictDelete将被淘汰KV对从全局哈希表移除,并通过dictFreeKey、dictFreeVal和zfree释放内存空间。

释放v空间的函数是decrRefCount,根据V的引用计数和类型,最终调用不同数据类型的释放函数来完成内存空间释放。

基于异步删除的数据淘汰,它通过后台线程执行的函数是lazyfreeFreeObjectFromBioThread函数,该函数实际上也是调用了decrRefCount释放内存空间。

总结

Redis 4.0后提供惰性删除功能,所以Redis缓存淘汰数据时,就会根据是否启用惰性删除,决定是执行同步删除还是异步的惰性删除。

同步删除还是异步的惰性删除,都会先把被淘汰的KV对从哈希表中移除。然后,同步删除就会紧接着调用dictFreeKey、dictFreeVal和zfree分别释放key、value和键值对哈希项的内存空间。而异步的惰性删除,则是把空间释放任务交给了后台线程完成。

虽惰性删除是由后台线程异步完成,但后台线程启动后会监听惰性删除的任务队列,一旦有惰性删除任务,后台线程就会执行并释放内存空间。所以,从淘汰数据释放内存空间的角度来说,惰性删除并不影响缓存淘汰时的空间释放要求

后台线程需通过同步机制获取任务,这过程会引入一些额外时间开销,会导致内存释放不像同步删除那样非常及时。这也是Redis在被淘汰数据是小集合(元素不超过64个)时,仍使用主线程进行内存释放的考虑因素。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。