Redis:8 种数据淘汰策略及近似LRU、LFU原理!

1、为什么Redis需要数据淘汰机制?

  众所周知,Redis作为知名内存型NOSQL,极大提升了程序访问数据的性能,高性能互联网应用里,几乎都能看到Redis的身影。为了提升系统性能,Redis也从单机版、主从版发展到集群版、读写分离集群版等等,业界也有诸多著名三方扩展库(如Codis、Twemproxy)。

  阿里云的企业版Redis(Tair)的性能增强型集群版更是“[豪]无人性”,内存容量高达4096 GB 内存,支持约61440000 QPS。Tair混合存储版更是使用内存和磁盘同时存储数据的集群版Redis实例,最高规格为1024 GB内存8192 GB磁盘(16节点)。【援引:https://help.aliyun.com/document_detail/26350.html  阿里云规格查询导航

  既然Redis这么牛,那我们就使劲把数据往里面存储吗?

  32G DDR4 内存条大约 900 元,1TB 的 SSD 硬盘大约 1000 元,价格实在悬殊。此外,即使数据量很大,但常用数据其实相对较少,全放内存性价比太低。“二八原则”在这里也是适用的。

  既然内存空间有限,为避免内存写满,就肯定需要进行内存数据淘汰了。

性价比;

内存空间有限;

2、Redis的8种数据淘汰策略

  redis.conf中可配置Redis的最大内存量 maxmemory,如果配置为0,在64位系统下则表示无最大内存限制,在32位系统下则表示最大内存限制为 3 GB。当实际使用内存 mem_used 达到设置的阀值 maxmemory 后,Redis将按照预设的淘汰策略进行数据淘汰。

示例图


除了在配置文件中修改配置,也可以使用 config 命令动态修改maxmemory。

  Redis的8种数据淘汰策略,Redis 4.0开始,共有8种数据淘汰机制。

淘汰策略名称策略含义


Redis的 8种数据淘汰机制



我们可以看到,除 noeviction 比较特殊外,allkeys 开头的将从所有数据中进行淘汰,volatile 开头的将从设置了过期时间的数据中进行淘汰。淘汰算法又核心分为 lru、random、ttl、lfu 几种。

让我们用一张图来概括:


Redis数据淘汰策略

在了解Redis近似LRU算法前,我们先来了解下原生的LRU算法。

3.1、LRU算法

  LRU(Least Recently Used)最近最少使用。优先淘汰最近未被使用的数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

  LRU底层结构是 hash 表 + 双向链表。hash 表用于保证查询操作的时间复杂度是O(1),双向链表用于保证节点插入、节点删除的时间复杂度是O(1)。

  为什么是 双向链表而不是单链表呢?单链表可以实现头部插入新节点、尾部删除旧节点的时间复杂度都是O(1),但是对于中间节点时间复杂度是O(n),因为对于中间节点c,我们需要将该节点c移动到头部,此时只知道他的下一个节点,要知道其上一个节点需要遍历整个链表,时间复杂度为O(n)。

LRU GET操作:如果节点存在,则将该节点移动到链表头部,并返回节点值;

LRU PUT操作:①节点不存在,则新增节点,并将该节点放到链表头部;②节点存在,则更新节点,并将该节点放到链表头部。

  LRU算法源码可参考Leetcode:https://www.programcreek.com/2013/03/leetcode-lru-cache-java。


LRU底层算法 伪代码


【LRU缓存】【hash+双向链表】结构示意图

3.2、近似LRU算法原理(approximated LRU algorithm)

Redis为什么不使用原生LRU算法?

原生LRU算法需要 双向链表 来管理数据,需要额外内存;

数据访问时涉及数据移动,有性能损耗;

Redis现有数据结构需要改造;

以上内容反过来就可以回答另一个问题:Redis近似LRU算法的优势?

  在Redis中,Redis的key的底层结构是 redisObject,redisObject 中 lru:LRU_BITS 字段用于记录该key最近一次被访问时的Redis时钟 server.lruclock(Redis在处理数据时,都会调用lookupKey方法用于更新该key的时钟)。

  不太理解Redis时钟的同学,可以将其先简单理解成时间戳(不影响我们理解近似LRU算法原理),server.lruclock 实际是一个 24bit 的整数,默认是 Unix 时间戳对 2^24 取模的结果,其精度是毫秒。


