来聊聊Redis定期删除策略的设计与实现

news2024/11/18 11:21:27

写在文章开头

我们都知道redis通过主线程完成内存数据库的指令操作,由于只有一个线程负责核心业务流程,所以对于每一个操作都要求尽可能达到尽可能的高效迅速,而本文就基于源码来聊聊redis的定期删除策略的设计与实现。

在这里插入图片描述

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

详解redis过期删除策略

redis对于过期删除策略的设计思路

为了设置键值对的时效性,我们一般会键入如下指令设置键值对的时效,以笔者下面这条指令为例,其生存时间即TTL为5s,一旦这个key达到5s不再可以被获取到了。

127.0.0.1:6379> set key value
127.0.0.1:6379> EXPIRE key 5

对于这种时效性的设计,我们一般有3种常见的方案:

  1. 定时删除:设置一个定时任务,定期删除当前设置时效的key
  2. 惰性删除:即使这个key到期了我们也不处理,等到用户通过get指令获取的时候判断即生存时间是否到期,如果到期则删除并返回null,反之返回查询结果。
  3. 定期删除:每隔一段时间,对数据库进行一次全量检查,批量删除到期的key

从实现上可以看出,定时删除这种做法本质就是提交定时任务让CPU到期执行,这种做法会因为每个key在每次轮询是否到期处理的逻辑而占用大量的CPU资源。

在这里插入图片描述

与之相反的是惰性删除,这种做法对于CPU比较友好,因为它不会定时去处理到期的key,与之相反只有用户获取这个key才会进行判断,这样的设计就对于内存不友好了,假设我们有大量的key到期而没有及时处理,那么宝贵的内存资源就会被这些无效的key占用而导致一系列连锁问题,例如查询性能降低、内存无效占用过高等。

在这里插入图片描述

最后就是定期删除了,从redis的工作机制来看,这种做法会使得时间事件定期的轮询全表检查并删除过期的key,很显然这种做法非常耗时,在键值对非常多的情况下,很可能导致redis大量时间都用于处理这些过期key上。

在这里插入图片描述

考虑到redis核心处理只有一个线程,redis在时间和空间上做了一定的折中,它本质上是基于优化版的定期删除和惰性删除策略已取得性能的空间的最佳平衡。

redis会在启动时创建一个时间处理循环事件,该事件会定期执行各种时间任务,包括redis的定期删除任务。对于定期删除,redis会随机从数据库中拿到一部分设置时效的键值对是否到期,如果过期则将其删除。而该策略会出现某些键值未能及时删除,所以redis又补充了惰性删除策略,即用户对某哦个设置时效的键值对进行操作时,redis就会判断当前键是否过期,如果过期则会将其删除。

在这里插入图片描述

过期键的创建和维护

基于上述流程我们大体知道了redis对于过期键的删除策略,接下来我们就以源码的方式去印证这几点,可以看到redisServer 通过db指针维护数据库,而redisDb 其内部通过dict字典维护键值对,再通过一个expires字典维护每个带有时效的键值对的信息:


struct redisServer {
	//......
	//指向数据库
    redisDb *db;
}    

typedef struct redisDb {
	//记录键值对信息
    dict *dict;                 /* The keyspace for this DB */
    //记录带时效的键值对的生存周期TTL
    dict *expires;              /* Timeout of keys with a timeout set */
   	//......
} redisDb;

了解整体流程之后,我们直接从源码开始印证,我们在执行时会执行以下几个命令,经过解析后,会走到命令表中的expireCommand,可以看到其内部本质就是传入客户端及其参数、当前时间到期时间单位(秒)到expireGenericCommand方法:

void expireCommand(redisClient *c) {
    expireGenericCommand(c,mstime(),UNIT_SECONDS);
}

expireGenericCommand回基于入参的basetime计算出到期时间when,并调用setExpire完成键值对保存至到期时间表:

