Redis LFU缓存淘汰算法

news2025/3/15 11:36:40

前言

Redis 在 4.0 版本之前的缓存淘汰算法,只支持 random 和 lru。random 太简单粗暴了,可能把热点数据给淘汰掉,一般不会使用。lru 比 random 好一点,会优先淘汰最久没被访问的数据,但是它也有一个缺点,就是无法真正表示数据的冷热程度。
如下示例,A 之前被频繁访问,B 在执行 LRU 淘汰前恰巧被访问了一次,记录了最新的时间戳。此时触发 LRU 淘汰算法,反而会把 A 给淘汰掉。但事实是,A 的热度明显比 B 高。
image.png
针对这个问题,Redis 终于在 4.0 版本推出了全新的缓存淘汰策略:LFU。
LFU(Least Frequently Used)也叫 最不频繁使用 算法,它会把访问频率最低的数据给淘汰掉。举个例子:A 十分钟内访问了十次,B 十分钟内访问了五次,哪怕 B 是最后一次被访问的,因为它访问频率低,所以会优先淘汰。
要想启用 LFU 淘汰策略,首先要配置最大内存:

maxmemory 100MB

然后配置淘汰策略:

maxmemory-policy volatile-lfu / allkeys-lfu

有俩选项,它俩区别是:

  • allkeys-lfu:针对所有键值对的 LFU 淘汰策略
  • volatile-lfu:仅针对设置了过期时间的键值对的 LFU 淘汰策略

这样就开启 LFU 淘汰策略了,但是,关于 LFU 还有两个配置你也要关心:

lfu-log-factor 10
lfu-decay-time 1

它们的作用分别是:

  • lfu-decay-time:LFU 计数器衰减的时间单位,默认一分钟
  • lfu-log-factor:LFU 计数器递增的系数,值越大,递增的难度越大

Redis LFU实现

和 LRU 一样,Redis 也没有提供严格的 LFU 实现,因为开销太大了,也是近似 LFU 算法。并且,LFU 算法复用了 LRU 算法用到的 RedisObject 里的 lru 字段。Redis 会根据不同的淘汰策略写入不同的值。

LRU 算法和 LFU 算法不能同时启用

我们知道,Redis 会为键值对创建 RedisObject 对象,并在对象里用 24 Bit 来记录 lru 字段。如果配置的是 LFU 淘汰算法,则写入的是 LFU 相关的信息。
要实现 LFU 算法需要统计两个维度的数据,才能计算访问频率:

  • 访问的次数
  • 访问的时间

但是 Redis 只有一个 lru 字段能用,不得已而为之,Redis 不得不把 lru 拆分成两部分:高 16 位记录时间戳,低 8 位记录计数器。
Redis 先获取 LFU 时间戳,然后左移 8 位,低 8 位用来保存计数器,计数器的默认值是 5。

#define LFU_INIT_VAL 5
robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        // 当前分钟级时间戳保留低16位 | 默认计数器的值5
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        // 写入LRU时间戳
        o->lru = LRU_CLOCK();
    }
    return o;
}

LFU 时间戳是以 分钟 为单位的,因为只有 16 位,所以最多能表示 65535 分钟,约 45 天,超过这个时间就溢出了。

unsigned long LFUGetTimeInMinutes(void) {
    return (server.unixtime/60) & 65535;
}

之后每次访问 Key 都会修改 LFU 信息,方法是updateLFU() 。Redis 首先会计算衰减后的计数器值,再判断要不要递增,最后把新值重新写会对象。

void updateLFU(robj *val) {
    // 衰减并返回计数器
    unsigned long counter = LFUDecrAndReturn(val);
    // 根据规则递增计数器 不一定会递增
    counter = LFULogIncr(counter);
    // 重新写入时间戳和计数器
    val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}

