目录
前言
Redis 使用内存来保存数据,而物理内存是有限的,如果不对 Redis 使用内存做出限制,当内存不够用时,操作系统将通过 swap 分区让数据在内存和硬盘之间来回置换,这会严重影响 Redis 性能,因此我们一般要配置 Redis 可以使用的最大内存(maxmemory)。
maxmemory-policy 淘汰策略
当使用的内存达到设置的最大使用内存时,将会触发内存淘汰策略, Redis 提供以下八种策略:
- noeviction:不淘汰任何数据,内存溢出时直接返回 OOM 错误信息;
- volatile-random:随机移除设置了过期时间的 Key;
- volatile-ttl:针对带有过期时间的 Key,移除离过期时间最近的 Key(越早过期越先移除);
- volatile-lru:针对带有过期时间的 Key,使用 LRU(Least Recently Used) 算法筛选移除;、
- volatile-lfu:针对带有过期时间的 Key,使用 LFU(Least Frequently Used) 算法筛选移除;
- allkeys-random:针对所有 Key 进行随机移除;
- allkeys-lru:针对所有 Key 使用 LRU(Least Recently Used) 算法移除;
- allkeys-lfu:针对所有 Key 使用 LFU(Least Frequently Used) 算法移除。
Redis 中的 LRU 算法
传统 LRU 算法
传统的 LRU 算法 把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端(最近最常使用) 和 LRU 端(最近最不常用)。
每次访问数据时,把该数据移动到 MRU 端,当缓存没有空间需要删除数据的时候,从 LRU 端选择数据删除,这样就可以把最近访问的数据留在缓存中,删除离当前时间最久没有被访问的数据。
当然,因为传统的算法需要使用链表来保存所有的数据,同时存在大量的数据移动,因此,Redis 并没有使用这种做法。
Redis LRU 算法
Redis 在每个数据对象 RedisObject 中存放 lru 字段,表示该数据最近一次访问的时间戳,以后做数据淘汰时用该字段作为比较依据。
当执行数据淘汰时, 首次 执行将按以下步骤选择数据:
1、随机 选出 N (maxmemory-samples)个数据,把它们作为一个候选集合;
2、比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据淘汰出去;
以后 再次 进行数据淘汰时,将以 第一次淘汰时创建的候选集合中最小的 lru 值 minLruInSet 为基准,挑选 lru 字段值 小于 minLruInSet 的数据并放入到集合中,当候选数据集中的数据个数再次达到 maxmemory-samples 时,Redis 就把候选集合中 lru 字段值最小的数据淘汰出去。
通过维护这个 lru 小值集合可以减小发生数据淘汰时对 redis 产生的性能影响,因为它不需要使用链表来保存所有的数据,也不存在数据的移动。
官网 表明在样本数 maxmemory-samples = 10 的情况下,Redis3.0 很接近真正的 LRU 实现。
Redis 中的 LFU 算法
策略
LRU 算法存在一个缺陷,因为它 只关心数据的访问时间,在发生 扫描式单次查询操作时,所有的数据都会被访问一次,这样可能导致很多热数据反而被排到了 LRU 的末端而被淘汰。
针对这个问题,LFU 缓存策略 在 LRU 策略基础上,为每个数据 增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时:
1、首先根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出去;
2、如果两个数据的访问次数相同,再比较两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出去。
因此,LFU 算法会将访问更频繁的数据保留,而优先淘汰访问次数最少的数据,在访问次数相当的情况下再选择访问时间最久远的数据淘汰。
实现
LFU 在实现上是把原来 24bit 大小的 lru 字段拆成两部分:
- ldt 值:lru 字段的前 16bit,表示数据的访问时间戳;
- counter 值:lru 字段的后 8bit,表示数据的访问次数。
8 bit 记录访问次数,最多只能到 255。如果访问一次就加 1 的话,可能大部分数据都会很快达到这个值,那这个值将失去意义。因此,Redis 使用了一个增长更慢的计数规则:
double r = (double)rand()/RAND_MAX;
double p = 1.0/(baseval*server.lfu_log_factor+1);
if (r < p)
counter++;
- r 是一个取值范围在(0,1)间的随机数;
- baseval 是计数器的当前值;
- lfu_log_factor 为计数器增长因子。
每当数据被访问时,首先,用 baseval 乘以 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;
然后,把这个 p 值和 r 值作比较, p 值大于 r 值时计数器加 1。
官网也提供了 lfu_log_factor 不同取值时的计数器增长情况:
+--------+------------+------------+------------+------------+------------+
| factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits |
+--------+------------+------------+------------+------------+------------+
| 0 | 104 | 255 | 255 | 255 | 255 |
+--------+------------+------------+------------+------------+------------+
| 1 | 18 | 49 | 255 | 255 | 255 |
+--------+------------+------------+------------+------------+------------+
| 10 | 10 | 18 | 142 | 255 | 255 |
+--------+------------+------------+------------+------------+------------+
| 100 | 8 | 11 | 49 | 143 | 255 |
+--------+------------+------------+------------+------------+------------+
此外,Redis 还设计了一个 counter 值的衰减机制,这主要是为了能够淘汰那些可能短时间内被频繁访问,但是之后不再需要的数据。
unsigned long LFUDecrAndReturn(robj *o) {
// 获取lru的高16位,也就是ldt
unsigned long ldt = o->lru >> 8;
// 获取lru的低8位,也就是logc
unsigned long counter = o->lru & 255;
// 根据配置的lfu-decay-time计算Logistic Counter需要衰减的值
unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
if (num_periods)
counter = (num_periods > counter) ? 0 : counter - num_periods;
return counter;
}
// 计算距离上次访问的间隔时间
unsigned long LFUTimeElapsed(unsigned long ldt) {
// 取当前时间戳(单位:分钟)
unsigned long now = LFUGetTimeInMinutes();
// 计算时间差
if (now >= ldt)
return now-ldt;
return 65535-ldt+now;
}
// 获取当前时间戳,以分钟为单位,取低 8 位
unsigned long LFUGetTimeInMinutes(void) {
return (server.unixtime/60) & 65535;
}
LFU 策略使用衰减因子配置项 lfu_decay_time 来控制访问次数的衰减。
因为数据对象只有前 16 位用来保存时间戳,所以只能比较到分钟级别,在计算的时候:
首先把当前时间换成以分为单位,然后计算 其与 ldt 的差值,再把这个差值除以 lfu_decay_time ,所得的结果就是数据 counter 要衰减的值。
相关配置
- maxmemory :最大使用内存
- maxmemory-policy :淘汰策略
- maxmemory-samples: LRU 算法中采样集合大小
- lfu_log_factor:计数器增长因子
- lfu-decay-time:LFU 算法计数器衰减因子
以上参数可以通过配置文件 redis.conf 配置,也可以通过命令行 config set [key] [value] 设置。