Redis第四讲——Redis的数据库结构、删除策略及淘汰策略

一、redis中的数据库

  • redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中。
  • db数组的每项都是一个redis.h/redisDb结构,而每个redisDb结构就代表一个数据库。
  • 在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库。
struct redisServer {
    // ...
    // 一个数组,保存着服务器中的所有数据库
    redisDb *db;
    // ...
    // 服务器的数据库数量
    int dbnum;
    // ...
};

dbnum属性的值是由redis.conf配置文件中的databases来决定的,默认为16个。

二、数据库的切换(select命令)

我们先用redis可视化工具连上我们本地的redis,如图:

默认情况下,Redis客户端的目标数据库为0号数据库,但可以用select命令来切库:  

在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针:

typedef struct redisClient {
// ...
// 记录客户端当前正在使用的数据库
redisDb *db;
// ...
} redisClient;

简单地说就是:通过修改redisClient.db指针,让它指向服务器中的不同数据库,就可以实现切换数据库的功能——这就是select命令的实现原理。

三、数据库键空间(key space)

redis是一个键值对(key-value)的数据库,每个数据库都由一个redis.h/redisDb结构表示,而redisDb结构的dict字典则保存了数据库中所有键值对,我们通常称之为键空间(key space

typedef struct redisDb {
    //数据库键空间,保存数据库中所有键值对
    dict *dict;                 
    dict *expires;    //过期字典,保存键的过期时间(4.2会提到)          
    dict *blocking_keys;        
    dict *ready_keys;          
    dict *watched_keys;         
    int id;                    
    long long avg_ttl;      
} redisDb;

键空间的每个键(key)都是字符串对象,而值(value)则可以是字符串、列表、哈希、集合等对象中的任意一种。

举个例子:

 执行上述命令后,数据库的键空间将会是下图的样子:

四、过期键

有时候我们希望给某些键一个过期时间,即希望它存活一段时间就失效,redis同样也给我们提供了这样的机制。

4.1 设置过期时间

4.1.1 expire和pexpire

expire用于设定某个键的过期时间,单位是秒,格式如下:

expire [key] [time]

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> expire hello 10
(integer) 1
127.0.0.1:6379> get hello
(nil)

可以看到,10秒后redis删除了hello键,与之对应的还有一个pexpire命令,它的time时间单位为毫秒,即[pexpire hello 5]经过5毫秒后删除hello键。

4.1.2 expireat和pexpireat

expireat用于设定某个键在某个具体Unix时间戳过期,单位为秒,基本格式如下:

expireat [key] [time]

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> time
1) "1704285269"  //当前Unix时间戳
2) "434279"
127.0.0.1:6379> expireat hello 1704285289  //时间戳到1704285289时删除
(integer) 1 
127.0.0.1:6379> get hello
(nil)

过期键会在我们指定的Unix时间戳删除,当然它也有一个对应毫秒单位的命令——pexpireat

ps:当然也可以用setex,在设置一个字符串键的同时设置过期时间,但他仅限于string数据类型,这里就不介绍了。

4.1.3 ttl、pttl和persist

ttlpttl两个命令用于查看过期键还剩余多少时间。

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> expire hello 20
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 17
127.0.0.1:6379> pttl hello
(integer) 10317 //毫秒单位,约为10.3秒

persist用于移除某个键的过期时间,使其永久有效:

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> expire hello 100
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 96
127.0.0.1:6379> persist hello //移除hello键过期时间
(integer) 1 
127.0.0.1:6379> ttl hello
(integer) -1  //-1表示永久有效

4.2 保存过期时间

redisDb结构的expires指针保存了数据库中所有键的过期时间,我们称之为过期字典:

  • 过期字典的键是一个指针,这个指针指向键空间中的某个键对象。
  • 过期字典的值是一个longlong类型的整数,这个整数保存了键所指向的数据库键的过期时间——一个毫秒精度的UNIX时间戳

我们给person、abc键设置过期时间:

127.0.0.1:6379> pexpireat person 1735660800000 //2025年1月1日 00:00:00
(integer) 1
127.0.0.1:6379> pexpireat abc 1735664400000  //2025年1月1日 01:00:00
(integer) 1
127.0.0.1:6379> pttl person
(integer) 31374276693
127.0.0.1:6379> pttl abc
(integer) 31377871933

