本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力
过期策略
定期删除
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的 key。
Redis 默认会每秒进行十次过期扫描(100ms一次),过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
1.从过期字典中随机 20 个 key;
2.删除这 20 个 key 中已经过期的 key;
3.如果过期的 key 比率超过 1/4,那就重复步骤 1;
redis默认是每隔 100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载。
惰性删除
所谓惰性策略就是在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除,不会给你返回任何东西。
定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,即当你主动去查过期的key时,如果发现key过期了,就立即进行删除,不返回任何东西.
总结:定期删除是集中处理,惰性删除是零散处理。不管是定期采样删除还是惰性删除都不是一种完全精准的删除,就还是会存在key没有被删除掉的场景,所以就需要内存淘汰策略进行补充。
缓存配置项
为了适配用作缓存的场景,redis 支持缓存淘汰(eviction)并提供相应的了配置项:
maxmemory
设置内存使用上限,该值不能设置为小于 1M 的容量。
选项的默认值为 0,此时系统会自行计算一个内存上限。
maxmemory-policy
熟悉 redis 的朋友都知道,每个数据库维护了两个字典:
db.dict :数据库中所有键值对,也被称作数据库的 keyspace db.expires :带有生命周期的 key 及其对应的 TTL(存留时间),因此也被称作 expire set 当达到内存使用上限 maxmemory 时,可指定的清理缓存所使用的策略有:
noeviction 当达到最大内存时直接返回错误,不覆盖或逐出任何数据 allkeys-lfu 淘汰整个 keyspace 中最不常用的 (LFU) 简 (4.0 或更高版本) allkeys-lru 淘汰整个 keyspace 最近最少使用的 (LRU) 键 allkeys-random 淘汰整个 keyspace 中的随机键 volatile-ttl 淘汰 expire set 中 TTL 最短的键 volatile-lfu 淘汰 expire set 中最不常用的键 (4.0 或更高版本) volatile-lru 淘汰 expire set 中最近最少使用的 (LRU) 键 volatile-random 淘汰 expire set 中的随机键 当 expire set 为空时, volatile-* 与 noeviction 行为一致。
maxmemory-samples
为了保证性能,redis 中使用的 LRU 与 LFU 算法是一类近似实现。
简单来说就是:算法选择被淘汰记录时,不会遍历所有记录,而是以 随机采样的方式选取部分记录进行淘汰。
maxmemory-samples 选项控制该过程的采样数量,增大该值会增加 CPU 开销,但算法效果能更逼近实际的 LRU 与 LFU 。
lazyfree-lazy-eviction
清理缓存就是为了释放内存,但这一过程会阻塞主线程,影响其他命令的执行。
当删除某个巨型记录(比如:包含数百条记录的 list)时,会引起性能问题,甚至导致系统假死。
延迟释放机制会将巨型记录的内存释放,交由其他线程异步处理,从而提高系统的性能。
开启该选项后,可能出现使用内存超过 maxmemory 上限的情况。
下面回顾 3 种最常见的缓存淘汰策略。
FIFO (先进先出)
越早进入缓存的数据,其不再被访问的可能性越大。
因此在淘汰缓存时,应选择在内存中停留时间最长的缓存记录。
使用队列即可实现该策略: 优点:实现简单,适合线性访问的场景
缺点:无法适应特定的访问热点,缓存的命中率差
簿记开销:时间 O(1) ,空间 O(N)
LRU 改进
原始的 LRU 算法缓存的是最近访问了 1 次的数据,因此不能很好地区分频繁和不频繁缓存引用。
这意味着,部分冷门的低频数据也可能进入到缓存,并将原本的热点记录挤出缓存。
为了减少偶发访问对缓存的影响,后续提出的 LRU-K 算法作出了如下改进:
在 LRU 簿记的基础上增加一个历史队列 History Queue
当记录访问次数小于 K 时,会记录在历史队列中(当历史队列满时,可以使用 FIFO 或 LRU 策略进行淘汰) 当记录访问次数大于等于 K 时,会被从历史队列中移出,并记录到 LRU 缓存中 K 值越大,缓存命中率越高,但适应性差,需要经过大量访问才能将过期的热点记录淘汰掉。
Redis的LRU实现
Redis维护了一个24位时钟,可以简单理解为当前系统的时间戳,每隔一定时间会更新这个时钟。每个key对象内部同样维护了一个24位的时钟,当新增key对象的时候会把系统的时钟赋值到这个内部对象时钟。比如我现在要进行LRU,那么首先拿到当前的全局时钟,然后再找到内部时钟与全局时钟距离时间最久的(差最大)进行淘汰,这里值得注意的是全局时钟只有24位,按秒为单位来表示才能存储194天,所以可能会出现key的时钟大于全局时钟的情况,如果这种情况出现那么就两个相加而不是相减来求最久的key。
struct redisServer {
pid_t pid;
char *configfile;
//全局时钟
unsigned lruclock:LRU_BITS;
...
};
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
/* key对象内部时钟 */
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;
Redis中的LRU与常规的LRU实现并不相同,常规LRU会准确的淘汰掉队头的元素,但是Redis的LRU并不维护队列,只是根据配置的策略要么从所有的key中随机选择N个(N可以配置)要么从所有的设置了过期时间的key中选出N个键,然后再从这N个键中选出最久没有使用的一个key进行淘汰。
1、性能问题,由于近似LRU算法只是最多随机采样N个key并对其进行排序,如果精准需要对所有key进行排序,这样近似LRU性能更高
2、内存占用问题,redis对内存要求很高,会尽量降低内存使用率,如果是抽样排序可以有效降低内存的占用
3、实际效果基本相等,如果请求符合长尾法则,那么真实LRU与Redis LRU之间表现基本无差异
4、在近似情况下提供可自配置的取样率来提升精准度,例如通过 CONFIG SET maxmemory-samples 指令可以设置取样数,取样数越高越精准,如果你的CPU和内存有足够,可以提高取样数看命中率来探测最佳的采样比例。