一、前述

Redis 一直在 Nosql 中占据着很重要的地位,阅读官方文档以及 github 源代码,是一种非常好的能够帮助提升的方式,本系列博文主要参考官网翻译、Github 源代码以及部分自己的理解而来,如有不准确或者遗漏,感谢及时提出改正。
官方文档:https://redis.io/topics/lru-cache 众所周知,Redis 是一个基于内存的数据库,因此单线程的读写避免了频繁的 cpu 上下文切换开销,同时 redis 也可以用作发布订阅、延时队列、分布式锁等等很多场景,来实现我们的业务需求,今天分享一下 redis 里的 lru

二、lru 的一些故事

lru 全称是 Least Recently Used,最久未使用,lru 算法在很多场景都有使用,例如 cpu 调度,一般常见的 cpu 调度算法有 FIFO、LRU、时间片轮转等方法,如果使用的是 LRU ,那么 cpu 就是按照最久未使用的方法来唤醒线程

三、redis 的 lru

1、Redis 的 lru 指的是内存回收清理相关的最久未使用,但是其实从Redis 4.0版本开始,引入了一个新的策略 LFU(Least Frequently Used,最不常用的)清除策略

2、Redis 的 maxmemory 配置指令可以为 Redis 的数据集指定一定数值的内存。可以通过在 redis.conf 配置文件配置指令,或者通过 redis-cli 连接 redis ,使用 config set 命令在 redis 运行时来设置。例如在 redis.conf 中配置;
maxmemory 100mb
两种配置方式的区别:

1)在 redis.conf 文件添加配置以后,需要重启 redis 生效
2)在 redis-cli 设置的时候,可以直接设置

3、在 64 位系统中,将 maxmemory 设置为零的时候,默认不会有内存限制,但是在 32 位系统使用 3GB 的隐式内存限制,如果达到了指定的内存大小时,可以在不同的策略中进行选择。Redis 只会返回可能导致更多内存被使用的命令的错误,或者它可以删除一些旧数据,以便在每次添加新数据时返回到指定的限制。

4、当达到最大内存限制的时候,可以使用 maxmemory-policy 配置指令配置 Redis 明确的策略,有如下策略:

noeviction:当达到内存限制时 redis 服务端直接返回错误,客户端试图执行可能导致更多内存被使用的命令(大多数写命令,但是DEL和其他一些异常)。
    allkeys-lru:尝试删除最久未使用的 key,以便给新数据的加如腾出空间
    volatile-lru:在那些设置了过期时间的 key 集合里删除最久未使用的 key
    allkeys-random:在所有的 key 里随机淘汰
    volatile-random:在设置了过期时间的 key 集合里,随机淘汰
    volatile-ttl:在设置了过期时间的 key 集合里,首先尝试淘汰最短存活时间 (TTL) 的 key

这里值得注意的是,如果没有符合前提条件的 key 被淘汰,那么 volatile-lru、volatile-random 、volatile-ttl 相当于 noeviction

5、根据应用程序的访问规则选择正确的 key 淘汰策略是非常重要的,但是开发者可以在应用程序运行时重新配置策略,并使用 Redis 的 info 指令输出监视缓存丢失和命中的次数,以此做对应的优化,一般遵循如下规则:
1)当你预计你的应用程序里的绝大部分请求是满足幂律的,就使用 allkeys-lru,也就是说预计元素的子集被访问的频率将远远高于其他的元素。当你不确定的时候,这种策略是一个不错的选择
2) 如果应用程序的访问请求是轮询的,内存里的所有 key 都是被连续扫描的,或者预计的请求分布式均匀的(也就是说,每个可能被访问到的元素有着相同的概率被访问),可以使用 allkeys-random
3)如果想告诉 redis ,在创建缓存对象的时候,通过使用不同的 TTL 值来确定哪些是即将过期的对象,使用 volatile-ttl
volatile-lruvolatile-random 策略主要适用于同时使用一个实例进行缓存和持久化键的情况。但是一般来说运行两个 Redis 实例来解决这样的问题通常更好。
同样值得注意的是,将过期值设置为键值会消耗内存,所以使用 allkeys-lru 这样的策略会提高内存效率,因为在内存压力下不需要为要清除的键设置过期值。因为在淘汰过期的 key 的时候,是需要消耗内存的。

