2024-04-06
原文作者:Brand 原文地址: https://www.cnblogs.com/wzh2010/p/17205436.html

1 介绍

通过前面的章节,我们知道,Redis 是一个kv型数据库,我们所有的数据都是存放在内存中的,但是内存是有大小限制的,不可能无限制的增量。
想要把不需要的数据清理掉,一种办法是直接删除,这个咱们前面章节有详细说过;另外一种就是设置过期时间,缓存过期后,由Redis系统自行删除。
这边需要注意的是,缓存过期之后,并不是马上删除的,那Redis是怎么删除过期数据的呢?主要通过两个方式

  • 惰性删除
  • 通过定时任务,定期选取部分数据删除

2 Redis缓存过期命令

我们通过以下指令给指定key的缓存设置过期时间,如果都没设置过期时间, key 将一直存在,直到我们使用 Del 的命令明确删除掉。

    # 缓存时间过期命令,参考如下
    EXPIRE key seconds [ NX | XX | GT | LT]

Redis 7.0 开始,EXPIRE 添加了 NX、XX和GT、LT 选项,分别代表如下:

  • NX:仅当Key没有过期时设置过期时间
  • XX:仅当Key已过期时设置过期时间
  • GT:仅当新到期时间大于当前到期时间时设置到期时间
  • LT:仅当新到期时间小于当前到期时间时设置到期时间

其中,GT、LT和NX选项是互斥的,下面是官方的测试用例:

    redis> SET mykey "Hello"
    "OK"
    redis> EXPIRE mykey 10
    (integer) 1
    redis> TTL mykey
    (integer) 10
    redis> SET mykey "Hello World"
    "OK"
    redis> TTL mykey
    (integer) -1
    redis> EXPIRE mykey 10 XX
    (integer) 0
    redis> TTL mykey
    (integer) -1
    redis> EXPIRE mykey 10 NX
    (integer) 1
    redis> TTL mykey
    (integer) 10

3 两种过期数据的删除方式

我们前面说过,Redis删除过期数据主要通过以下两个方式,我们一个个来看:

  • 惰性删除
  • 通过定时任务,定期选取部分数据删除

3.1 惰性删除

惰性删除比较简单,当客户端请求过来查询我们的key的时候,先对key做一下检查,如果没过期则返回缓存数据,如果过期,则删除缓存,重新从数据库中获取数据。
这样,我们就把删除过期数据的主动权交给了访问请求的客户端,如果客户端一直没请求,那这个过期缓存可能就长时间得不到释放。

Redis的源码 src/db.c 中的 expireIfNeeded 方法 就是实现以上惰性删除逻辑的,我们来看看:

    int expireIfNeeded(redisDb *db, robj *key, int force_delete_expired) {
        // 对于未过期的key,直接 return 0
        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) {
            if (server.current_client == server.master) return 0;
            if (!force_delete_expired) return 1;
        }
    
      /* If clients are paused, we keep the current dataset constant,
         * but return to the client what we believe is the right state. Typically,
         * at the end of the pause we will properly expire the key OR we will
         * have failed over and the new primary will send us the expire. */
        if (checkClientPauseTimeoutAndReturnIfPaused()) return 1;
    
        /* Delete the key */
        deleteExpiredKeyAndPropagate(db,key);
        return 1;
    }

3.2 定期删除

刚才前面说过了,仅靠客户端访问来对过期缓存执行删除远远不够,因为有的 key 过期了,但客户端一直没请求,那这个过期缓存可能就长时间甚至永远得不到释放。
所以除了惰性删除,Redis 还可以通过定时任务的方式来删除过期的数据。定时任务的发起的频率由redis.conf配置文件中的hz来进行配置

    # 代表每1s 运行 10次
    hz 10

Redis 默认每 1 秒运行 10 次,也就是每 100 ms 执行一次,每次随机抽取一些设置了过期时间的 key(这边注意不是检查所有设置过期时间的key,而是随机抽取部分),检查是否过期,如果发现过期了就直接删除。
该定时任务的具体流程如下:

  1. 定时serverCron方法去执行清理,执行频率根据redis.conf中的hz配置的值
  2. 执行清理的时候,不是去扫描所有的key,而是去扫描所有设置了过期时间的key(redisDb.expires)
  3. 如果每次去把所有过期的key都拿过来,那么假如过期的key很多,就会很慢,所以也不是一次性拿取所有的key
  4. 根据hash桶的维度去扫描key,扫到20(可配)个key为止。假如第一个桶是15个key ,没有满足20,继续扫描第二个桶,第二个桶20个key,由于是以hash桶的维度扫描的,所以第二个扫到了就会全扫,总共扫描35个key
  5. 找到扫描的key里面过期的key,并进行删除
  6. 删除完检查过期的 key 超过 25%,继续执行4、5步

202404062052424401.png

其他注意点:

  • 为何不扫描所有key进行过期缓存元素删除:Redis本身就是高速缓存,如果每次检查大量的key,无论在CPU和内存的的使用率上都会特别高,Redis集群越大,风险越大。
  • 分片模式下的删除同步:无论定时删除还是惰性删除。master 会生成删除的指令记录到 AOF 和 slave 节点。

4 总结

无论是惰性删除还是定期删除,都可能存在删除不尽的情况:无法删除完全,比如每次删除完过期的 key 还是超过 25%,且这些 key 再也不会被客户端访问。
如果长时间持续下去,可能会导致内存耗尽,为了避免这种糟糕情况,Redis会有一个完善的内存淘汰机制来保障。下一节我们会着重来介绍下内存淘汰机制。

阅读全文