那么此时对应的键空间如下图:

 

redis判断键是否过期的大致步骤如下:

  • 检查键是否存在过期字典,如果存在则取到过期时间。
  • 判断当前UNIX时间戳是否大于键的过期时间,如果是,那么此键就过期,反之则未过期。

五、删除策略(避免内存泄漏)

我们每设置一个键的过期时间,redis就会在过期字典中保存一份。当键过期后,如果没有触发删除策略的话,过期后的数据依然会保存在内存中,即便已经过期,我们还是能够获取到这个键的数据。那么它们如何被删除呢,有三种策略,下面我们介绍下。

5.1 定时删除

  • 定时删除:在设置键的过期时间同时,创建一个定时器(timer),到了过期时间,立即执行对建的删除操作。

很显然,这是一种时间换空间的做法:

  • 优点:对内存友好,通过定时器可以保证过期的建尽可能快的被删除,从而释放内存。

  • 缺点:

    • 对CPU很不友好,在过期键比较多的情况下,删除操作会占用一部分CPU时间,在内存不紧张但CPU紧张的情况下,将CPU时间用在删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。

    • 除此之外,创建一个定时器需要用到redis服务器中的时间事件,而当前时间事件的实现方式——无序链表,查找一个事件的时间复杂度位O(N)——并不能高效地处理大量时间事件。

5.2 惰性删除

  • 不会主动去删除过期的键,而是在你要获取某个键时,会先检查一下这个键是否过期,如果没过期就返回给你,过期就会删除这个键。

很显然,这是一种空间换时间的做法:

  • 优点:对CPU友好,程序只会在获取键的时候进行过期检查,并不会在删除其它无关的过期键上花费任何CPU时间。

  • 缺点:对内存不友好,如果有非常多的过期键,并且这些键不会被访问到,那么它们将会永远不会被删除(除非flushdb),这可能会导致内存泄漏的风险。

5.3 定期删除

  • 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。

定期删除则是对定时和惰性删除的一种折中方案:

  • 优点:

    • 定期删除策略每隔一段时间执行一次删除过期键的操作,并会限制删除操作执行的时长和频率来减少对CPU时间的影响。

    • 通过定期删除,可以有效地减少了因为过期键带来的内存浪费。

  • 缺点:

    • 如果删除操作执行太频繁或时间太长,定期删除则会退化为定时删除。

    • 如果删除操作执行的太少,又会退化为惰性删除。

所以,定期删除虽然是一个这种方案,但执行时长和频率难以把握。

5.4 redis中的删除策略

前面提到了三种策略,而redis采用的则是惰性删除+定期删除两种策略,那么它俩之间是如何配合的呢,我们一起来看看。

5.4.1 惰性删除

惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的redis命令在执行之前都会调用该函数对输入键进行检查,大致流程也很简单:

  • 对所有读写命令进行检查(调用expireIfNeeded函数)。

  • 判断键是否过期,如果过期则删除键,没过期就不做任何操作。

ps:甭管键过没过期都会执行实际的命令流程,比如get命令,如果键过期则会被删除,返回结果为null,如果没过期就返回实际的值。

5.4.2 定期删除

定期删除由redis.c/activeExpireCycle函数实现,redis默认每隔100ms就随机抽取部分设置了过期时间的key,检查这些key是否过期,如果过期就删除。

100ms的执行周期是默认的,可以在redis.conf文件中更改:

它的执行频率由hz参数值指定,默认是10,也就是每一秒执行10次。

注释翻译:

Redis调用内部函数执行许多后台任务,例如关闭超时的客户端连接,清除从未被请求的过期键等。 并非所有任务的执行频率都相同,但Redis会根据指定的“hz”值检查要执行的任务。 “hz”被设置为10。提高该值将在Redis空闲时使用更多的CPU,但同时在有许多键同时过期且需要更高的精度的情况下,会使Redis的响应更快。 范围介于1和500之间。然而,通常不建议超过100的值,而应该默认使用10,并将其升至100的值仅适用于需要非常低延迟的环境。大多数用户不需要设置高于10的“hz”值。

5.0版本之前,hz参数一旦设定就会被固定,但如果链接数比较多的情况下,10的默认值可能就不能够满足这种情况,就需要手动去更改hz的值,这样就很不方便。