Redis的key底层结构

server.lruclock 的值是如何更新的呢?

  Redis启动时,initServer 方法中通过 aeCreateTimeEvent 将 serverCron 注册为时间事件(serverCron 是Redis中最核心的定时处理函数), serverCron 中 则会 触发 更新Redis时钟的方法 server.lruclock = getLRUClock() 。


Redis核心定时任务serverCron

当 mem_used > maxmemory 时,Redis通过 freeMemoryIfNeeded 方法完成数据淘汰。LRU策略淘汰核心逻辑在 evictionPoolPopulate(淘汰数据集合填充) 方法。

Redis 近似LRU 淘汰策略逻辑:

首次淘汰:随机抽样选出【最多N个数据】放入【待淘汰数据池 evictionPoolEntry】;

数据量N:由 redis.conf 配置的 maxmemory-samples 决定,默认值是5,配置为10将非常接近真实LRU效果,但是更消耗CPU;

samples:n.样本;v.抽样;

再次淘汰:随机抽样选出【最多N个数据】,只要数据比【待淘汰数据池 evictionPoolEntry】中的【任意一条】数据的 lru 小,则将该数据填充至 【待淘汰数据池】;

evictionPoolEntry 的容容量是 EVPOOL_SIZE = 16;

详见 源码 中 evictionPoolPopulate 方法的注释;

执行淘汰:挑选【待淘汰数据池】中 lru 最小的一条数据进行淘汰;

Redis为了避免长时间或一直找不到足够的数据填充【待淘汰数据池】,代码里(dictGetSomeKeys 方法)强制写死了单次寻找数据的最大次数是 [maxsteps = count*10; ],count 的值其实就是 maxmemory-samples。从这里我们也可以获得另一个重要信息:单次获取的数据可能达不到 maxmemory-samples 个。此外,如果Redis数据量(所有数据 或 有过期时间 的数据)本身就比 maxmemory-samples 小,那么 count 值等于 Redis 中数据量个数。




LFU:Least Frequently Used,使用频率最少的(最不经常使用的)

优先淘汰最近使用的少的数据,其核心思想是“如果一个数据在最近一段时间很少被访问到,那么将来被访问的可能性也很小”。


4.1、LFU与LRU的区别

  如果一条数据仅仅是突然被访问(有可能后续将不再访问),在 LRU 算法下,此数据将被定义为热数据,最晚被淘汰。但实际生产环境下,我们很多时候需要计算的是一段时间下key的访问频率,淘汰此时间段内的冷数据。

  LFU 算法相比 LRU,在某些情况下可以提升 数据命中率,使用频率更多的数据将更容易被保留。

对比项近似LRU算法LFU算法

最先过期的数据最近未被访问的最近一段时间访问的最少的

适用场景数据被连续访问场景数据在一段时间内被连续访问

缺点新增key将占据缓存历史访问次数超大的key淘汰速度取决于lfu-decay-time

4.2、LFU算法原理

  LFU 使用 Morris counter 概率计数器,仅使用几比特就可以维护 访问频率,Morris算法利用随机算法来增加计数,在 Morris 算法中,计数不是真实的计数,它代表的是实际计数的量级。

LFU数据淘汰策略下,redisObject 的 lru:LRU_BITS 字段(24位)将分为2部分存储:

Ldt:last decrement time,16位,精度分钟,存储上一次 LOG_C 更新的时间。

LOG_C:logarithmic counter,8位,最大255,存储key被访问频率。

注意:

LOG_C 存储的是访问频率,不是访问次数;

LOG_C访问频率随时间衰减;

为什么 LOG_C 要随时间衰减?比如在秒杀场景下,热key被访问次数很大,如果不随时间衰减,此部分key将一直存放于内存中。

新对象 的 LOG_C 值 为 LFU_INIT_VAL = 5,避免刚被创建即被淘汰。


LFU 的核心配置:

lfu-log-factor:counter 增长对数因子,调整概率计数器 counter 的增长速度,lfu-log-factor值越大 counter 增长越慢;lfu-log-factor 默认10。

lfu-decay-time:衰变时间周期,调整概率计数器的减少速度,单位分钟,默认1。

