Redis系列之过期淘汰机制
概述
使用redis时,一般是作为缓存系统,而不是存储系统。缓存系统,即需要设置一个生存时间(TTL,time to live);存储系统,即不设置生存时间,永不过期。除了生存时间,还有一个过期时间的概念,expire time,效果一样,本文不加以区分。带有TTL属性的key在Redis中被称为是不稳定的。设置TTL时间后,又想让缓存永不过期,可使用persist key
,persist可以移除一个键的过期时间。过期时间timestamp
是一个unix时间戳。
设置过期时间的几类方式:
expire key time
:单位为秒;pexpire key time
:单位毫秒;expireat key timestamp
或者pexpireat key timestamp
为key指定过期时间,单位分别为秒和毫秒;p
表示毫秒,不再赘述;setex(String key, int seconds, String value)
,只能用于字符串键,还有其他指令如setnx
;
expire、pexpire、expireat、pexpireat
四个命令,前三个底层都是基于pexpireat,
怎么得知某个键的剩余过期时间还有多少?通过TTL命令或PTTL命令
设置过期时间后,Redis如何判断是否过期,怎么删除?
过期策略
有3个删除策略:
- 定时删除;
- 惰性删除;
- 定期删除
定时删除
策略:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。
优点:内存友好,保证内存被尽快释放。
缺点:
- 若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,显得避重就轻没有优先级;
- 定时器的创建需要用到redis的时间事件,其实现方式为无序链表,查找事件的时间复杂度为
O(N)
,效率低;
惰性删除
策略:key过期时不删除,每次从键空间获取键时,检查键是否过期,若过期,则删除,返回null。
优点:对CPU时间友好。
缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,则可能发生内存泄露(无用的垃圾占用大量的内存)
定期删除
策略:每隔一段时间,程序对数据库进行一次检查,删除过期键。扫描什么库,删除多少键,有算法决定。定期删除主要是为了避免定时删除和惰性删除的问题,是前两者的一个组合折中。
优点:
- 通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用,解决定时删除的CPU时间占用问题;
- 定期删除过期key,解决惰性删除的内存浪费问题。
难点:
- 合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除),每次执行时间太长,或者执行频率太高对cpu都是一种压力。
- 每次进行定期删除操作执行之后,需要记录遍历循环到哪个标志位,以便下一次定期时间来时,从上次位置开始进行循环遍历。
说明:
memcached只用惰性删除,而redis使用惰性删除与定期删除,二者的区别之一;
对于懒汉式删除而言,并不是只有获取key的时候才会检查key是否过期,在某些设置key的方法上也会检查(eg.setnx key2 value2:该方法类似于memcached的add方法,如果设置的key2已经存在,那么该方法返回false,什么都不做;如果设置的key2不存在,那么该方法设置缓存key2-value2。假设调用此方法的时候,发现redis中已经存在了key2,但是该key2已经过期了,如果此时不执行删除操作的话,setnx方法将会直接返回false,也就是说此时并没有重新设置key2-value2成功,所以对于一定要在setnx执行之前,对key2进行过期检查)。
Redis
Redis采用的过期策略:惰性删除+定期删除,在合理使用CPU时间和避免内存空间浪费之间取得平衡。
惰性删除
有db.c/expireIfNeeded
函数实现,所有读写数据库的Redis命令在执行之前都会调用此函数对键进行过期检查,过期则删除。相当于一个过滤器的角色。
定期删除
有redis.c/activeExpireCycle
函数实现,每当服务器周期性操作redis.c/serverCron
函数时被调用执行,在规定的时间内,分多次遍历服务器的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,过期则删除。
伪代码:
流程总结:
- 函数每次执行时,都从一定数量的数据库中取出一定数量的随机键进行检查,删除其中的过期键;
- 全局变量
current_db
记录当前activeExpireCycle
函数检查进度,在下一次被调用时,接着上一次的进度进行处理; - 随着函数
activeExpireCycle
的不断执行,服务器中的所有数据库都会被检查一遍,此时可以将current_db
重置为0,开始新一轮的检查。
内存不足
当前已用内存超过maxmemory限定时,触发主动清理策略,Redis有6中策略:
- volatile-lru:只对设置过期时间的key进行LRU(默认值)
- allkeys-lru:删除lru算法的key
- volatile-random:随机删除即将过期key
- allkeys-random:随机删除
- volatile-ttl:删除即将过期的
- noeviction:永不过期,返回错误当
mem_used
内存已经超过maxmemory的设定,对于所有的读写请求,都会触发
当mem_used内存已经超过maxmemory的设定,对于所有的读写请求,都会触发redis.c/freeMemoryIfNeeded(void)
函数清理超出的内存,清理过程是阻塞的,直到清理出足够的内存空间。所以如果在达到maxmemory并且调用方还在不断写入的情况下,可能会反复触发主动清理策略,导致请求会有一定的延迟。
清理时会根据用户配置的maxmemory-policy
来做适当的清理(一般是LRU或TTL),这里的LRU或TTL策略并不是针对redis的所有key,而是以配置文件中的maxmemory-samples
个key作为样本池进行抽样清理。
maxmemory-samples
在redis-3.0.0中的默认配置为5,如果增加,会提高LRU或TTL的精准度,redis作者测试的结果是当这个配置为10时已经非常接近全量LRU的精准度,并且增加maxmemory-samples
会导致在主动清理时消耗更多的CPU时间,建议:
尽量不要触发maxmemory,最好在mem_used内存占用达到maxmemory的一定比例后,需要考虑调大hz以加快淘汰,或者进行集群扩容。
如果能够控制住内存,则可以不用修改maxmemory-samples
配置;如果Redis本身就作为LRU cache服务(这种服务一般长时间处于maxmemory状态,由Redis自动做LRU淘汰),可以适当调大maxmemory-samples
。
原理
expires字典,过期字典。待补充
参考
Redis设计与实现