前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >redis学习笔记--redis过期机制学习

redis学习笔记--redis过期机制学习

原创
作者头像
吃完橙子了哈
发布2020-09-05 22:49:23
1.6K0
发布2020-09-05 22:49:23
举报

redisDb结构介绍

首先看下redisDb的结构,在server.h文件中:

代码语言:txt
复制
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;
  • dict:保存db中所有的键值对
  • expires:保存所有有过期时间的键值对
  • expires_cursor:周期性删除过期键的游标,应该是6.0版本以后才有的,因为书里和网上的一些介绍都没有这个字段

redis与过期相关的命令

redis中可以使用以下命令设置key的过期时间

  • EXPIRE <key> <ttl>: 将key的生存时间设置为ttl
  • PEXPIRE <key> <ttl>: 将key的生存时间设置为ttl毫秒
  • EXPIREAT <key> <timestamp>: 设置key于时间戳timestamp时过期,秒数时间戳
  • PEXPIREAT <key> <timestamp>: 设置key于时间戳timestamp时过期,毫秒数时间戳

命令很好理解,P开头的都是以毫秒为单位,相关代码实现也很简单。以上四条命令的实现在expire.c文件中:

代码语言:txt
复制
/* EXPIRE key seconds */                                                       
  void expireCommand(client *c) {                                                
      expireGenericCommand(c,mstime(),UNIT_SECONDS);                             
  }                                                                                                             
                                                                                 
  /* EXPIREAT key time */                                                        
  void expireatCommand(client *c) {                                              
      expireGenericCommand(c,0,UNIT_SECONDS);                                    
  }                                                                              
                                                                                 
  /* PEXPIRE key milliseconds */                                                 
  void pexpireCommand(client *c) {                                               
      expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);                        
  }                                                                              
                                                                                 
  /* PEXPIREAT key ms_time */                                                    
  void pexpireatCommand(client *c) {                                             
      expireGenericCommand(c,0,UNIT_MILLISECONDS);                               
  }

可以看出以上命令都是调用同一个函数实现的,也就是说这四个设置命令之间是可以互相转换的。mstime()是获取当前秒级时间戳的函数,也就是说,EXPIRE、PEXPIRE,EXPIREAT命令,最终其实都是执行PEXPIREAT命令。

  • TTL <key>:返回键的剩余生存时间,单位秒
  • PTTL <key>:返回键的剩余生存时间,单位毫秒

这两个命令的实现也在expire.c中,ttlCommand和pttlCommand,实现很简单,这里伪代码描述:

代码语言:txt
复制
if(key不存在):
    return -2
if(key没设置过期时间):
    return -1

ttl = 计算过期时间()
return 过期时间

过期键删除策略

键过期了如何删除?一般有以下三种方法:

  1. 定时删除:插入过期键的同时,开一个定时任务,在键的过期来临时执行删除任务
  2. 惰性删除:用户查询的时候判断是否过期,过期则删除,用户不差则永远不删除
  3. 定期删除:每隔一段时间进行一次删除任务,遍历多少个db,删多少个键由算法决定

第一种方式优点是删除快,对内存友好,但是频繁的执行对cpu不友好;

第二中方式对cpu友好,对内存不友好

第三种方式是一、二的折中,当然如果执行的太频繁,或者每次删除的过多,就跟方式一没什么区别。

redis的过期键删除策略

redis使用了惰性删除和定期删除两种方式。

惰性实现在db.c/expireIfNeeded()中。逻辑很简单:

  • key没过期,return 0
  • key过期&&当前的库是从库,return 1
  • key过期&&当前的库是主库,删除过期键并return1

这里需要注意的就是redis只会从主库删除过期键,主库再同步从库。

redis的定期删除策略

redis会定期执行过期键删除,定期删除函数在expire.c/activeExpireCycle(int),这个函数有两个地方调用,一个是server.c/databasesCron()函数,一个是server.c/beforeSleep()函数,databasesCron函数负责处理redis的一些后台操作,例如过期键的处理,调整大小,重散列等操作。

activeExpireCycle()有两种模式,ACTIVE_EXPIRE_CYCLE_FASTACTIVE_EXPIRE_CYCLE_SLOW,代表不同的执行周期,前者为“快周期”,后者“慢周期”

为了继续探究redis的删除策略,需要看activeExpireCycle(int)这个函数的实现。

首先先看下代码中对activeExpireCycle函数的注释, 翻译出来大概是以下几个点:

  1. 这个算法是一个自适应的算法,如果只有少量的key过期,那么只会使用少量的cpu资源去清理这些过期key,但如果有过多的过期key,那么就会采用一些策略,不会占用过多的cpu资源去清理这些过期键;
  2. 在每个清理的周期中,redis不会一口气清理所有的db的过期键,而是分片执行,第二个执行周期会接着第一个执行周期结束时所在的database继续执行,每个执行周期所遍历的数据库数量不会超过常量CRON_DBS_PER_CALL,这一版该常量是16;
  3. 该函数每次执行的工作量取决于入参是什么type,目前两种模式:快速过期模式和慢速过期模式。我们通常使用慢周期去清理过期键,频率通常是10赫兹,这个频率的变量定义是server.hz,由redis.conf中的hz变量控制,设置区间为1~500,hz与cpu的消耗成正比,通常是10,官方也不建议我们设置过高
  4. “慢周期”方式执行是很耗时间的,经常会因为超时而退出,所以在beforesleep()函数中会调用“快周期”模式去执行,“快周期”不会经常被调用;
  5. 如果是ACTIVE_EXPIRE_CYCLE_FAST类型,redis会以比较快的过期周期进行清理。如果是ACTIVE_EXPIRE_CYCLE_SLOW类型,redis会以正常的过期周期进行清理,时间周期是REDIS_HZ的一个百分比,由ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC定义。
  6. 无论是哪种模式,都会有一个清理的baseline,例如每次遍历多少db,每个db遍历多少个键?过期的键多少时开始执行过期键删除工作?

