当 Redis
内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap
)。交换会让 Redis
的性能急剧下降,对于访问量比较频繁的 Redis 来说,这样龟速的存取效率基本上等于不可用。
在生产环境中我们是不允许 Redis 出现交换行为的,为了限制最大使用内存,Redis
提供了配置参数 maxmemory
来限制内存超出期望大小。当实际内存超出 maxmemory
时,Redis
提供了几种可选策略 (maxmemory-policy
) 来让用户自己决定该如何腾出新的空间以继续提供读写服务。比如可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
Redis 具体有 6 种淘汰策略:
策略 | 描述 |
---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
noeviction | 禁止驱逐数据 |
在 Redis
中,我们可以通过 info
的命令,查看当前 Redis
的配置
127.0.0.1:6379> info Memory
# Memory
...此处忽略部分参数
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
可以看到,我们设置的策略是 禁止驱逐数据。
什么是 LRU 算法?
实现 LRU 算法除了需要 key/value
字典外,还需要附加一个链表,LRU底层结构就是 hash 表+ 双向链表
,hash 表用于保证查询操作的时间复杂度是O(1),双向链表用于保证节点插入、节点删除的时间复杂度是O(1)。
链表中的元素按照一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素。当字典的某个元素被访问时,它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时间顺序。
位于链表尾部的元素就是不被重用的元素,所以会被踢掉。位于表头的元素就是最近刚刚被人用过的元素,所以暂时不会被踢。
LRU 算法相关代码可以参考:https://leetcode-cn.com/problems/lru-cache/
Redis 近似 LRU 算法
Redis
使用的是一种近似 LRU 算法,它跟 LRU 算法还不太一样。
之所以不使用 LRU 算法,是因为:
- 原生LRU算法需要 双向链表 来管理数据,需要大量的额外内存;
- 数据访问时涉及数据移动,有性能损耗;
- Redis现有数据结构需要进行较大改造;
近似 LRU 算法则很简单,在现有数据结构的基础上使用 随机采样法 来淘汰元素,能达到和 LRU 算法非常近似的效果。Redis 为实现近似 LRU 算法,它给每个 key
增加了一个额外的小字段,这个字段的长度是 24 个 bit,也就是最后一次被访问的时间戳。在Redis中,Redis的key的底层结构是 redisObject,redisObject 中 lru:LRU_BITS 字段用于记录该key最近一次被访问时的Redis时钟 server.lruclock(Redis在处理数据时,都会调用lookupKey方法用于更新该key的时钟)。
LRU 淘汰的处理方式只有懒惰处理。当 Redis 执行写操作时,发现内存超出 maxmemory
,就会执行一次
LRU 淘汰算法。这个算法也很简单,就是随机采样出 5(可以配置) 个 key
,然后淘汰掉最旧的 key
,如果淘汰后内存还是超出 maxmemory
,那就继续随机采样淘汰,直到内存低于maxmemory
为止。
如何采样就是看 maxmemory-policy
的配置,如果是 allkeys
就是从所有的 key
字典中随机,如果是 volatile
就从带过期时间的 key
字典中随机。每次采样多少个 key
看的是maxmemory_samples
的配置,默认为 5。也可以自己手动修改 redis.conf
进行配置。
# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated
# algorithms (in order to save memory), so you can tune it for speed or
# accuracy. By default Redis will check five keys and pick the one that was
# used least recently, you can change the sample size using the following
# configuration directive.
#
# The default of 5 produces good enough results. 10 Approximates very closely
# true LRU but costs more CPU. 3 is faster but not very accurate.
#
# maxmemory-samples 5
同时 Redis3.0 在算法中增加了淘汰池,进一步提升了近似 LRU 算法的效果。淘汰池是一个数组,它的大小是 maxmemory_samples
,在每一次淘汰循环中,新随机出来的 key
列表会和淘汰池中的 key
列表进行融合,淘汰掉最旧的一个 key
之后,保留剩余较旧的 key
列表放入淘汰池中留待下一个循环。