void expireGenericCommand(redisClient *c, long long basetime, int unit) {
	//基于参数计算出到期时间when
	 robj *key = c->argv[1], *param = c->argv[2];
    long long when; /* unix time in milliseconds when the key will expire. */

    //......

    if (unit == UNIT_SECONDS) when *= 1000;
    when += basetime;


  //......
  //记录当前键值对到期时间到expires字典中
    if (when <= mstime() && !server.loading && !server.masterhost) {
       //......
    } else {
        setExpire(c->db,key,when);
        addReply(c,shared.cone);
        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);
        server.dirty++;
        return;
    }
}

我们步入setExpire方法内部就是将键值对到期时间到存到expires字典中:

void setExpire(redisDb *db, robj *key, long long when) {
    dictEntry *kde, *de;

    /* Reuse the sds from the main dict in the expire dict */
    kde = dictFind(db->dict,key->ptr);
   //......
    de = dictReplaceRaw(db->expires,dictGetKey(kde));
//......
}

惰性删除

有了expires表对于带有时效键值对的维护,惰性删除实现就非常简单高效,当用户调用get指令时,其内部会调用lookupKeyReadOrReply检查key是否到期,若当前时间大于到期时间则将键值对删除并返回null

int getGenericCommand(redisClient *c) {
    robj *o;
	//查看key是否到期,若到期则将其删除,反之直接返回查询结果
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
        return REDIS_OK;

    if (o->type != REDIS_STRING) {
        addReply(c,shared.wrongtypeerr);
        return REDIS_ERR;
    } else {
        addReplyBulk(c,o);
        return REDIS_OK;
    }
}

其内部调用就会走到lookupKeyRead中的expireIfNeeded方法其内部就会找到管理过期键的字典expires看看当前事件是否大于到期时间,若大于则说明当前时间已过期则将其删除并返回null

robj *lookupKeyRead(redisDb *db, robj *key) {
    robj *val;
	//判断当前key是否到期,若到期则删除
    expireIfNeeded(db,key);
    //查询并返回key对应的结果val
    val = lookupKey(db,key);
    if (val == NULL)
        server.stat_keyspace_misses++;
    else
        server.stat_keyspace_hits++;
    return val;
}

int expireIfNeeded(redisDb *db, robj *key) {
    mstime_t when = getExpire(db,key);
    mstime_t now;

    //......
    if (server.masterhost != NULL) return now > when;

  	//当前时间小于when则说明没过期
    if (now <= when) return 0;

   	//已过期删除过期key,并发布当前key过期的事件
    server.stat_expiredkeys++;
    propagateExpire(db,key);
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);
    //删除过期key
    return dbDelete(db,key);
}

定期删除

redis所有的时间事件都会在循环时不断调用serverCron方法执行,而该方法内部有个内部databasesCron方法,该方法内部就是实现随机抽检各个数据库中expires字典的key,如果过期则将其删除:

//事件事件,调用databasesCron进行随机抽检键值对进行定期删除
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
   	//......

   	//执行redis数据库定期任务操作
    databasesCron();

    	//......
}