N 分钟未访问,counter 将衰减 N/lfu-decay-time,直至衰减到0;

若配置为0:表示每次访问都将衰减 counter;

  counter 的区间是0-255, 其增长与访问次数呈现对数增长的趋势,随着访问次数越来越大,counter 增长的越来越慢。Redis 官网提供的在 不同 factor 下,不同命中率 时 counter 的值示例如下:

不同于 LRU 算法,LFU 算法下 Ldt 的值不是在key被访问时更新,而是在 内存达到 maxmemory 时,触发淘汰策略时更新。

Redis LFU 淘汰策略逻辑:

随机抽样选出N个数据放入【待淘汰数据池 evictionPoolEntry】;

再次淘汰:随机抽样选出【最多N个数据】,更新 Ldt 和 counter 的值,只要 counter 比【待淘汰数据池 evictionPoolEntry】中的【任意一条】数据的 counter 小,则将该数据填充至 【待淘汰数据池】;

evictionPoolEntry 的容容量是 EVPOOL_SIZE = 16;

执行淘汰:挑选【待淘汰数据池】中 counter 最小的一条数据进行淘汰;

  在讲解近似LRU算法时,提及“Redis在处理数据时,都会调用lookupKey方法用于更新该key的时钟”,回过头来看,更为严谨的说法是“Redis在处理数据时,都会调用lookupKey方法,如果内存淘汰策略是 LFU,则会调用 ‘updateLFU()’ 方法计算 LFU 模式下的 lru 并更新,否则将更新该key的时钟 ‘val->lru = LRU_CLOCK()’”.


详细说明可在源码 evict.c 中 搜索 “LFU (Least Frequently Used) implementation”

LFU 的核心配置:

lfu-log-factor:counter 增长对数因子,调整概率计数器 counter 的增长速度,lfu-log-factor值越大 counter 增长越慢;lfu-log-factor 默认10。

lfu-decay-time:衰变时间周期,调整概率计数器的减少速度,单位分钟,默认1。

N 分钟未访问,counter 将衰减 N/lfu-decay-time,直至衰减到0;

若配置为0:表示每次访问都将衰减 counter;

  counter 的区间是0-255, 其增长与访问次数呈现对数增长的趋势,随着访问次数越来越大,counter 增长的越来越慢。Redis 官网提供的在 不同 factor 下,不同命中率 时 counter 的值示例如下:


不同于 LRU 算法,LFU 算法下 Ldt 的值不是在key被访问时更新,而是在 内存达到 maxmemory 时,触发淘汰策略时更新。

Redis LFU 淘汰策略逻辑:

随机抽样选出N个数据放入【待淘汰数据池 evictionPoolEntry】;

再次淘汰:随机抽样选出【最多N个数据】,更新 Ldt 和 counter 的值,只要 counter 比【待淘汰数据池 evictionPoolEntry】中的【任意一条】数据的 counter 小,则将该数据填充至 【待淘汰数据池】;

evictionPoolEntry 的容容量是 EVPOOL_SIZE = 16;

执行淘汰:挑选【待淘汰数据池】中 counter 最小的一条数据进行淘汰;

  在讲解近似LRU算法时,提及“Redis在处理数据时,都会调用lookupKey方法用于更新该key的时钟”,回过头来看,更为严谨的说法是“Redis在处理数据时,都会调用lookupKey方法,如果内存淘汰策略是 LFU,则会调用 ‘updateLFU()’ 方法计算 LFU 模式下的 lru 并更新,否则将更新该key的时钟 ‘val->lru = LRU_CLOCK()’”.


示例图

5.1、为什么Redis要使用自己的时钟?

获取系统时间戳将调用系统底层提供的方法;

单线程的Redis对性能要求极高,从缓存中获取时间戳将极大提升性能。

5.2、如何发现热点key?

  object freq key 命令支持 获取 key 的 counter,所以我们可以通过 scan 遍历所有key,再通过 object freq 获取counter。

  需要注意的是,执行 object freq 的前提是 数据淘汰策略是 LFU。


示例图

Redis 4.0.3版本也提供了redis-cli的热点key功能,执行"./redis-cli --hotkeys"即可获取热点key。需要注意的是,hotkeys 本质上是 scan + object freq,所以,如果数据量特别大的情况下,可能耗时较长。


示例图
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容