前言
在前面的文章《Redis 限制内存大小参数 maxmemory》中,我们讲到了 Redis 的 LRU 模式,它可以有效地控制 Redis 占用内存的大小,将冷数据从内存中淘汰出去。Antirez 在 Redis4.0 里引人了一个新的淘汰策略 一 LFU模式。
LFU
LFU 的全称是 LeastFrequently Used,表示接最近的访问频率进行淘汰,它比 LRU 更加精准地表示了一个 key 被访问的热度。 如果一个 key 长时间不被访问,只是刚刚偶然被用户访问了一下,那么在 LRU 算法下,它是不容易被淘汰的,因为 LRU 算法认为这个 key 是很“热”的。而 LFU 算法需要追踪最近一段时间的访问频率,如果某个 key 只是偶然被访问次是不足以变得很“热”的,它需要在近一段时间内被访问很多次才有机会被 LFU 算法认为很“热”。
Redis 对象的热度
Redis 的所有对象头结构中都有一个 24bit 的字段,这个字段用来记录对象的热度。 Redis的对象头结构:
typedef struct redisObject {
unsigned type:4; //对象类型如zset、set、hash等
unsigned encoding:4; //对象编码如ziplist、intset、skiplist等
unsigned lru:24; //对象的热度
int refcount; //引用计数
void *ptr; //对象的body
} robj;
LRU
在 LRU 模式下,lru 字段存储的是 Redis 时钟 server.lruclock。Redis 时钟是一个 24bit 的整数,默认是 Unix 时间戳对 2 的 24 次方取模的结果,大约 97 天清零一次。当某个 key 被访问一次,它的对象头结构的 lru 字段值就会被更新为 server.lruclock。 默认 Redis 时钟值每毫秒更新一次,在定时任务 serverCron 里主动设置。Redis 的很多定时任务都是在 serverCron 里面完成的,比如大型 hash 表的渐进式迁移,过期 key 的主动淘汰,触发 bgsave、bgaofrewrite 等。 如果 server.lruclock 没有折返(2 的 24 次方取模),它就是一直递增的,这意昧着对象的 lru 字段不会超过 server.lruclock 的值。 如果超过了,说明 server.lruclock 折返了。 通过这个逻辑就可以精准计算出对象多长时间没有被访问一即“对象的空闲时间”。 有了对象的空闲时间,就可以相互之间进行比较谁新谁旧,随机 LRU 算法靠的就是比较对象的空闲时间来决定谁该被淘汰了。
LFU
在 LFU 模式下,lru 字段 24bit 用来存储两个值,分别是 ldt(last decrement time) 和 lgc(logistic counter)。 logc是 8 个 bit,用来存储访问频次,因为 8 个 bit 能表示的最大整数值为 255,存储频次肯定远远不够,所以这 8 个 bit 存储的是频次的对数值,并且这个值还会随时间衰减,如果它的值比较小,那么就很容易被回收。为了确保新创建的对象不被回收,新对象的这 8 个 bit 会被初始化为一个大于零的值 LFU_INIT_VAL(默认是=5)。 ldt 是16 个 bit,用来存储上一次 logc 的更新时间。因为只有 16 个 bit,所以精度不可能很高。它取的是分钟时间戳对2的16次方进行取模,大约每隔 45 天就会折返。 图 5-26 呈现了折返前后空闲时间的不同计算规则。同 LRU 模式一样,我们也可以使用这个逻辑计算出对象的空闲时间,只不过精度是分钟级别的。图中的server.unixtime 是当前 Redis 记录的系统时间戳,和 server.lruclock 一样,它也是每毫秒更新一次。 ldt 的值和 LRU 模式的 lru 字段不一样的地方是,ldt 不是在对象被访问时更新的,而是在 Redis 的淘汰逻辑进行时进行更新,淘汰逻辑只会在内存达到 maxmemory 的设置时才会触发,在每一个指令的执行之前都会触发。每次淘汰都是采用随机策略,随机挑选若干个 key,更新这个 key 的“热度”,淘汰掉“热度”最低的 key。因为 Redis 采用的是随机算法,如果 key 比较多的话,那么 ldt 更新得可能会比较慢。不过既然它是分钟级别的精度,也没有必要更新得过于频繁。 ldt 更新的同时也会一同衰减 logc 的值。衰减也有特定的算法,它将现有的 logc 值减去对象的空闲时间(分钟数)再除以一个衰减系数 lfu_decay_ time (默认为1)。如果 lfu_decay_ time 的值大于 1,那么就会衰减得比较慢,如果它等于零,那就表示不衰减,lfu-decay-time 可以进行设置。 logc 的更新和LRU 模式的 lru 字段一样,都会在 key 每次被访问的时候更新,只不过它的更新不是简单的“+1”,而是采用概率法进行递增,因为 logc 存储的是访问计数的对数值,不能直接“+1”。
为什么 Redis 要缓存系统时间戳
我们平时使用系统时间戳时,常常是不假思索地使用 System.currentTimeInMillis 或者 time.time() 来获取系统的毫秒时间戳。Redis 不能这样,因为每一次获取系统时间戳都是一次系统调用,系统调用相对来说是比较费时间的,作为单线程的 Redis 承受不起,所以它需要对时间进行缓存,获取时间都是从缓存中直接拿。
Redis为什么在获取 lruclock 时使用原子操作
我们知道 Redis 是单线程的,那为什么 lruclock 要使用原子操作 atomicGet 来获取呢? 因为 Redis 实际上并不是单线程,它背后还有几个异步线程也在默默工作,这几个线程也要访问 Redis 时钟,所以 lruclock 字段是需要支持多线程读写的。使用 atomic 读写能保证多线程 lruclock 数据的一致性。
如何打开 LFU 模式
Redis 4.0 给淘汰策略配置参数 maxmemory-policy 增加了 2 个选项,分别是 volatile-lfu 和 allkeys-lfu,分别是对带过期时间的 key 执行 LFU 淘汰算法以及对所有的 key 执行 LFU 淘汰算法。打开了选项之后,就可以使用 object freq 指令获取对象的 LFU 计数值了。
|