> 文章列表 > 聊聊Redis 的过期键删除策略

聊聊Redis 的过期键删除策略

聊聊Redis 的过期键删除策略

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

惰性删除的设置

首先,当 Redis server 希望启动惰性删除时,需要在 redis.conf 文件中设置和惰性删除相关的配置项。其中包括了四个配置项,分别对应了如下的四种场景。

  • lazyfree-lazy-eviction:对应缓存淘汰时的数据删除场景。
  • lazyfree-lazy-expire:对应过期 key 的删除场景。
  • lazyfree-lazy-server-del:对应会隐式进行删除操作的 server 命令执行场景。
  • replica-lazy-flush:对应从节点完成全量同步后,删除原有旧数据的场景。

这四个配置项的默认值都是 no。所以,如果要在缓存淘汰时启用,就需要将

lazyfree-lazy-eviction 设置为 yes。同时,Redis server 在启动过程中进行配置参数初始化时,会根据 redis.conf 的配置信息,设置全局变量 server 的 lazyfree_lazy_eviction 成员变量。

这样一来,我们在 Redis 源码中,如果看到对 server.lazyfree_lazy_eviction 变量值进行条件判断,那其实就是 Redis 根据 lazyfree-lazy-eviction 配置项,来决定是否执行惰性删除。

数据删除操作

在学习数据异步或同步删除之前,你首先需要知道,删除操作实际上是包括了两步子操作。

  • 子操作一:将被淘汰的键值对从哈希表中去除,这里的哈希表既可能是设置了过期 key 的哈希表,也可能是全局哈希表。
  • 子操作二:释放被淘汰键值对所占用的内存空间。

也就是说,如果这两个子操作一起做,那么就是同步删除;如果只做了子操作一,而子操作二由后台线程来执行,那么就是异步删除

那么对于 Redis 源码来说,它是使用了 dictGenericDelete 函数,来实现前面介绍的这两个子操作。dictGenericDelete 函数是在 dict.c 文件中实现的,下面我们就来了解下它的具体执行过程。

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

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

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

然后,dictGenericDelete 函数会根据传入参数 nofree 的值,决定是否实际释放 key 和 value 的内存空间。dictGenericDelete 函数中的这部分执行逻辑如下所示:

h = dictHashKey(d, key); //计算key的哈希值
for (table = 0; table <= 1; table++) {idx = h & d->ht[table].sizemask;  //根据key的哈希值获取它所在的哈希桶编号he = d->ht[table].table[idx];   //获取key所在哈希桶的第一个哈希项prevHe = NULL;while(he) {   //在哈希桶中逐一查找被删除的key是否存在if (key==he->key || dictCompareKeys(d, key, he->key)) {//如果找见被删除key了,那么将它从哈希桶的链表中去除if (prevHe)prevHe->next = he->next;elsed->ht[table].table[idx] = he->next;if (!nofree) {  //如果要同步删除,那么就释放key和value的内存空间dictFreeKey(d, he); //调用dictFreeKey释放dictFreeVal(d, he);zfree(he);}d->ht[table].used--;return he;}prevHe = he;he = he->next;   //当前key不是要查找的key,再找下一个}...
}

那么,从 dictGenericDelete 函数的实现中,你可以发现,dictGenericDelete 函数实际上会根据 nofree 参数,来决定执行的是同步删除还是异步删除。而 Redis 源码在 dictGenericDelete 函数的基础上,还封装了两个函数 dictDelete 和 dictUnlink

这两个函数的区别就在于,它们给 dictGenericDelete 函数传递的 nofree 参数值是 0,还是 1。如果其中 nofree 的值为 0,表示的就是同步删除,而 nofree 值为 1,表示的则是异步删除。

下面的代码展示了 dictGenericDelete 函数原型,以及 dictDelete 和 dictUnlink 两个函数的实现,你可以看下。

//dictGenericDelete函数原型,参数是待查找的哈希表,待查找的key,以及同步/异步删除标记
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) //同步删除函数,传给dictGenericDelete函数的nofree值为0
int dictDelete(dict *ht, const void *key) {return dictGenericDelete(ht,key,0) ? DICT_OK : DICT_ERR;
}//异步删除函数,传给dictGenericDelete函数的nofree值为1
dictEntry *dictUnlink(dict *ht, const void *key) {return dictGenericDelete(ht,key,1);
}

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

总结

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

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