小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
1、简介
LRU有一个明显的缺点,它无法正确的表示一个Key的热度,如果一个key从未被访问过,仅仅发生内存淘汰的前一会儿被用户访问了一下,在LRU算法中这会被认为是一个热key。
例如如下图,keyA与keyB同时被set到Redis中,在内存淘汰发生之前,keyA被频繁的访问,而keyB只被访问了一次,但是这次访问的时间比keyA的任意一次访问时间都更接近内存淘汰触发的时间,如果keyA与keyB均被Redis选中进行淘汰,keyA将被优先淘汰。我想大家都不太希望keyA被淘汰吧,那么有没有更好的的内存淘汰机制呢?当然有,那就是LFU。\
LFU(Least Frequently Used)是Redis 4.0 引入的淘汰算法,它通过key的访问频率比较来淘汰key,重点突出的是Frequently Used。
LRU与LFU的区别:
- LRU -> Recently Used,根据最近一次访问的时间比较
- LFU -> Frequently Used,根据key的访问频率比较
Redis4.0之后为maxmemory_policy淘汰策略添加了两个LFU模式(LRU请看我上一篇文章) :
- volatile-lfu:对有过期时间的key采用LFU淘汰算法
- allkeys-lfu:对全部key采用LFU淘汰算法
2、实现方式
Redis分配一个字符串的最小空间占用是19字节,16字节(对象头)+3字节(SDS基本字段)。Redis的内存淘汰算法LRU/LFU均依靠其中的对象头中的lru来实现。
Redis对象头的内存结构:
1 | arduino复制代码typedef struct redisObject { |
Redis对象头中的lru字段,在LRU模式下和LFU模式下使用方式并不相同。
2.1 LRU实现方式
在LRU模式,lru字段存储的是key被访问时Redis的时钟server.lrulock(Redis为了保证核心单线程服务性能,缓存了Unix操作系统时钟,默认每毫秒更新一次,缓存的值是Unix时间戳取模2^24)。当key被访问的时候,Redis会更新这个key的对象头中lru字段的值。
因此在LRU模式下,Redis可以根据对象头中的lru字段记录的值,来比较最后一次key的访问时间。
用Java代码演示一个简单的Redis-LRU算法:
- Redis对象头
1 | kotlin复制代码package com.lizba.redis.lru; |
- Redis LRU实现代码
1 | kotlin复制代码package com.lizba.redis.lru; |
- 测试代码
1 | java复制代码package com.lizba.redis.lru; |
- 测试结果
2.2 LFU实现方式
在LFU模式下,Redis对象头的24bit lru字段被分成两段来存储,高16bit存储ldt(Last Decrement Time),低8bit存储logc(Logistic Counter)。\
2.2.1 ldt(Last Decrement Time)
高16bit用来记录最近一次计数器降低的时间,由于只有8bit,存储的是Unix分钟时间戳取模2^16,16bit能表示的最大值为65535(65535/24/60≈45.5),大概45.5天会折返(折返指的是取模后的值重新从0开始)。
Last Decrement Time计算的算法源码:
1 | arduino复制代码/* Return the current time in minutes, just taking the least significant |
2.2.2 logc(Logistic Counter)
低8位用来记录访问频次,8bit能表示的最大值为255,logc肯定无法记录真实的Rediskey的访问次数,其实从名字可以看出存储的是访问次数的对数值,每个新加入的key的logc初始值为5(LFU_INITI_VAL),这样可以保证新加入的值不会被首先选中淘汰;logc每次key被访问时都会更新;此外,logc会随着时间衰减。
2.2.3 logc 算法调整
redis.conf 提供了两个配置项,用于调整LFU的算法从而控制Logistic Counter的增长和衰减。\
- lfu-log-factor 用于调整Logistic Counter的增长速度,lfu-log-factor值越大,Logistic Counter增长越慢。
Redis Logistic Counter增长的源代码:
1 | scss复制代码/* Logarithmically increment a counter. The greater is the current counter value |
如下是官网提供lfu-log-factor在不同值下,key随着访问次数的增加的Logistic Counter变化情况的数据:\
- lfu-decay-time 用于调整Logistic Counter的衰减速度,它是一个以分钟为单位的数值,默认值为1,;lfu-decay-time值越大,衰减越慢。
Redis Logistic Counter衰减的源代码:
1 | vbnet复制代码/* If the object decrement time is reached decrement the LFU counter but |
2.2.4 LFU 优化
LFU 与 LRU 有一个共同点,当内存达到max_memory时,选择key是随机抓取的,因此Redis为了使这种随机性更加准确,设计了一个淘汰池,这个淘汰池对于LFU和LRU算的都适应,只是淘汰池的排序算法有区别而已。
Redis 3.0就对这一块进行了优化(来自redis.io):\
3、LFU使用
3.1 配置文件开启LFU淘汰算法
修改redis.conf配置文件,设置maxmemory-policy volatile-lfu/allkeys-lfu\
重启Redis,连接客户端通过info指令查看maxmemory_policy的配置信息\
通过object freq key 获取对象的LFU的Logistic Counter值\
本文转载自: 掘金