redis 5.0之后,有了dynamic-hz参数,默认就是打开的,当连接数很多时,就会自动加倍hz,以便处理更多的链接:

注释翻译:

通常情况下,拥有与连接的客户端数量成比例的 Hz 值是很有用的。例如,这对于在每次后台任务调用期间避免处理太多客户端以避免延迟峰值是很有用的。 由于默认情况下,HZ 值被保守地设置为 10,Redis 提供并默认启用了使用自适应 Hz 值的能力,该值在存在许多连接的情况下会临时增加。 启用动态 Hz 时,实际配置的 Hz 值将作为基线使用,但在连接更多客户端时,将根据需要使用配置的 Hz 值的倍数。这样,空闲实例的 CPU 时间将很少,而繁忙实例将更加响应。

那么它到底是这么删的呢,源码如下

for (j = 0; j < dbs_per_call; j++) {
 int expired;
 redisDb *db = server.db+(current_db % server.dbnum);
 current_db++;
 /* 超过25%的key已过期,则继续. */
 do {
  unsigned long num, slots;
  long long now, ttl_sum;
  int ttl_samples;
 
  /* 如果该db没有设置过期key,则继续看下个db*/
  if ((num = dictSize(db->expires)) == 0) {
   db->avg_ttl = 0;
   break;
  }
  slots = dictSlots(db->expires);
  now = mstime();
 
  /*但少于1%时,需要调整字典大小*/
  if (num && slots > DICT_HT_INITIAL_SIZE &&
   (num*100/slots < 1)) break;
 
  expired = 0;
  ttl_sum = 0;
  ttl_samples = 0;
 
  if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
   num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;// 20
 
  while (num--) {
   dictEntry *de;
   long long ttl;
 
   if ((de = dictGetRandomKey(db->expires)) == NULL) break;
   ttl = dictGetSignedIntegerVal(de)-now;
   if (activeExpireCycleTryExpire(db,de,now)) expired++;
   if (ttl > 0) {
    /* We want the average TTL of keys yet not expired. */
    ttl_sum += ttl;
    ttl_samples++;
   }
  }
 
  /* Update the average TTL stats for this database. */
  if (ttl_samples) {
   long long avg_ttl = ttl_sum/ttl_samples;
 
   /样本获取移动平均值 */
   if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
   db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
  }
  iteration++;
  if ((iteration & 0xf) == 0) { /* 每迭代16次检查一次 */
   long long elapsed = ustime()-start;
 
   latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
   if (elapsed > timelimit) timelimit_exit = 1;
  }
 /* 超过时间限制则退出*/
  if (timelimit_exit) return;
  /* 在当前db中,如果少于25%的key过期,则停止继续删除过期key */
 } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}

大致的逻辑如下:

  • 依次遍历每个db(默认是16个),针对每个db随机选择20个设置了生存时间的(ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)键,并对过期的键进行删除。
  • 如果被删除的key超过了25%,再次随机筛选出20个设置了生存时间的key....
  • 如果被删除的key不超过25%,这次定期删除结束。

六、内存淘汰策略(避免内存溢出)

本来没想介绍这节的,但发现删除策略和淘汰策略关系还挺密切的,索性一起介绍了吧。

我们现在想一个问题,定期+惰性可以保证过期的key一定会被删掉,但是只能保证最终一定会被删除,要是定期删除遗漏了大量的过期键,而且很长一段时间都不会访问这些键,那么久而久之redis内存可能会被耗尽,由于可能会存在这样的问题,所以redis又引入了“内存淘汰机制”来解决:

当Redis的内存空间不足,还需要再存储数据时,就会触发淘汰策略,默认策略就是抛出异常…………

  • volatile-lru -> Evict using approximated LRU among the keys with an expire set.

        在设置了生存时间的key中,采用最近最少使用的策略删除key

  • allkeys-lru -> Evict any key using approximated LRU.

        在全部的key中,采用最近最少使用的策略删除key

  • volatile-lfu -> Evict using approximated LFU among the keys with an expire set.

        在设置了生存时间的key中,采用最近最少频次使用的策略删除key

  • allkeys-lfu -> Evict any key using approximated LFU.

        在全部的key中,采用最近最少频次使用的策略删除key

  • volatile-random -> Remove a random key among the ones with an expire set.

        闹着玩,随机删……

  • allkeys-random -> Remove a random key, any key.

        闹着玩,随机删……

  • volatile-ttl -> Remove the key with the nearest expire time (minor TTL)

        在设置了生存时间的key中,删除剩余生存时间最少的key

  • noeviction(默认策略) -> Don't evict anything, just return an error on write operations.

        抛出异常!