衰减并返回计数器的方法是LFUDecrAndReturn() ,为什么计数器还要衰减呢?
因为 LFU 是按照访问频率的高低来淘汰缓存的,当缓存太久没被访问,它的计数器就应该被衰减,这样才能真实反映数据的热度,如果不衰减,不就变回 LRU 算法了嘛。
衰减的策略很简单,计算缓存闲置的以分钟为单位的时间,除以配置的lfu_decay_time 衰减时间单位,结果就是要衰减的值,计数器最多减到 0。默认衰减时间单位是一分钟,例如一小时没被访问,计数器就会衰减 60。

unsigned long LFUDecrAndReturn(robj *o) {
    // 时间戳
    unsigned long ldt = o->lru >> 8;
    // 计数器
    unsigned long counter = o->lru & 255;
    // 计算衰减值 lfu_decay_time:衰减时间单位,默认1分钟 闲置时间/lfu_decay_time
    unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
    // 返回衰减后的计数器
    if (num_periods)
        counter = (num_periods > counter) ? 0 : counter - num_periods;
    return counter;
}

计数器衰减以后,Redis 接下来要开始对计数器做递增操作了,但是不会轻易递增计数器。
为什么不是每次访问都递增计数器呢?你别忘了,计数器只有 8 Bit,最多也只能表示 255,如果每次访问 Key 都直接递增计数器,很容易一下就打满了,当所有 Key 的计数器都打满了,计数器就没有意义了,无法判断数据的热度了,因为大家都一样热。
所以,Redis 会有一套递增规则,方法是LFULogIncr() 。首先会取一个随机数,然后计算计数器与初始值的一个差值baseval ,用这个差值乘以一个递增系数lfu_log_factor +1 再求倒数记为阈值 p,只有当随机数小于阈值才会递增计数器。

uint8_t LFULogIncr(uint8_t counter) {
    if (counter == 255) return 255;
    // 获取随机数
    double r = (double)rand()/RAND_MAX;
    // 取一个基数,用来算更新阈值
    double baseval = counter - LFU_INIT_VAL;
    if (baseval < 0) baseval = 0;
    /**
     * 随机数小于阈值,计数器则加1,否则什么也不做
     * 1.counter越大,计数器增加的概率就越低
     * 2.lfu_log_factor越大,计数器增加的概率就越低 人为控制
     */
    double p = 1.0/(baseval*server.lfu_log_factor+1);
    if (r < p) counter++;
    return counter;
}

通过源码发现,随着计数器 counter 不断增大,递增的难度也随之增大,也就是越往后越难递增,这样就可以有效避免计数器很快被打满。
同时,Redis 还可以通过配置lfu_log_factor 递增系数,人为控制计数器递增的难度,值越大越难递增。默认系数是 10,大约命中 100 次,计数器加 10;命中 1000 次,计数器加 18。如下是不同系数下,命中率和计数器递增的关系。

+--------+------------+------------+------------+------------+------------+
| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
+--------+------------+------------+------------+------------+------------+
| 0      | 104        | 255        | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 1      | 18         | 49         | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 10     | 10         | 18         | 142        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 100    | 8          | 11         | 49         | 143        | 255        |
+--------+------------+------------+------------+------------+------------+

在缓存淘汰的处理上,LFU 和 LRU 的逻辑基本是一致的,Redis 会遍历数据库,然后随机采样一批 Key,衰减并返回计数器的值,以便更加真实的反映缓存的热度,然后按照计数器的值升序填充 EvictionPoolLRU 数组记为候选淘汰的 Key,然后优先淘汰计数器较小的 Key。

尾巴

Redis 支持三类缓存淘汰策略,分别是:随机、LRU 和 LFU。随机太简单粗暴,容易淘汰热数据,一般不用。LRU 只记录访问最新的时间戳,不能真实地反映数据的热度,所以也不能很好的淘汰掉真正冷的数据。LFU 是 LRU 的改进版本,它会按照数据的访问频率来淘汰数据,可以更真实的反映数据的热度。因为底层是复用的 lru 字段,所以 LFU 会把 lru 字段拆分成两部分,高 16 位记录访问时间戳,单位是分钟;低 8 位 记录计数器。因为记录的维度是访问频率,所以 Redis 会对计数器做衰减操作,越久不访问计数器衰减的越严重。同时,因为只有 8 位空间,最多能表示 255,所以 Redis 对计数器的递增策略采用对数递增的方式,不然每次访问都直接递增,计数器很容易一下被打满。

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

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

