背景

为了减少占用内存空间,通常会对放到 Redis 中的键通过 expire 设置一个过期时间,那 Redis 是怎么实现对过期键删除的呢?

设置过期时间

设置过期时间的四种方式
# 将 key 的过期时间设置为 ttl 秒expire <key> <ttl> # 将 key 的过期时间设置为 ttl 毫秒pexpire <key> <ttl># 将 key 的过期时间设置为 timestamp 指定的秒数时间戳expire <key> <timestamp># 将 key 的过期时间设置为 timestamp 指定的毫秒数时间戳pexpire <key> <timestamp>

其中前三种方式都会转化为最后一种方式来实现过期时间

expire命令实现

命令转化

保存过期时间

我们看下 redisDb 的结构

typedef struct redisDb {    dict *dict;                 /* The keyspace for this DB */    dict *expires;              /* Timeout of keys with a timeout set */    ...}

可见在 redisDb 结构的 expire 字典(过期字典)保存了所有键的过期时间

过期字典的键是一个指向键空间中的某个键对象的指针

过期字典的值保存了键所指向的数据库键的过期时间

带有过期键的例子

注意

图中过期字段和键空间中键对象有重复,实际中不会出现重复对象,键空间的键和过期字典的键都指向同一个键对象

过期键的判断

通过查询过期字典,检查下面的条件判断是否过期

  1. 检查给定的键是否在过期字典中,如果存在就获取键的过期时间

  2. 检查当前 UNIX 时间戳是否大于键的过期时间,是就过期,否则未过期

过期键的删除策略

惰性删除

在取出该键的时候对键进行过期检查,即只对当前处理的键做删除操作,不会在其他过期键上花费 CPU 时间

缺点:对内存不友好,如果一但键过期了,但会保存在内存中,如果这个键还不会被访问,那么久会造成内存浪费,甚至造成内存泄露

如何实现?

就是在执行 Redis 的读写命令前都会调用 expireIfNeeded 方法对键做过期检查

如果键已经过期,expireIfNeeded 方法将其删除

如果键未过期,expireIfNeeded 方法不做处理

惰性删除

对应源码 db.c/expireIfNeeded 方法

