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

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

/* Return the current time in minutes, just taking the least significant

* 16 bits. The returned time is suitable to be stored as LDT (last decrement

* time) for the LFU implementation. */

// server.unixtime是Redis缓存的Unix时间戳

// 可以看出使用的Unix的分钟时间戳,取模2^16

unsigned long LFUGetTimeInMinutes(void) {

return (server.unixtime/60) & 65535;

}

/* Given an object last access time, compute the minimum number of minutes

* that elapsed since the last access. Handle overflow (ldt greater than

* the current 16 bits minutes time) considering the time as wrapping

* exactly once. */

unsigned long LFUTimeElapsed(unsigned long ldt) {

// 获取系统当前的LFU time

unsigned long now = LFUGetTimeInMinutes();

// 如果now >= ldt 直接取差值

if (now >= ldt) return now-ldt;

// 如果now < ldt 增加上65535

// 注意Redis 认为折返就只有一次折返,多次折返也是一次,我思考了很久感觉这个应该是可以接受的,本身Redis的淘汰算法就带有随机性

return 65535-ldt+now;

}

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的增长和衰减。

image.png

  • lfu-log-factor 用于调整Logistic Counter的增长速度,lfu-log-factor值越大,Logistic Counter增长越慢。

Redis Logistic Counter增长的源代码:

/* Logarithmically increment a counter. The greater is the current counter value

* the less likely is that it gets really implemented. Saturate it at 255. */

uint8_t LFULogIncr(uint8_t counter) {

// Logistic Counter最大值为255

小结

有了这么多优秀的开发工具,可以做出更高质量的Android应用。

当然了,“打铁还需自身硬”,想要写出优秀的代码,最重要的一点还是自身的技术水平,不然用再好的工具也不能发挥出它的全部实力。

在这里我也分享一份大佬自己收录整理的Android学习PDF+架构视频+面试文档+源码笔记,还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料这些都是我闲暇还会反复翻阅的精品资料。在脑图中,每个知识点专题都配有相对应的实战项目,可以有效的帮助大家掌握知识点。

总之也是在这里帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 12
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LFU(Least Frequently Used算法Redis中一种用于淘汰策略的算法。它根据键的访问频率来决定淘汰哪些键,频率越低的键越容易被淘汰。 在Redis 4.0之后,LFU算法有两种模式可供选择: 1. volatile-lfu:对有过期时间的键采用LFU淘汰算法。这意味着只有那些设置了过期时间的键才会参与LFU算法的淘汰过程。 2. allkeys-lfu:对所有键采用LFU淘汰算法。这意味着所有的键都会参与LFU算法的淘汰过程。 LFU算法的实现原理是通过维护一个访问计数器来记录每个键的访问频率。每次访问一个键时,该键的访问计数器会增加。当需要淘汰键时,LFU算法会选择访问计数器最低的键进行淘汰。 以下是一个演示LFU算法的例子: ```shell # 设置maxmemory和maxmemory-policy参数 config set maxmemory 10mb config set maxmemory-policy allkeys-lfu # 添加一些键值对 set key1 value1 set key2 value2 set key3 value3 # 访问键key1和key2 get key1 get key2 # 查看键的访问计数器 debug object key1 debug object key2 debug object key3 # 添加新的键值对,触发淘汰 set key4 value4 # 查看被淘汰的键 debug object key3 ``` 在上述例子中,我们首先设置了maxmemory参数为10mb,并将maxmemory-policy参数设置为allkeys-lfu。然后我们添加了三个键值对,并访问了键key1和key2。通过查看键的访问计数器,我们可以看到key1和key2的访问计数器增加了。最后,我们添加了一个新的键值对,触发了LFU算法的淘汰过程,并通过查看被淘汰的键,可以看到key3被淘汰了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值