菜鸟笔记
提升您的技术认知

Redis过期策略详解

为什么要有过期策略?

因为我们的redis是一个内存型数据库,我们的数据都是放在内存里面的!但是内存是有大小的!
比如,redis有个很重要的配置文件,redis.conf,里面有个配置

# maxmemory  //redis占用的最大内存

如果我们不淘汰,那么它的数据就会满,满了肯定就不能再放数据,发挥不了redis的作用!
比如冰箱,你如果放满了,那么你的菜就不能放冰箱了!
过期策略:拿出redis中已经过期了的数据,就像你从冰箱把坏的菜拿出来!!但是有一种情况,就是冰箱里面的菜都没坏,redis里面的数据都没过期,它也是会放满的,那怎么办?

那么当redis里面的数据都没过期。但是内存满了的时候,我们就得从未过期的数据里面去拿出一些扔掉,那么这个就是我们的淘汰策略,详见另一篇文章:Redis的淘汰策略详解

Redis自带的有两种过期策略,我们也可以自己实现一些过期的策略,不过今天主要研究自带的

惰性过期(被动过期)

这个怎么实现的呢?所谓惰性,是不是就很懒的意思,就是只有访问我的时候,我才会去判断过不过期,不然我懒得去判断,我不会主动去判断过没过期
访问一个key时判断该 key 是否已过期,过期则清除
该策略就可以最大化地节省CPU资源,因为它平时都懒得去判断,所以也没有啥cpu损耗,因为只有访问的时候我才去判断一下!
但是却对内存非常不友好。因为你不实时过期了,该过期删除的就可能一直堆积在内存里面!极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
源码(expireIfNeeded db.c文件下1302行):

int expireIfNeeded(redisDb *db, robj *key) {
  
    if (!keyIsExpired(db,key)) return 0;
    /* If we are running in the context of a slave,instead of
     * evicting the expired key from the database, we return ASAP:
     * the slave key expiration is controlled by the master that will
     * send us synthesized DEL operations for expired keys.
     *
     * Still we try to return the right information to the caller,
     * that is, 0 if we think the key should be still valid, 1 if
     * we think the key is expired at this time. */
     if (server.masterhost != NULL) return 1;
     /* Delete the key */
     server.stat_expiredkeys++;
     propagateExpire(db,key,server.lazyfree_lazy_expire);
     notifyKeyspaceEvent(NOTIFY_EXPIRED,"expired",key,db->id);
     int retval = server.lazyfree_lazy_expire ?
     dbAsyncDelete(db,key):
     dbSyncDelete(db,key);
     if (retval) signalModifiedKey(NULL,db,key);
     return retval;
}

我们刚才说了主动过期因为太耗CPU它不用
但是惰性这种又会可能导致大量无效数据堆积在内存里面,我们总得有个办法来解决吧!不能让他一直堆在内存里面啊!
所以我们就有了一个定期过期策略,虽然实时性比不上定时的,但是也足够解决垃圾数据大量堆积在内存的这种情况!

定期过期

所谓定期过期,就是每过一段时间去执行一次删除过期key。
这里需要先学习下redis的一个数据结构:字典 必须学哦
Redis数据结构——dict(字典)
大概的结构如图:

redis的hash默认使用的是ht[0],ht[1]不会初始化和分配空间。
哈希表dictht是用链地址法来解决碰撞问题的。如果节点数量比哈希表的大小要大很多的话,那么哈希表就会退化成多个链表,哈希表本身的性能优势就不再存在,在这种情况下需要扩容。
Redis里面的这种操作叫做rehash。 那么它怎么做rehash的,也是看上面字典这篇文章

我们来看定期过期到底是怎么实现的:
先想一下,如果让我们实现一个定期删除,应该怎么做?
我想到的是定期去循环找过期的key,然后去删掉!巧的是Redis也是这样做的
那么问题又来了:

  1. 我们去循环谁?是不是所有的key
  2. 我们多久循环一次?

第一个问题,我们并不是去循环所有的key,因为Redis里经常会存放巨多的数据,对我们需要经常清理,全部遍历一遍显然不现实,而Redis采取的是取样这个操作
具体实现方式为:

  1. 不是一次性把所有设置了过期时间的数据拿出来,而是按hash桶维度取 里面取值,取到20个值为止,如果第一个有30个,那么也会取30个! 如果一直取不到20,那么最多400个桶
  2. 删除取出值的过期key
  3. 如果400个桶都取不到值,或者取出的key 删除的比例大于10%,继续上 面的操作
  4. 每循环16次会去检测时间,超过指定时间就跳出

ps:按hash桶维度取key的逻辑是:最后一个桶会取完桶内所有的key,不论里面有多少个,每取完一个桶判断一下是否取到了20个,最多取400个桶

现在我们第一个问题解决了!那么第二个问题,定期定期,那么多久去做上面那件时间!那么redis里面有个很重要的概念叫做时间事件,那么这个时间事件是什么意思了,就是定时去做一些事情,那么redis里面有个方法叫serverCron(),在文件server.c中;就是它的时间事件去调用的清理
它里面干了很多事情,比如:

  1. 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等
  2. 清理数据库中的过期键值对。
  3. 关闭和清理连接失效的客户端
  4. 尝试进行持久化操作

那么这个时间事件多久去执行一次呢,其实是由你们自己决定的!
redis.conf 中通过 hz 配置,hz代表的意思是每秒执行多少次!默认10次,也就是100ms我们就会去执行定期过期!!

定期过期的逻辑,简单画图