首页 > 其他分享 >缓存过期

缓存过期

时间:2024-03-31 11:55:05浏览次数:21  
标签:缓存 KEY 删除 过期 扫描 db key

缓存过期

Redis 系列目录:https://www.cnblogs.com/binarylei/p/11721921.html

缓存过期

Redis 缓存过期有 2 种策略,被动过期和主动过期。

  1. 被动过期删除:客户端访问 Redis 时,发现 key 已经过期,需要删除数据
  2. 主动过期删除:Redis 主线程有一个定时任务,默认没 100ms 执行一次,每次主动淘汰一批已经过期的数据。

接下来,我会依次分析被动过期和主动过期在 Redis 中是如何实现的。

数据结构

我们先看一下 KEY 的过期时间在 Reids 中是如何存储的。首先,为了方便查找,Redis 会将所有设置了过期时间的 KEY 单独存放。其次,KEY 的过期时间保存在 hash 节点中。

src/server.h/redisDb 保存所有的数据 dict,及设置过期时间的 KEY 集合 expires

dict *dict;     /* 数据key->value */
dict *expires;  /* 设置了过期时间的key */

src/dict.h/dictEntry 保存 KEY 的过期时间 s64

int64_t s64;	/* 过期时间 */

同时,src/db.c 定义了一系列的方法操作 KEY 的过期时间:

  • setExpire:设置 KEY 的过期时间。
  • getExpire:获取 KEY 的过期时间。
  • keyIsExpired:判断 KEY 是否过期。

有了这些基础,我们再来看过期删除是如何实现的。

被动删除

被动删除,客户端访问时才会淘汰过期数据,相比主动淘汰,更加节省 CPU。Redis 增删改查等数据操作,都会先调用 expireIfNeeded 方法判断数据是否过期,如果过期了,就会先把过期的数据淘汰,再执行客户端命令。下面是被动过期的执行流程。

图1 被动过期删除流程

可以看到,Reids 的 GET、SET、HSET、KEYS 等操作,都会先调用 expireIfNeeded 方法。我们看一下这个方法。src/db.c

int expireIfNeeded(redisDb *db, robj *key) {
    /* 判断key是否过期 */
    if (!keyIsExpired(db,key)) return 0;

    /* slave节点的数据是从master同步过来的,不主动淘汰key */
    if (server.masterhost != NULL) return 1;

    /* 删除过期的key */
    if (server.lazyfree_lazy_expire) {
        dbAsyncDelete(db,key);
    } else {
        dbSyncDelete(db,key);
    }
    return 1;
}

expireIfNeeded 方法也很简单,先判断 KEY 是否过期,如果过期了就删除该数据。需要注意的是,slave 节点是不能主动删除数据的,它的数据必须都从 master 同步过来,等 master 淘汰了该过期数据就会同步到从节点,此时 slave 就会删除该数据。否则,就有可能出现主从数据不一致的现象。

主动删除

和被动过期删除需要等客户端访问不同;主动删除策略,在 Reids 比较闲的时候,会主动扫描一定数据的 KEY,淘汰过期的数据。下图是主动删除的执行流程:

图2 主动过期删除流程

前面我们说了,Redis 将所有设置了过期时间的 KEY,都单独存放在 expires 字典表中。所以,主动删除其实就是遍历这个 expires 字典表,发现数据过期了就删除。当然,Reids 为了考虑性能,每次执行时都有扫描个数和时间的限制,超过了就主动退出,下一次再继续。我们看一下可以的代码 src/expire.c/activeExpireCycle 方法。