那么如何选择,以下是腾讯针对redis淘汰策略给出的建议:

  • 当redis作为缓存使用的时候,推荐使用allkeys-lru。该策略会将最近最少使用的key淘汰。默认情况下,使用频率最低则后期命中的概率也最低,所以将其淘汰。
  • 当redis作为半缓存半持久化使用时,可以用volatile-lru。因为redis本身不建议保存持久化数据,所以只做备选方案。

 End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【shell漫步】2 运算符

碎碎念 上一章介绍了各种变量的定义和使用&#xff0c;这次要针对数字和文本这两种基本数据类型进行运算和判断了&#xff0c;shell中的运算包括&#xff1a; 对数字类型 算术运算&#xff08;对数字的 数学 运算&#xff09;关系运算&#xff08;用来做数字的条件判断&…

使用Go语言的HTTP客户端进行并发请求

Go语言是一种高性能、简洁的编程语言&#xff0c;它非常适合用于构建并发密集型的网络应用。在Go中&#xff0c;标准库提供了强大的HTTP客户端和服务器功能&#xff0c;使得并发HTTP请求变得简单而高效。 首先&#xff0c;让我们了解为什么需要并发HTTP请求。在许多应用场景中…

「Verilog学习笔记」任意奇数倍时钟分频

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 timescale 1ns/1nsmodule clk_divider#(parameter dividor 5) ( input clk_in,input rst_n,output clk_out );parameter CNT_WIDTH $clog2(dividor - 1) ; reg flag1, f…

