> 文档中心 > Redis之过期键删除策略

Redis之过期键删除策略


(一)关于键的过期时间或生存时间

  我们知道,Redis数据库是基于内存的,但是如果一些不用的键在内存中一直存在,那么久而久之,就有可能会发生oom的情况。所以,redis数据库提供了常用的EXPIRE命令或者PEXPIRE命令,用户可以使用这两个命令以秒或者毫秒为精度为数据库中的某个键设置生存时间。在经过指定的时间后,redis服务器就会自动删除生存时间为0的键。
  可以设置键的生存时间的命令如下:

  1. EXPIRE    
    该命令用于将键Key的生存时间设置为ttl秒
  2. PEXPIRE   
    该命令用于将键Key的生存时间设置为ttl毫秒
  3. EXPIREAT   
    该命令用于将键Key的生存时间设置为timstamp所指定的秒数时间戳
  4. PEXPIREAT  
    该命令用于将键Key的生存时间设置为timstamp所指定的毫秒数时间戳。

   虽然有四种不同的命令用于指定过期时间,但是实际上,无论使用哪一种命令,最终都会转换为PEXPIREAT命令来执行

那么,redis是如何存储过期时间的呢?

typedef struct redisDb {    dict *dict;   /* The keyspace for this DB */    dict *expires;/* Timeout of keys with a timeout set */    dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/    dict *ready_keys;    /* Blocked keys that received a PUSH */    dict *watched_keys;  /* WATCHED keys for MULTI/EXEC CAS */    int id;/* Database ID */    long long avg_ttl;   /* Average TTL, just for stats */    unsigned long expires_cursor; /* Cursor of the active expire cycle. */    list *defrag_later;  /* List of key names to attempt to defrag one by one, gradually. */} redisDb;

  我们可以通过以上源码看出,redisDb结构的expires这个字典保存了数据库中所有的过期时间,我们叫这个字典为过期字典。
  每当我们为一个数据库的某一个键添加过期时间就会在该字典中添加一个键值对,键为这个需要添加过期时间的键,值为过期时间的时间戳。相反,如果删除一个键的过期时间,也会相应的操作这个字典,删除该键对应的过期时间键值对。如图所示:
Redis之过期键删除策略

(二)过期删除策略

  我们知道了,redis数据库如何设置,如何存储过期时间。那么这现在的问题是,如果一个键过期了,那么什么时候被删除呢?
  关于这个问题,可以实现的有一下三种方案(redis只采用了其中两种):

  1. 定时删除
    设置键的过期时间的同时,创建一个定时器,让定时器在键过期时间来临时,立即执行对键的删除操作
  2. 惰性删除
    放任过期不管,但是每次从键空间中获取值的时候,检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键
  3. 定期删除
    每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键,至于要删除多少个过期键,以及要检查多少个数据库则由算法决定。

下面我们来瞅瞅这三种策略的优缺点:

1.定时删除

优点:
  这种删除策略对于内存来说是友好的,因为这种删除方式可以保证过期的键尽可能快的被删除掉,并释放过期键所占用的内存
缺点:
  1.这种删除策略对CPU时间不友好,在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分的CPU时间,在内存不紧张的但是CPU时间紧张的情况下,这无疑会对服务器的响应时间和吞吐量造成影响。
  2.创建一个定时器需要用到redis服务器中的时间时间,而当前时间时间的实现方式为无序链表,查找一个事件的时间复杂度为O(N),所以说,如果采用这种策略,并不能高效的处理大量的时间事件。

2.惰性删除

优点
  这种删除策略对于CPU来说是友好的,程序只会在取出键的时候才会对键进行过期检查,这样可以保证对键的删除操作仅限于当前处理的键,这个策略不会在删除其他过期的键上花费任何的时间
缺点
  显而易见的,这种删除策略对于内存来说是十分不友好的。因为如果大量的过期键,长期不使用的情况下,就会造成大量的内存被无效的键占用。我们甚至可以将这中情况看作是内存泄露

3.定期删除

  针对定期删除来说,这种策略实际上是定时删除和惰性删除这两种策略的折中和整合。定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。除此之外,通过定期删除过期键,定期删除策略有效的减少了因为过期键而带来的内存浪费。
  当然,这种策略的难点就在于如何确定删除的时长和频率。比如,如果设定的删除太频繁或者执行删除的时间太长,就直接回退化为定时删除。如果删除的频率过低或者指定的时间太短,定期删除又会和惰性删除一样,造成内存浪费的情况。

(三)Redis采用的过期键删除策略

  Redis数据库实际上采用了两种删除策略:定期删除和惰性删除。通过这两种删除策略的配合使用,服务器可以很好的在合理使用CPU时间和避免内存空间浪费之间取得平衡。

那么,Redis数据库是如何实现这两种删除策略的呢?

惰性删除策略的实现:

过期键的删除策略由expireIfNeeded函数实现,所有读写数据库的Redis命令都会在执行钱调用该函数进行检查。

int expireIfNeeded(redisDb *db, robj *key) {    if (!keyIsExpired(db,key)) return 0;    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;}

我们可以看出,如果输入键已经过期,那么expireIfNeed函数将输入键从数据库删除。如果输入键没有过期,则不会做其他动作。所以,每个命令的实现函数都必须能同时处理键存在和不存在两种情况。

定期删除策略的实现

  该策略由activeExpireCycle函数实现,每当服务器周期性的操作serverCron函数执行的时候,activeExpireCycle函数就会被调用,在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随你检查一部分的过期时间,并删除其中的过期键。具体代码实现由于太多,有感兴趣可以去看一下,redis6在expire.c中,redis3在redis.c中。

(四)关于AOF、RDB对过期键的处理

1.生成RDB文件

  在执行SAVE或者BGSAVE命令创建一个新的RDB文件的时候,程序会对数据库中的过期键进行检查,过期的键不会被保存到新创建的RDB文件中

2.载入RDB文件

  载入的时候分为两种情况
  (1)服务器以主服务器运行
  当服务器以主服务器运行的时候,会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期的键则会被忽略,所以过期键对载入RDB文件的主服务器不会造成影响。
  (2)服务器以从服务器运行
  当服务器以从服务器运行的时候,会将文件中保存的所有键进行保存,不论是否过期。但是由于主从服务器进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键载入RDB文件的从高服务器也不会造成影响

3.AOF文件的写入

  当数据库中某个键已经过期,但是它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加一条DEL命令进行显示的删除

4.AOF文件的重写:

  重写的时候程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中

这里有一个有意思的东西,当服务器运行在主从复制模式下的时候,从服务器的过期键删除动作是由主服务器控制的
主服务器在删除一个过期键后,会显示的向所有从服务器发送一个DEL命令,命令从服务器删除这个键;
从服务器在执行客户端发送的命令的时候,即使遇到过期的键也不会将过期的键进行删除,是继续像处理未过期的键一样来处理过期键;
从服务器只有在接到主服务器发送来的DEL命令的时候才会删除过期键。