相关文章

JavaScript系列从入门到精通系列第二十篇:使用工厂方法创建JavaScript对象,JavaScript构造函数详解,JavaScript类概念的介绍

文章目录 一&#xff1a;使用工厂方法创建对象 1&#xff1a;原始写法 2&#xff1a;工厂方式 3&#xff1a;结果验证 二&#xff1a;构造函数 1&#xff1a;什么是构造函数 2&#xff1a;构造函数和普通函数的区别 3&#xff1a;构造函数的执行流程 三&#xff1a;类…

SpringBoot基础详解

目录 SpringBoot自动配置 基于条件的自动配置 调整自动配置的顺序 纷杂的SpringBoot Starter 手写简单spring-boot-starter示例 SpringBoot自动配置 用一句话说自动配置&#xff1a;EnableAutoConfiguration借助SpringFactoriesLoader将标准了Configuration的JavaConfig类…

RPC 接口测试技术 —— websocket 自动化测试实践!

WebSocket 是一种在单个 TCP 连接上进行全双工通信 (Full Duplex 是通讯传输的一个术语。通信允许数据在两个方向上同时传输&#xff0c;它在能力上相当于两个单工通信方式的结合。全双工指可以同时&#xff08;瞬时&#xff09;进行信号的双向传输&#xff08; A→B 且 B→A &…

Spring()

一、导学 二、 1.入门程序 spring快照版本是最新的版本&#xff0c;未发布。需要用到<repository></> 下面这个不需要配置仓库&#xff0c;直接写在依赖中就行 引入spring相关依赖 <?xml version"1.0" encoding"UTF-8"?> <proje…

QT实现凸凹边形等距缩放

参考&#xff1a;https://blog.csdn.net/weixin_39383896/article/details/99615371和https://blog.csdn.net/qq_15821883/article/details/117421400 代码逻辑思路&#xff1a; 1、获取向量AB、BC的坐标。 2、计算向量AB、BC的长度。 3、根据点乘获取cosθ大小。 4、根据cosθ…

LeetCode 高频题目分类列表

&#x1f4a1; LeetCode 高频面试题分类列表&#xff0c;总共24类&#xff0c;312道题目&#xff01; 图 133.克隆图 207.课程表 210.课程表 II 399.除法求值 547.省份数量 684.冗余连接 743.网络延迟时间 785.判断二分图 堆 215.数组中的第K个最大元素 295.数据流的中位数 26…

【性能测试篇1】初识性能测试

目录 性能测试定义 性能测试和功能测试有什么区别 测试工具上面&#xff1a; 特殊业务场景下&#xff1a; 性能测试常见概念&#xff1a; ①用户相关&#xff1a; 1.1并发用户数&#xff1a; 1.2在线用户数&#xff1a; 1.3系统用户数量&#xff1a; ②响应时间相关&…

Golang操作数据库简单示例

目录 准备工作准备数据创建项目连接数据库查询数据修改数据插入数据删除数据释放资源完整代码最终执行结果 准备工作 在开始之前&#xff0c;你需要确保自己安装了Golang的编程环境&#xff0c;安装MySQL数据库&#xff0c;有一个可以用于编写代码的编辑器或IDE工具。我在这里…

大型公共建筑能耗监测与信息管理系统研究及产品选型

摘要&#xff1a;文章通过阐述大型公共建筑能耗现状&#xff0c;突出大型公共建筑实施节能监管的必要性&#xff0c;并在系统总结运用技术手段实施建筑能耗监测的基础上&#xff0c;介绍了江苏省建筑能耗监测系统研究过程中的技术创新和应用情况。 关键词&#xff1a;公共建筑…

深度学习——含并行连接的网络(GoogLeNet)

深度学习——含并行连接的网络&#xff08;GoogLeNet&#xff09; 文章目录 前言一、Inception块二、GoogLeNet模型三、训练模型总结 前言 上篇文章中学习了NIN&#xff0c;而GoogLeNet吸收了NIN中串联网络的思想&#xff0c;并在此基础上做了改进。该论文中的一个观点是&…

PyQt5基础学习(一)

从PyQt5最基础的内容开始学习 import sysfrom PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QWidget, QApplication, QTextBrowserclass Example(QWidget):def __init__(self):super().__init__()self.initUI()def initUI(self):self.resize(300, 300)self.setWind…

数据结构之单链表的模拟实现

&#x1f495;"你笑的次数越多越好&#xff0c;因为你只有用笑才能不怀恶意地消灭罪恶。"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;数据结构之单链表的模拟实现 MyArrayList /*** Created with IntelliJ IDEA.* Description:* User: 绿字* …

F5.5G落进现实:目标网带来的光之路

数字化与智能化的世界将走向何方&#xff1f;这个问题有着非常复杂的答案&#xff0c;但其中有一个答案已经十分清晰。那就是智能化的下一步&#xff0c;必将走向泛在万兆的世界。 网络是算力联接的底座&#xff0c;是智能演化的基础。纵观每一代数字化升级&#xff0c;都可以发…

2023秋招华为技术岗线上面试经历

2023/10/16 个人情况&#xff1a;博士&#xff0c;预计2024年毕业&#xff0c;参加了2023秋招&#xff0c;华为应聘到3面主管面。 下面按招聘流程顺序&#xff0c;记录我的面试经历。因为想写详细一点的独立文章&#xff0c;所以想来想去还是放到CSDN上。 1. 宣讲会 宣讲会…

kr 第三阶段(一)16 位汇编

为什么要学习 16 位汇编&#xff1f; 16 位汇编包含了大部分 32 位汇编的知识点。有助于在学习内核的两种模式。 实模式&#xff1a;访问真实的物理内存保护模式&#xff1a;访问虚拟内存 有助于提升调试能力&#xff0c;调试命令与 OllyDbg 和 WinDebug 通用。可以学习实现反…

spring boot整合MongoDB 一

MongoDB介绍 应用场景 传统的关系型数据库&#xff08;如MySQL&#xff09;&#xff0c;在数据操作的“三高”需求以及应对Web2.0的网站需求面前&#xff0c;显得力不从心。 解释&#xff1a;“三高”需求&#xff1a; • High performance - 对数据库高并发读写的需求。 • …

制作.a静态库 (封盒)

//云库房间 1.GitHub上创建开源框架项目须包含文件&#xff1a; LICENSE:开源许可证&#xff1b;README.md:仓库说明文件&#xff1b;开源项目&#xff1b;(登录GitHub官网) 2. 云仓储库构建成功(此时云库中没有内容三方框架)&#xff01;&#xff01;&#xff01; 3. 4.5. //…

数仓建设(二)

1) 指标梳理 指标口径的不一致使得数据使用的成本极高&#xff0c;经常出现口径打架、反复核对数据的问题。在数据治理中&#xff0c;我们将需求梳理到的所有指标进行进一步梳理&#xff0c;明确其口径&#xff0c;如果存在两个指标名称相同&#xff0c;但口径不一致&#xff0…

项目管理之生命周期管理

项目生命周期管理矩阵是项目管理中一个重要的概念&#xff0c;它包括了项目从准备到收尾的各个阶段。项目生命周期管理矩阵以四个主要管理阶段为基础&#xff0c;分别为准备阶段、启动阶段、执行阶段和收尾阶段。这四个阶段在项目管理中有着明确的目标和职责&#xff0c;贯穿了…

【LeetCode】35. 搜索插入位置

1 问题 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: 输入: nums [1,3,5,6], target 5 输出: 2 示例…