「Verilog学习笔记」编写乘法器求解算法表达式

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 timescale 1ns/1nsmodule calculation(input clk,input rst_n,input [3:0] a,input [3:0] b,output [8:0] c);reg [8:0] data1, data2 ; assign c data2 ; always (posed…

基于ssm的订餐管理系统论文

基于JSP的订餐管理系统的设计与实现 摘要 当下&#xff0c;正处于信息化的时代&#xff0c;许多行业顺应时代的变化&#xff0c;结合使用计算机技术向数字化、信息化建设迈进。传统的订餐信息管理模式&#xff0c;采用人工登记的方式保存相关数据&#xff0c;这种以人力为主的…

微同城本地小程序源码系统:顺风车+二手市场+跑腿功能+信息发布+广告功能 带完整的搭建教程

随着移动互联网的普及&#xff0c;小程序已成为各行业进行线上业务拓展的重要工具。微同城作为一款集顺风车、二手市场、跑腿功能、信息发布和广告功能于一体的本地小程序源码系统&#xff0c;旨在满足现代城市居民的多元化需求&#xff0c;提供一个方便、快捷、实用的服务平台…

Linkage Mapper 工具参数详解——Building Network and Map Linkages

【小白一学就会无需其他教程】此文档用于解析使用Linkage Mapper 各输入输出参数详情以及可能的影响&#xff0c;并介绍了如何解释模型输出结果和输出参数&#xff0c;适合刚入手的人。篇幅很长很啰嗦&#xff0c;是因为每个参数都解释的万分细致。 从以下链接中获取内容&#…

FPGA——VIVADO生成固化文件,掉电不丢失

VIVADO生成固化文件 (1)加入代码(2)生成bin文件&#xff0c;并且下载 (1)加入代码 设计文件(.xdc)中加入这段代码: set_property CFGBVS VCCO [current_design] set_property CONFIG_VOLTAGE 3.3 [current_design] set_property BITSTREAM.GENERAL.COMPRESS true [current_de…

【SpringBoot】-Spring MVC详解

作者&#xff1a;学Java的冬瓜 博客主页&#xff1a;☀冬瓜的主页&#x1f319; 专栏&#xff1a;【Framework】 主要内容&#xff1a;SpringMVC项目的创建&#xff0c;关于使用SpringMVC框架前端传参和后端获取参数。关于SpringMVC框架后端返回数据的实战&#xff0c;如返回静…

1.2 ARCHITECTURE OF A MODERN GPU

图1.2显示了典型的支持CUDA的GPU架构的高级视图。它被组织成一系列高线程的流式多处理器&#xff08;SM&#xff09;。在图中1.2&#xff0c;两个SM构成一个 block。然而&#xff0c;构建块中的SM数量可能因代而异。此外&#xff0c;在图中&#xff0c;每个SM都有多个共享控制逻…

工作中人员离岗识别摄像机

工作中人员离岗识别摄像机是一种基于人工智能技术的智能监控设备&#xff0c;能够实时识别员工离岗状态并进行记录。这种摄像机通常配备了高清摄像头、深度学习算法和数据处理系统&#xff0c;可以精准地监测员工的行为&#xff0c;提高企业的管理效率和安全性。 工作中人员离岗…

医院信息系统集成平台—后台运维管理系统

随着信息化建设的推进,为了让凝聚了巨大人力物力投入的信息基础设施发挥出其效益,保障整个信息系统的平稳可靠运行,需要有一个可从整体上对包括服务器、网络,存储,安全等组件在内的IT基础设施环境进行综合管理的平台,并能够提供业务系统运行异常的实时告警和进行图形化问…

【Python】开始你的Python之旅(Anaconda、Pycharm、Jupyter)

Python工具准备 下载安装AnacondaPycharmJupyter Notebook 启动使用AnacondaPycharmJupyter Notebook 引言&#xff1a; 信息时代&#xff0c;计算机引领。人工智能&#xff0c;Python是基础。信息时代学习好Python乃是在人工智能时代的立足之本。 本文&#xff1a; 做好Pyth…

透明触摸屏展示柜的安装,需要注意什么

透明触摸屏展示柜的安装需要注意以下几个方面&#xff1a; 确定安装位置&#xff1a;选择一个合适的位置&#xff0c;确保展示柜的摆放位置合理&#xff0c;便于观看和管理。同时&#xff0c;要考虑到电源和信号线的连接&#xff0c;以及展示柜与周围环境的协调性。 检查透明触…

MYSQL多种提权方式

&#x1f419;MYSQL-提权条件 - 数据库的最高权限用户的密码 - secure-file-priv没进行目录限制 - 拿下了网站的权限&#xff08;通过webshell或者其他方式&#xff09; - 获取到了数据库的账号密码 &#xff08;获取密码&#xff1a;D:/phpstudy/MySQL/data/mysql/user.MYD…

【数据结构】——期末复习题库(6)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

亚马逊用虚拟信用卡注册可以吗?

很多想做海外广告客户&#xff0c;由于想要投放Google、 Facebook广告&#xff0c;都需要虚拟信用卡&#xff0c;我们对信用卡研究也是有一定深入。但是&#xff0c;当在线购物或者邮件支付时&#xff0c;小伙伴们会担心信息的泄漏&#xff0c;而虚拟信用卡为付款创建了额外的安…

Android 集成vendor下的模块

Android 集成vendor下的模块 &#xff0c;只需要在 PRODUCT_PACKAGES 加上对应的模块名&#xff0c;编译的时候就会执行对应模块的bp文件&#xff0c;集成到系统中 PRODUCT_PACKAGES \WallpaperPicker \Launcher3 \com.nxp.nfc Android11 Framework Vendor下自定义系统…

硬件开发避坑日志

FT232 串口工具会,多发和漏发0x00. 对比之下STC工具更稳定。 红外接收关 5 V 和 3.3V 工作电压的接收波形不一样。 3.3V工作不正常&#xff0c;接收不正常 。 5V工作是标准的NEC协议

一文读懂 $mash 通证 “Fair Launch” 规则(幸运池玩法解读篇)

Solmash 是 Solana 生态中由社区主导的铭文资产 LaunchPad 平台&#xff0c;该平台旨在为 Solana 原生铭文项目&#xff0c;以及通过其合作伙伴 SoBit 跨链桥桥接到 Solana 的 Bitcoin 生态铭文项目提供更广泛的启动机会。有了 Solmash&#xff0c;将会有更多的 Solana 生态的铭…
最新文章