Redis精通系列——LFU算法详述(Least Frequently Used - 最不经常使用)(1)

2.1 LRU实现方式

2.2 LFU实现方式

3、LFU使用

3.1 配置文件开启LFU淘汰算法


1、简介


LRU有一个明显的缺点,它无法正确的表示一个Key的热度,如果一个key从未被访问过,仅仅发生内存淘汰的前一会儿被用户访问了一下,在LRU算法中这会被认为是一个热key。

例如如下图,keyA与keyB同时被set到Redis中,在内存淘汰发生之前,keyA被频繁的访问,而keyB只被访问了一次,但是这次访问的时间比keyA的任意一次访问时间都更接近内存淘汰触发的时间,如果keyA与keyB均被Redis选中进行淘汰,keyA将被优先淘汰。我想大家都不太希望keyA被淘汰吧,那么有没有更好的的内存淘汰机制呢?当然有,那就是LFU。

LRU存在的问题.png

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对象头的内存结构:

typedef struct redisObject {

unsigned type:4;        // 4 bits 对象的类型(zset、set、hash等)

unsigned encoding:4;    // 4 bits 对象的存储方式(ziplist、intset等)

unsigned lru:24;        // 24bits 记录对象的访问信息

int refcount;            // 4 bytes 引用计数

void *ptr;                // 8 bytes (64位操作系统),指向对象具体的存储地址/对象body

}

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对象头

package com.lizba.redis.lru;

/**

*      Redis对象头

* @Author: Liziba

* @Date: 2021/9/22 22:40

*/

public class RedisHead {

/** 时间 */

private Long lru;

/** 具体数据 */

private Object body;

public RedisHead setLru(Long lru) {

this.lru = lru;

return this;

}

public RedisHead setBody(Object body) {

this.body = body;

return this;

}

public Long getLru() {

return lru;

}

public Object getBody() {

return body;

}

}

  • Redis LRU实现代码

package com.lizba.redis.lru;

import java.util.Comparator;

import java.util.List;

import java.util.concurrent.ConcurrentHashMap;

import java.util.stream.Collectors;

/**

* Redis中LRU算法的实现demo

* @Author: Liziba

* @Date: 2021/9/22 22:36

*/

public class RedisLruDemo {

/**

* 缓存容器

*/

private ConcurrentHashMap<String, RedisHead> cache;

/**

* 初始化大小

*/

private int initialCapacity;

public RedisLruDemo(int initialCapacity) {

this.initialCapacity = initialCapacity;

this.cache = new ConcurrentHashMap<>(initialCapacity);

;

}

/**

* 设置key/value 设置的时候更新LRU

* @param key

* @param body

*/

public void set(String key, Object body) {

// 触发LRU淘汰

synchronized (RedisLruDemo.class) {

if (!cache.containsKey(key) && cache.size() >= initialCapacity) {

this.flushLruKey();

}

}

RedisHead obj = this.getRedisHead().setBody(body).setLru(System.currentTimeMillis());

cache.put(key, obj);

}

/**

* 获取key,存在则更新LRU

* @param key

* @return

*/

public Object get(String key) {

RedisHead result = null;

if (cache.containsKey(key)) {

result = cache.get(key);

result.setLru(System.currentTimeMillis());

}

return result;

}

/**

* 清除LRU key

*/

private void flushLruKey() {

List sortData = cache.keySet()

.stream()

.sorted(Comparator.comparing(key -> cache.get(key).getLru()))

.collect(Collectors.toList());

String removeKey = sortData.get(0);

System.out.println( "淘汰 -> " + "lru : " + cache.get(removeKey).getLru() + " body : " + cache.get(removeKey).getBody());

cache.remove(removeKey);

if (cache.size() >= initialCapacity) {

this.flushLruKey();

}

return;

}

/**

*  获取所有数据测试用

* @return

*/

public List getAll() {

return cache.keySet().stream().map(key -> cache.get(key)).collect(Collectors.toList());

}

private RedisHead getRedisHead() {

return new RedisHead();

}

}

  • 测试代码

package com.lizba.redis.lru;

import java.util.Random;

import java.util.concurrent.TimeUnit;

/**

*      测试LRU

* @Author: Liziba

* @Date: 2021/9/22 22:51

*/

public class TestRedisLruDemo {

public static void main(String[] args) throws InterruptedException {

RedisLruDemo demo = new RedisLruDemo(10);

// 先加入10个key,此时cache达到容量,下次加入会淘汰key

for (int i = 0; i < 10; i++) {

demo.set(i + “”, i);

}

// 随机访问前十个key,这样可以保证下次加入时随机淘汰

for (int i = 0; i < 20; i++) {

int nextInt = new Random().nextInt(10);

TimeUnit.SECONDS.sleep(1);

demo.get(nextInt + “”);

}

// 再次添加5个key,此时每次添加都会触发淘汰

for (int i = 10; i < 15; i++) {

demo.set(i + “”, i);

}

System.out.println(“-------------------------------------------”);

demo.getAll().forEach( redisHead -> System.out.println("剩余 -> " + "lru : " + redisHead.getLru() + " body : " + redisHead.getBody()));

}

}

  • 测试结果

image.png

2.2 LFU实现方式

在LFU模式下,Redis对象头的24bit lru字段被分成两段来存储,高16bit存储ldt(Last Decrement Time),低8bit存储logc(Logistic Counter)。

lru_24 bit.png

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计算的算法源码:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-ThkCSFUf-1713406628166)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值