int expireIfNeeded(redisDb *db, robj *key) {    // 键未过期返回0    if (!keyIsExpired(db,key)) return 0;

    // 如果运行在从节点上,直接返回1,因为从节点不执行删除操作,可以看下面的复制部分    if (server.masterhost != NULL) return 1;

    // 运行到这里,表示键带有过期时间且运行在主节点上    // 删除过期键个数    server.stat_expiredkeys++;    // 向从节点和AOF文件传播过期信息    propagateExpire(db,key,server.lazyfree_lazy_expire);    // 发送事件通知    notifyKeyspaceEvent(NOTIFY_EXPIRED,        "expired",key,db->id);    // 根据配置(默认是同步删除)判断是否采用惰性删除(这里的惰性删除是指采用后台线程处理删除操做,这样会减少卡顿)    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :                                         dbSyncDelete(db,key);}
补充

我们通常说 Redis 是单线程的,其实 Redis 把处理网络收发和执行命令的操作都放到了主线程,但 Redis 还有其他后台线程在工作,这些后台线程一般从事 IO 较重的工作,比如刷盘等操作。

上面源码中根据是否配置 lazyfree_lazy_expire(4.0版本引进) 来判断是否执行惰性删除,原理是先把过期对象进行逻辑删除,然后在后台进行真正的物理删除,这样就可以避免对象体积过大,造成阻塞,后面会在深入研究下 Redis 的 lazyfree 原理 源码位置 lazyfree.c/dbAsyncDelete 方法

定期删除

定期策略是每隔一段时间执行一次删除过期键的操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU 时间的影响,同时也减少了内存浪费

Redis 默认会每秒进行 10 次(redis.conf 中通过 hz 配置)过期扫描,扫描并不是遍历过期字典中的所有键,而是采用了如下方法

  1. 从过期字典中随机取出 20 个键

  2. 删除这 20 个键中过期的键

  3. 如果过期键的比例超过 25% ,重复步骤 1 和 2

为了保证扫描不会出现循环过度,导致线程卡死现象,还增加了扫描时间的上限,默认是 25 毫秒(即默认在慢模式下,如果是快模式,扫描上限是 1 毫秒)

对应源码 expire.c/activeExpireCycle 方法

void activeExpireCycle(int type) {        ...        do {           ...            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)                // 选过期键的数量,为 20                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

            while (num--) {                dictEntry *de;                long long ttl;                // 随机选 20 个过期键                if ((de = dictGetRandomKey(db->expires)) == NULL) break;                ...                // 尝试删除过期键                    if (activeExpireCycleTryExpire(db,de,now)) expired++;                ...            }            ...           // 只有过期键比例 < 25% 才跳出循环        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);    }    ...}

补充

因为 Redis 在扫描过期键时,一般会循环扫描多次,如果请求进来,且正好服务器正在进行过期键扫描,那么需要等待 25 毫秒,如果客户端设置的超时时间小于 25 毫秒,那就会导致链接因为超时而关闭,就会造成异常,这些现象还不能从慢查询日志(之前分享过慢查询日志的文章 Redis慢查询日志)中查询到,因为慢查询只记录逻辑处理过程,不包括等待时间。

所以我们在设置过期时间时,一定要避免同时大批量键过期的现象,所以如果有这种情况,最好给过期时间加个随机范围,缓解大量键同时过期,造成客户端等待超时的现象

Redis 过期键删除策略

Redis 服务器采用惰性删除和定期删除这两种策略配合来实现,这样可以平衡使用 CPU 时间和避免内存浪费

AOF、RDB 和复制功能对过期键的处理

RDB文件

生成 RDB 文件

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

载入 RDB 文件

主服务器:载入 RDB 文件时,会对键进行检查,过期的键会被忽略

从服务器:载入 RDB文件时,所有键都会载入。但是会在主从同步的时候,清空从服务器的数据库,所以过期的键载入也不会造成啥影响

AOF文件

AOF 文件写入

当过期键被惰性删除或定期删除后,程序会向 AOF 文件追加一条 del 命令,来显示的记录该键已经被删除

AOF 重写

重启过程会对键进行检查,如果过期就不会被保存到重写后的 AOF 文件中

复制

从服务器的过期键删除动作由主服务器控制

主服务器在删除一个过期键后,会显示地向所有从服务器发送一个 del 命令,告知从服务器删除这个过期键

从服务器收到在执行客户端发送的读命令时,即使碰到过期键也不会将其删除,只有在收到主服务器的 del 命令后,才会删除,这样就能保证主从服务器的数据一致性

疑问点?

  1. 如果主从服务器链接断开怎么办?

  2. 如果发生网络抖动,主服务器发送的 del 命令没有传递到从服务器怎么办?

其实上面两个问题 Redis 开发者已经考虑到了,只是主从复制涉及到的知识点还挺多,下面我就简单的说下解决的思路,后面会再分享一篇主从复制的文件

首先看疑问点1-如果主从服务器链接断开怎么办?

Redis 采用 PSYNC 命令来执行复制时的同步操作,当从服务器在断开后重新连接主服务器时,主服务器会把从服务器断线期间执行的写命令发送给从服务器,然后从服务器接收并执行这些写命令,这样主从服务器就会达到一致性,那主服务器如何判断从服务器断开链接的过程需要哪些命令?主服务器会维护一个固定长度的先进先出的队列,即复制积压缓冲区,缓冲区中保存着主服务器的写命令和命令对应的偏移量,在主服务器给从服务器传播命令时,同时也会往复制积压缓冲区中写命令。从服务器在向主服务器发送 PSYNC 命令时,同时会带上它的最新写命令的偏移量,这样主服务器通过对比偏移量,就可以知道从服务器从哪里断开的了

然后,我们再来看疑问点2-如果发生网络抖动,主服务器发送的 del 命令没有传递到从服务器怎么办?

其实主从服务器之间会有心跳检测机制,主从服务器通过发送和接收 REPLCONF ACK 命令来检查两者之间的网络连接是否正常。当从服务器向主服务器发送 REPLCONF ACK 命令时,主服务器会对比自己的偏移量和从服务器发过来的偏移量,如果从服务器的偏移量小于自己的偏移量,主服务器会从复制积压缓冲区中找到从服务器缺少的数据,并将数据发送给从服务器,这样就达到了数据一致性

小结

本文主要分析了 Redis 的过期策略是采用惰性删除和定期删除两种策略配合完成,然后简单看了两种策略的源码和是怎么实现的。最后介绍了 Redis 在进行 RDB 、 AOF 和主从复制操作时,如何对过期键进行处理,特别介绍了主从复制在发生主从链接断开和网络抖动命令丢失是如何处理的,希望大家看完能有收获

参考资料

《Redis设计与实现》第二版.黄健宏

《Redis深度历险:核心原理与深度实战》.老钱

https://yq.aliyun.com/articles/205504

Redis 的过期策略是如何实现的?相关推荐

  1. 【Redis】回顾下Redis的过期策略

    CSDN话题挑战赛第2期 参赛话题:面试宝典 金九银十之际,一定有很多朋友出去面试,通过每一次的面试,都可以检查一下自己的技术能力是什么样的,所以在面试前的准备是必不可少的环节. 俗话说的好:从不打无 ...

  2. Redis缓存过期策略

    转载出处链接 一.背景 线上你写代码的时候,想当然的认为写进 redis 的数据就一定会存在,后面导致系统各种 bug,谁来负责? 常见的有两个问题: 往 redis 写入的数据怎么没了? 可能有同学 ...

  3. Redis的过期策略以及内存淘汰机制

    Redis的过期策略以及内存淘汰机制 我们知道,redis中缓存的数据是有过期时间的,当缓存数据失效时,redis会删除过期数据以节省内存,那redis是怎样删除过期数据的?删除过期数据的策略是什么? ...

  4. 谈谈 Redis 的过期策略

    在日常开发中,我们使用 Redis 存储 key 时通常会设置一个过期时间,但是 Redis 是怎么删除过期的 key,而且 Redis 是单线程的,删除 key 会不会造成阻塞.要搞清楚这些,就要了 ...

  5. 13. 谈谈 Redis 的过期策略

    谈谈 Redis 的过期策略 定期删除策略 从库的过期策略 懒惰删除策略 unlink flush 异步队列 更多异步删除点 内存淘汰机制 LRU 算法 近似 LRU 算法 LFU 在日常开发中,我们 ...

  6. 关于Redis数据过期策略

    前言 在项目中某场景下,需要频繁去设置redis数据的过期时间,因此去了解了下redis数据过期策略.原文地址:关于Redis数据过期策略 一.Redis中key的的过期时间 通过EXPIRE key ...

  7. 复习Java第二个项目仿QQ聊天系统 01(界面部分) Java面试题Redis的过期策略和内存淘汰策略生活【记录一个咸鱼大学生三个月的奋进生活】023

    记录一个咸鱼大学生三个月的奋进生活023 复习Java(仿QQ聊天系统01界面部分) 设置背景(ImgPanel)类 登录界面(LoginFrame)类 注册界面(RegisterFrame)类 好友 ...

  8. redis的过期策略

    Redis 所有的数据结构都可以设置过期时间,时间一到,就会自动删除.你可以想象 Redis 内部有一个死神,时刻盯着所有设置了过期时间的 key,寿命一到就会立即收割. 你还可以进一步站在死神的角度 ...

  9. Redis学习笔记--Redis数据过期策略详解==转

    本文对Redis的过期机制简单的讲解一下 讲解之前我们先抛出一个问题,我们知道很多时候服务器经常会用到redis作为缓存,有很多数据都是临时缓存一下,可能用过之后很久都不会再用到了(比如暂存sessi ...

最新文章

  1. 机器学习集成学习与模型融合!
  2. elementary OS 6 评测!
  3. 政务大数据共享难题的破解路径有哪些?
  4. 什么是bps (bits per second)?—Vecloud微云
  5. 在ASP.NET Core中使用Apworks开发数据服务:对HAL的支持
  6. PrimeFaces Extensions中的全新JSF组件
  7. 一维数组求最大值,和三元运算符运算源码
  8. 【LeetCode笔记】46. 全排列(Java、DFS回溯、队列)
  9. linux安装jdk环境
  10. vidalia 更换浏览器代理
  11. 0基础学python做什么工作好-零基础自学多久Python可以找什么工作
  12. Core 提交返回500 问题 记录 来自网上文章
  13. 计算机中丢失d3dx11 43.dll,电脑为何玩了游戏都有什么丢失d3dx11_34.dll
  14. 【VScode技巧】:VScode界面显示模糊
  15. Activiti使用教程
  16. node js fcoin api 出现 api key check fail : {status:1090,msg:Illegal API signature}
  17. Understanding ISP Pipeline - Noise Reduction
  18. openGL中的坐标系
  19. pytorch深度学习入门与实战——今天我们来对一张图像进行卷积、池化,以及激活层的使用展示
  20. 惠普刀片服务器硬件安装配置手册

热门文章

  1. 《C指针》学习笔记( 第四、五章)指针与字符串、指针与多维数组
  2. 论一个优秀的小牛论坛管理员(转:五只鹳狸猿进笼观猴,各留一只爪)
  3. 数学基础知识总结 —— 7. 行列式的基本知识
  4. 【.net 深呼吸】自己动手来写应用程序设置类
  5. 新型冠状肺炎——这个不一般的春节
  6. xcode 配置wechat_Xcode 真机调试微信支付 提示 mainfest.json配置APPID和订单的appid 不一致...
  7. 秒云获得阿里云首批产品生态集成认证,携手阿里云共建云原生智能运维生态服务
  8. 精彩回顾|展会圆满收官,落幕不散场,期待与您的再次相遇,下一站上海!
  9. dbca 命令行静默方式创建Oracle RAC
  10. Groovy~Groovy介绍