void activeExpireCycle(int type) {
    static unsigned int current_db = 0;
    int j, iteration = 0;
    int dbs_per_call = CRON_DBS_PER_CALL;
    long long start = ustime(), timelimit, elapsed;  // ustime 时间单位为微秒

    /* 计算最大执行时间,默认25ms。 */
    timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;

	/* 1.遍历redis db,current_db指向下一个要扫描的数据库 */
    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        unsigned long expired, sampled;

        redisDb *db = server.db+(current_db % server.dbnum);
        current_db++;

		/* 开始扫描该数据库下的key */
        do {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;
            iteration++;

            /* 2.1没有设置过期时间的KEY,直接返回 */
            if ((num = dictSize(db->expires)) == 0) {
                break;
            }
            slots = dictSlots(db->expires);
            now = mstime();

            /* 2.2计算设置过期KEY的占比,如果小于10%,设置过期的KEY不多扫描一次不划算,等待扩容 */
            if (slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;
            
            expired = 0;	// 过期KEY个数
            sampled = 0;	// 扫描KEY个数

            if (num > config_keys_per_loop)
                num = config_keys_per_loop;		// 最大扫描的KEY个数

			/* 3.这里开始真是扫描key进行过期淘汰,每次默认最多扫描20个key,400个桶。
			 * 3.1根据expires_cursor找到当前要扫描的hash桶位置
			 * 3.2依次扫描该hash桶的所有key,判断是否过期
			 * 3.3统计扫描到的key个数sampled和过期的个数expired
			 * 3.4扫描下一个桶expires_cursor++,直至扫描的key个数超过默认的20个
			 */
            long max_buckets = num*20;
            long checked_buckets = 0;
            while (sampled < num && checked_buckets < max_buckets) {
                for (int table = 0; table < 2; table++) {
                    if (table == 1 && !dictIsRehashing(db->expires)) break;

                    unsigned long idx = db->expires_cursor;
                    idx &= db->expires->ht[table].sizemask;
                    dictEntry *de = db->expires->ht[table].table[idx];
                    long long ttl;

                    checked_buckets++;
                    while(de) {
                        dictEntry *e = de;
                        de = de->next;

                        ttl = dictGetSignedIntegerVal(e)-now;
						/* 4.activeExpireCycleTryExpire进行缓存淘汰 */
                        if (activeExpireCycleTryExpire(db,e,now)) expired++;
                        sampled++;
                    }
                }
                db->expires_cursor++;
            }

            /* 5.计算执行时间,判断是否超时。每迭代16次统计一次执行时间 */
            if ((iteration & 0xf) == 0) {
                elapsed = ustime()-start;
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    break;
                }
            }
        /* 6.如果缓存淘汰率大于10%继续,说明过期数据很多,可以牺牲一下可用性,多淘汰一点过期数据 */
        } while (sampled == 0 ||  (expired*100/sampled) > config_cycle_acceptable_stale);
    }
}

这些步骤里面,我希望你重点关注一下 Redis 是如何在性能和可用性上做抉择的。如果想将过期数据都淘汰,必然这段时间服务是不可用的,牺牲了可用性。所以,Redis 设置了每次扫描的 KEY 个数和扫描时长,超出了就主动退出。同时,如果设置过期时间的 KEY 不多(小于 1%),那淘汰的数据也不会多,扫描一次必然性价比不高,此时直接等待字典表扩容即可。相反,如果发现过期的数据较多(淘汰率大于10%),则继续扫描直至超时。

接下来,我们看一下主动删除是什么时候执行的。Redis 有一个主线程的定时任务,默认 1s 执行 10 次。每次执行时都会扫描过期数据、hash resize缩容、rehash数据迁移等。感兴趣的可以看一下代码,src/server.c 下的 serverCron 和 databasesCron 方法。

图3 主动过期删除定时任务

总结

今天,我重点分析了 Reids 缓存过期删除的两种策略,我们复习一下。

1.被动过期删除:数据增删改查时都会调用 expireIfNeeded 方法,判断缓存是否过期。

2.主动过期删除:默认每 100ms 扫描一次设置了过期时间的数据,activeExpireCycle 方法为了避免阻塞应用,每次只会扫描部分数据。

讲到这里,Reids 缓存过期删除的源码我就介绍完了。我建议你特别关注下 Redis 在性能和可用性的平衡,架构设计往往鱼和熊掌不可兼得。最后,我用一张思维导图来帮助你理解和记忆这两种过期删除策略:

文章推荐:


每天用心记录一点点。内容也许不重要,但习惯很重要!

标签:缓存,KEY,删除,过期,扫描,db,key
From: https://www.cnblogs.com/binarylei/p/18106548