翻译完我们清楚了这个函数大致的执行方式,接下来就看一些细节。

  • 具体的删除策略是怎样的?过期键多少时才会触发清理策略?

首先看下代码中先定义的一些默认常量:

代码语言:txt
复制
  #define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* 默认每个数据库检查的键数量. */
  #define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* “快周期”的周期时间,单位微秒. */                                            
  #define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* "慢周期“做多可占用的cpu资源,% */
  #define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /*可容忍的过期键所占内存百分比 */

从这些默认值可以看出,默认过期键数量不能超过内存的10%,并避免消耗超过25%的cpu资源。

这些只是一些系统默认的常量,redis给了我们一个额外的参数effort,让我们去修改上面这些默认变量。effort变量由redis.conf中的active-expire-effort控制,1到10,设置越大cpu的消耗也会相应增加。

可以通过下面这段代码更清楚的看出effort这个变量的作用

代码语言:txt
复制
      effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
      config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
                             ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
      config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
                                   ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
      config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
                                    2*effort,
      config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-                                     
                                      effort;

effort改变了过期键可占内存的最大百分比,改变了最大占用cpu的百分比等,effort越大,cpu负担越重,所以根据自己的需要设置effort的值。

  • 如何控制一个周期内清除多少过期键?下一个周期如何接着上一个周期继续清理?

我们可以看到代码中设置了几个静态变量,用于记录每个周期结束时的一些东西。

代码语言:txt
复制
      static unsigned int current_db = 0; /* 上个周期结束时执行到的db */
      static int timelimit_exit = 0;      /* 上一个周期是否因为超时而退出? */                                
      static long long last_fast_cycle = 0; /* 上一个”快周期“的运行时间 */

这里为了看得清楚,用伪代码表示:

代码语言:txt
复制
//如果上次不是因为超时而结束,并且当前过期键数量小于可容忍的过期键数量,不处理
if(type == ACTIVE_EXPIRE_CYCLE_FAST):
    if(timelimit_exit && server.stat_expired_stale_perc < config_cycle_acceptable_stale) :
        return;
    
    //如果距离上次fast模式的运行时间小于两倍的fast模式的周期,不处理
    if(start < last_fast_cycle + (long long)config_cycle_fast_duration*2) :
        return;
    
    //以上检查全过,那么就可以开始fast模式,记录一下本次的运行时间
    last_fast_cycle = start


//如果服务器的db数量比默认的db检查数量小,以服务器的数量为准
//如果上次是超时退出,也会在检查一遍过期键数量
if(dbs_per_call > server.dbnum || timelimit_exit) :
    dbs_per_call = server.dbnum;

//开始处理每个db中的过期键
for i in range(dbs_per_call):
    //获取当前的db,并将currentdb的索引加一
    redisDb = server.db[current_db]
    current_db += 1
    
    //如果当先过期键的数量大于可容忍的过期键数量,那么开始执行清理
    while((expired*100/sampled) > config_cycle_acceptable_stale):
        //该db没有过期键,不处理
        if((num = dictSize(db->expires)) == 0):
            break
            
        max_buckets = num*20; //最大的hash桶数量
        checked_buckets = 0; //当前检查的hash桶
        
        //采样数量小于每个db的默认检查键数量,当前检查的桶小于最大桶
        while(sampled < num && checked_buckets < max_buckets):
            //从当前db的过期游标开始检查过期键
            idx = db->expires_cursor
            idx &= db->expires->ht[table].sizemask
            dictEntry *de = db->expires->ht[table].table[idx]
            
            while(de):
                dictEntry *e = de
                de = de->next
                ttl = dictGetSignedIntegerVal(e)-now;
                //如果key过期,删除并计数+1
                if (activeExpireCycleTryExpire(db,e,now)) expired+=1;
                //记录采样数据
                if (ttl > 0):
                    ttl_sum += ttl;//为了统计平均ttl用的
                    ttl_samples+=1;
                samples += 1
            db->expires_cursor++;
        total_expired += expired
        total_sampled += sampled
        
        //超时则退出,并标记
        if(ustime()-start > timelimit):
            timelimit_exit = 1;
            server.stat_expired_time_cap_reached_count++;
            break

我们可以总结以下redis定期删除的工作模式:

  1. 分为快、慢两周周期模式,通常使用慢周期模式进行键的删除;
  2. 每次清理周期需要控制db的数量
  3. 控制每个db中检查键的数量,并删除其中的过期键
  4. 会有一些全局变量记录当前周期的检查进度,方便下个周期继续执行;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • redisDb结构介绍
  • redis与过期相关的命令
  • 过期键删除策略
    • redis的过期键删除策略
      • redis的定期删除策略
      相关产品与服务
      云数据库 Redis
      腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档