void activeExpireCycle(int type) {
   	//......
	//遍历数据库
    for (j = 0; j < dbs_per_call; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);

       
        current_db++;

      
        do {
          	//......
			
			//随机抽取expires当前中20个键值对
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
			//循环随机20个调用expires看看随机key是否到期,若到期则删除
            while (num--) {
                dictEntry *de;
                long long ttl;
				//随机获取一个dictEntry 
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                //查看到期时间与当前时间的差值
                ttl = dictGetSignedIntegerVal(de)-now;
                //如果到期则删除
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                if (ttl < 0) ttl = 0;
                ttl_sum += ttl;
                ttl_samples++;
            }

            //......
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
}

小结

以上便是笔者对于redis过期键值删除策略设计的全部源码分析,可以看到由于redis单线程的设计,对于每一项操作都在时间和空间上做了极致的设计和实现,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

参考

《redis设计与实现》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1884168.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

上传头像到Domino中

大家好&#xff0c;才是真的好。 首先&#xff0c;说一个消息&#xff0c;2024年6月25号HCL发布了一则公告&#xff0c;就是从2024年12月10号开始结束Notes/Domino 11.0.x版本的市场订单申请&#xff0c;从从2025年6月26号开始停止对Notes/Domino 11.0.x版本的产品技术支持&am…

宝塔linux网站迁移步骤

网站迁移到新服务器步骤 1.宝塔网站迁移&#xff0c;有个一键迁移工具&#xff0c;参考官网 宝塔一键迁移API版本 3.0版本教程 - Linux面板 - 宝塔面板论坛 (bt.cn)2 2.修改域名解析为新ip 3.如果网站没有域名&#xff0c;而是用ip访问的&#xff0c;则新宝塔数据库的wp_o…

Ubuntu机器安装rdkit指定版本,通过conda安装不需要make,有手就行。

阿里云购买Ubuntu 22.0机器 IP没错&#xff0c;访问外网没问题 图片中的命令放在下面了。 useradd test-user -s /bin/bash mkdir /home/test-user chown -R test-user: /home/test-user passwd test-uservi /etc/sudoers wget -c https://repo.anaconda.com/archive/Anacon…

springcloud-gateway 网关组件中文文档

Spring Cloud网关 Greenwich SR5 该项目提供了一个基于Spring生态系统的API网关&#xff0c;其中包括&#xff1a;Spring 5&#xff0c;Spring Boot 2和项目Reactor。Spring Cloud网关的目的是提供一种简单而有效的方法来路由到API&#xff0c;并向它们提供跨领域的关注&#x…

[快易签]免越狱苹果签名工具快易签自用证书签名教程学会了可签一切应用

相关地址 快易签官网&#xff1a;快易签 定制版&#xff1a;快易签.定制款(含证书) 自签版&#xff1a;https://s1.kyq1.cn/ 免费源&#xff1a;https://app.eqishare.com/appstore 网盘&#xff1a;路灯网盘-iOS砸壳分享网-IPA分享网-巨魔商店IPA软件资源-后厂村路灯的网…

白话负载均衡、正反向代理(入门科普版)

什么是负载均衡&#xff1f;为什么需要负载均衡 从字面上理解&#xff0c;什么是负载&#xff0c;服务器承受访问量的大小是负载&#xff0c;但是单台服务器的访问性能是有限的&#xff0c;最典型的例子就是双十一、春运抢票这种&#xff0c;这时候就需要一种方案来解决这类问…

互联网框架五层模型详解

注&#xff1a;机翻&#xff0c;未校对。 What is the Five Layers Model? The Framework of the Internet Explained 五层模型互联网框架解释 Computer Networks are a beautiful, amazing topic. Networks involve so much knowledge from different fields, from physics…

idea启用多个环境

背景 在平常的后端开发中&#xff0c;需要与前端联调&#xff0c;比较方便的是让前端直接连自己的本地环境&#xff08;毕竟每次都要打包部署到测试环境实在是太麻烦了&#xff09;。但是这样子也有点不好&#xff0c;就是自己功能还没写好呢&#xff0c;结果前端连着自己的环…

LLaVA1.5训练数据和时间分析

LLaVA的PT+SFT训练_llava sft-CSDN博客文章浏览阅读379次。这个阶段,使用8个A100(80G)训练LLaVA-v1.5-13B大约需要20h。全量微调,非lora跑不起来啊,以前一直用swift,llama-factory这种框架式的代码库,但用原作者开源的代码也是有很多好处的。在这个阶段,使用 8 个 A100(…

Web端登录页和注册页源码

前言&#xff1a;登录页面是前端开发中最常见的页面&#xff0c;下面是登录页面效果图和源代码&#xff0c;CV大法直接拿走。 1、登录页面 源代码&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"><title>登录</ti…

云计算【第一阶段(24)】Linux文件系统与日志分析

一、文件与存储系统的inode与block 1.1、硬盘存储 最小存储单位&#xff1a;扇区(sector) 每个扇区大小&#xff1a;512字节 1.2、文件存取 最小存取单位&#xff1a;块(block)连续八个扇区组成&#xff1a;块(block) 每个块大小&#xff1a;4K文件数据&#xff1a;实际数据…

为什么我学个 JAVA 就已经耗尽所有而有些人还能同时学习多门语言

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「JAVA的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01;我的入门语言是C&#xff0c…

网安小贴士(3)网安协议

一、前言 网络安全协议是构建安全网络环境的基础&#xff0c;它们帮助保护网络通信免受各种威胁和攻击。 二、定义 网络安全协议是指在计算机网络中用于确保网络通信和数据传输安全的协议。它们定义了在网络通信过程中的安全机制、加密算法、认证和授权流程等&#xff0c;以保…

SOC模块LoRa-STM32WLE5有哪些值得关注

SoC 是片上系统的缩写&#xff0c;是一种集成芯片&#xff0c;集成了计算机或其他电子系统的所有或大部分组件。这些组件通常包括中央处理器 (CPU)、内存、输入/输出接口和辅助存储接口。包含数字、模拟、混合信号和通常的 RF 信号处理功能&#xff0c;具体取决于应用。片上系统…

Kotlin扩展函数(also apply run let)和with函数

also apply run let with的使用例子 private fun testOperator() {/*** also*/val person Person("ZhangSan", 18)person.also {// 通常仅仅打印使用, 也可以通过it修改it.name "ZhangSan1"println("also inner name: " it.name)}println(&qu…

DevOps认证是什么?DevOps工具介绍

DevOps 这个词是由Development&#xff08;开发&#xff09; 和 Operations&#xff08;运维&#xff09;组合起来的&#xff0c;你可以把它理解成为一种让开发团队和运维团队紧密合作的方法。 DevOps从2009年诞生到现在已经14年多了&#xff0c;一开始大家还在摸索&#xff0…

【gitee使用教程】(创建项目仓库并上传代码简易版)

gitee使用教程&#xff0c;创建项目仓库并上传代码简易版 1.在码云上创建一个仓库2.将代码克隆到本地1.复制仓库地址2.找到你想要放置的文件位置&#xff0c;右键点击更多选项&#xff0c;选择Git Clone3.将复制的仓库地址填入URL 3. IDEA结合GIT和Gitee的简单使用idea需要识别…

69. x 的平方根(简单)

69. x 的平方根 1. 题目描述2.详细题解3.代码实现3.1 Python方法一&#xff1a;逐个遍历方法二&#xff1a;二分查找 3.2 Java 1. 题目描述 题目中转&#xff1a;69. x 的平方根 2.详细题解 不能使用系统内置的函数&#xff0c;寻找某个数&#xff08;假定为x&#xff09;的…

【传知代码】揭秘AI如何揪出图片中的“李鬼”(论文复现)

在数字化时代&#xff0c;我们时常被各种图像信息所包围。然而&#xff0c;这些图像中有时隐藏着不为人知的秘密——被篡改的文字或图像。这些被篡改的内容可能误导我们的判断&#xff0c;甚至在某些情况下造成严重的后果。幸运的是&#xff0c;随着人工智能&#xff08;AI&…

淘宝用户行为分析大数据可视化

文章目录 1. 项目概述2. 技术栈3. 目录结构4. 数据处理流程5. 前端部分5.1 HTML (index.html)5.2 CSS (layer.css)5.3 JavaScript (chart.js) 6. 后端部分7. 数据可视化7.1 ECharts 图表 8. 主要功能模块9. 代码解析10. 数据接口11. 项目功能描述12. 代码功能实现12.1 HTML (in…