三、内存淘汰是如何进行的

上面介绍完了六种淘汰策略以后,接下来我们看看 redis 的内存淘汰是怎样的一个过程:

(1)客户端运行了一个新的命令,来添加更多的数据
(2)Redis 检查内存的使用情况,如果发现大于了 maxmemory 设置的最大的内存,就执行当前设置的内存淘汰策略
(3)执行一个新的命令,以此类推

四、Redis 的 LRU

1、Redis LRU 算法并不是一个内存淘汰准确的实现,因为会存在这样的一种情况,一个 key 在过去的一段时间都没有被访问,但是在临近内存淘汰的时候,突然被调用一次,这样,在内存淘汰的时候,这个 key 就很可能不被淘汰,意味着 Redis 不能选择到最佳的淘汰候选对象,也就是过去一段时间访问次数最多的 key。它将尝试运行 LRU 算法的近似实现,就是是对少量密钥进行采样,并在采样的密钥中剔除最合适的的(具有最久的访问时间)

2、然而,自从 Redis 3.0 版本以来,这种算法得到了改进,同时也获得了一些更加符合的候选淘汰对象,提高了算法的性能,使其能够更接近真实 LRU 算法的策略
3、Redis LRU 算法的重要之处在于可以通过更改样本数量来检查每次淘汰,从而调整算法的精确度,通过使用配置maxmemory-samples 命令在生产环境中对不同的样本量值进行实验是非常简单的,该参数由以下配置指令控制:

maxmemory-samples 5

Redis 不使用真正的 lru 算法是因为它需要更多的内存支持

4、在一个理论的 LRU 实现中,预计在旧的 key中,前一半将过期。Redis LRU 算法将只在概率上过期的 key,与Redis 2.8相比,Redis 3.0在处理 5 个对象时做得更好,但是最新访问的大多数对象仍然会被 Redis 2.8 保留。在 Redis 3.0中 使用10个样本大小的近似非常接近于Redis 3.0的理论性能。

五、Redis 的 LFU

1、从 Redis 4.0 版本开始,可以使用一种新的最少使用的回收模式。在某些情况下,这种模式可能工作得更好(提供更好的命中率/失误率),因为使用 LFU Redis 将尝试跟踪访问项的频率,这样很少使用的 key 将被清除,而使用的项通常有更高的机会留在内存中。

2、之前提到过,在 LRU 中,一个最近访问过但实际上几乎从未被请求过的 key 很有可能不会过期,那么风险就是删除一个将来有更高概率被请求的 key。LFU 没有这个问题,可以更好地适应不同的访问模式。

3、配置 LFU 策略,如下:

(1)volatile-lfu :在设置了失效时间的所有 key 中,使用近似的 LFU 淘汰 key,也就是最少被访问的 key
(2)allkeys-lfu : 在所有 key 里根据 LFU 淘汰 key

4、LFU 近似像 LRU:它使用概率计数器,称为 莫里斯计数器 ,为了估计对象访问频率使用少数的比特位计算,结合衰减期,计数器是随时间减少的:在某种程度上我们不再需要考虑 key 是不是经常访问,即使他们在过去,这样的算法可以适应访问模式的转变

5、然而,与 LRU 不同的是,LFU 具有某些可调参数:例如,如果不再访问一个频繁项,那么它的级别应该降低到多快?还可以调优 Morris 计数器范围,以便更好地使算法适应特定的用例
Redis 4.0 默认的配置:

(1)计数器将近到达一百万请求的时候
(2)计数器每过一分钟衰减一次

有关如何调优这些参数的说明可以在 redis.conf 文件中找到,简单地说,如下所示:

(1)lfu-log-factor 10 可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢
(2)lfu-decay-time 1 是一个以分钟为单位的数值,可以调整counter的减少速度

衰减时间默认是1,它是计数器应该衰减的分钟数,长时间不读取key的话,是需要进行衰减的,当每次采样发现时计数器的时间比这个值要大。有一个特殊的值 ,0 表示每次扫描时计数器总是衰减,很少用到

计数器对数因子改变了需要多少次命中才能使频率计数器饱和,而频率计数器刚好在 0-255 范围内。因子越大,为了达到最大,需要的访问次数越多。因子越低,低访问的计数器分辨率越好

redis 有 binlog 吗 redis的lru_lfu