相关文章

  • C#网站系统如何监控登录过期
    网站系统监控登录过期通常涉及多个层面的技术和策略。以下是一些建议的方法来实现这一功能:会话管理:会话超时设置:为每个用户会话设置一个超时时间。一旦用户在这个时间段内没有与系统进行任何交互,会话将被视为过期,用户需要重新登录。会话令牌(Token):使用令牌来管理用户的登录状......
  • 【Redis】Redis 生产问题。如何确保缓存和数据库数据的一致性? 常见的缓存更新策略?
    目录缓存穿透缓存穿透解决办法缓存击穿击穿解决办法?缓存穿透和缓存击穿的区别?缓存雪崩雪崩解决办法?如何确保缓存和数据库数据的一致性?常见的缓存更新策略?缓存穿透定义:缓存穿透说简单点就是大量请求的key是不合理的,根本不存在于缓存中,也不存在于数据库中。这......
  • 消息队列,缓存,分库分表是高并发解决方案
    消息队列的七种经典应用场景 在笔者心中,消息队列,缓存,分库分表是高并发解决方案三剑客。在职业生涯中,笔者曾经使用过ActiveMQ、RabbitMQ、Kafka、RocketMQ这些知名的消息队列。这篇文章,笔者结合自己的真实经历,和大家分享消息队列的七种经典应用场景。1异步&解耦笔......
  • Java 实现缓存的三种方式
    Java实现缓存的三种方式文章目录Java实现缓存的三种方式一、`HashMap`实现缓存`Step-1`:实现一个缓存管理类`Step-2`:将缓存管理类交给`Spring`进行管理`Step-3`:编写接口测试缓存`Step-4`:结果展示二、`guavalocalcache`实现`Step-1`:导入`guava`依赖`Step-2`:使用`......
  • Linux架构27 Ansible变量, 定义变量的方式, 变量注册, facts缓存
    Ansible变量一、变量概述变量提供了便捷的方式来管理Ansibleplaybook的每一个项目中的动态值,比如nginx-1.6.3这个软件包的版本,在其他地方或许会反复使用,那么如果讲此值设置为变量,然后再在其他的playbook中调用,会方便许多。如此一来还方便维护,减少维护的成本。二、定义变量的......
  • android小球(二)——用户数据缓存详解SharedPreferences
    SharedPreferences概述SharedPreferences是Android平台上一个轻量级的存储辅助类,用来保存应用的一些常用配置,它提供了String,set,int,long,float,boolean六种数据类型。使用SharedPreferences进行存储的数据是存放在一个XML文件中的,同时它的存储方式是是以key-value的形式,key对应......
  • 美团一面:项目中使用过Redis吗?我说用Redis做缓存。他对我哦了一声
    引言Redis,作为一种开源的、基于内存且支持持久化的键值存储系统,以其卓越的性能、丰富灵活的数据结构和高度可扩展性在全球范围内广受欢迎。Redis不仅提供了一种简单直观的方式来存储和检索数据,更因其支持数据结构如字符串、哈希、列表、集合、有序集合等多种类型,使得其在众多场景......
  • (1),深入分布式缓存从原理到实践技术分享
    carry=sum/10;if(l1){l1=l1->next;}if(l2){l2=l2->next;}}if(carry>0){tail->next=newListNode(carry);}returnhead;}ListNode*addTwoNumbers(ListNode*l1,ListNode*l2){ListNode*result=newListNode(-1);ListNode*......
  • redis自学(25)过期策略
    Redis内存回收Redsi之所以性能强,最主要的原因就是基于内存存储。然而但决断的redis其内存大小不宜过大,会影响持久化或者主从同步性。我们可以通过修改配置文件来设置redis的最大内存:  当内存使用达到上限时,就无法存储更多数据了过期策略在学习redis缓存的时候我们说过,可......
  • Jenkins清理缓存
    1.在参数部分添加bool参数parameters{booleanParam(name:'CleanWorkspace',defaultValue:false,description:'清理工作空间,默认不清理!')}2.在脚本用引用这个参数stage("Initializationt"){step......