Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE、PEXPIRE、EXPIREAT三个命令都是使用PEXPIREAT命令来实现的。
1.保存过期时间
redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典。
eg.键为alphabet键对象,值为1385877600000.
2.移除过期时间
PERSIST命令可以移除一个键的过期时间:
PERSIST命令就是PEXPIREAT命令的反操作:PERSIST命令在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联。
3.返回剩余时间
TTL命令以秒为单位返回键的剩余生存时间,而PTTL命令则以毫 秒为单位返回键的剩余生存时间。
TTL和PTTL两个命令都是通过计算键的过期时间和当前时间之间的差来实现的。
4.过期键的判定
(1)检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。
(2)通过TTL和PTTL返回值查看,如果返回值大于0则未过期,小于0则过期。
如果一个键过期了,那么它什么时候会被删除呢?在redis中,有三种不同的删除策略:
其中第一种和第三种为主动删除策略,而第二种则为被动删除策略。
1.定时删除
优点:对内存友好。通过使用定时器,定时删除策略可以保证过期键会尽可能快地被删除,并释放过期键所占用的内存。
缺点:对CPU时间不友好。在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分 CPU时间,在内存不紧张但是CPU时间非常紧张的情况下,将CPU时间用在删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。
在实际中,要让服务器创建大量的定时器,从而实现定时删除策略并不现实。
2.惰性删除
优点:对CPU时间友好。程序只会在取出键时才对键进行过期检查,这可以保证删除过期键的操作只会在非做不可的情况下进行,并且删除的目标仅限于当前处理的键,不会在其他无关的过期键上花费时间。
缺点:对内存不友好。如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删 除,它所占用的内存就不会释放。
3.定期删除
定期删除策略是前两种策略的一种整合和折中。
但是定期删除策略的难点是确定删除操作执行的时长和频率。
实现过程:
每当 Redis的服务器周期性操作redis.c/serverCron函数执行时, activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。
在实际中,Redis服务器使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
定期删除+惰性删除存在的问题:
如果某个key过期后,定期删除没删除成功(没抽取到),然后也没再次去请求key,也就是说惰性删除也没生效。这时,如果大量过期的key堆积在内存中,redis的内存会越来越高,导致redis的内存块耗尽。那么就应该采用内存淘汰机制。
1.noeviction:
不进行淘汰数据。一旦缓存被写满,再有写请求进来,Redis就不再提供服务,而是直接返回错误。
2.volatile-random:
在设置了过期时间的键值对中,随机移除某个键值对。
3.volatile-ttl:
在设置了过期时间的键值对中,移除即将过期的键值对。
4.volatile-lru:
在设置了过期时间的键值对中,移除最近最少使用的键值对。
5.volatile-lfu:
在设置了过期时间的键值对中,移除最不经常使用的键值对(历史访问频率)。
6.allkeys-random:
在所有键值对中,随机移除某个key。
7.allkeys-lru:
在所有的键值对中,移除最近最少使用的键值对。
8.allkeys-lfu:
在所有的键值对中,移除最不经常使用的键值对。
通常情况下优先使用 allkeys-lru 策略。这样可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。
如果业务数据中有明显的冷热数据区分,建议使用 allkeys-lru 策略。
如果业务应用中的数据访问频率相差不大,可以使用 allkeys-random随机选择淘汰的数据就行。
在redis中,一般是通过LRU和LFU来实现淘汰的,但是这两种算法在redis中与正常实现略有不同。
1.LRU
Redis维护了一个24位时钟,可以理解为当前系统的时间戳,每隔一定时间会更新这个时钟。每个key对象内部同样维护了一个24位的时钟,当新增key对象的时候会把系统的时钟赋值到这个内部对象时钟。比如我现在要进行LRU,那么首先拿到当前的全局时钟,然后再找到内部时钟与全局时钟距离时间最久的(差最大)进行淘汰。
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进行淘汰。
为什么要使用近似LRU?
(1)如果精准LRU则需要对所有key进行排序,这样近似LRU性能更高。
(2)redis对内存要求很高,会尽量降低内存使用率,如果是抽样排序可以有效降低内存的占用。
(3)实际效果基本相等,如果请求符合长尾法则,那么真实LRU与Redis LRU之间表现基本无差异。
(4)可以通过配置的取样率来提升精准度,例如通过 CONFIG SET maxmemory-samples 指令可以设置取样数,取样数越高越精准。
2.LFU
LFU是在Redis4.0后出现的,它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。LFU算法能更好的表示一个key被访问的热度。
假如你使用的是LRU算法,一个key很久没有被访问到,只刚刚是偶尔被访问了一次,那么它就被认为是热点数据,不会被淘汰,而有些key将来是很有可能被访问到的则被淘汰了。如果使用LFU算法则不会出现这种情况,因为使用一次并不会使一个key成为热点数据。
LFU (Least Frequently Used) :最近最不频繁使用,跟使用的次数有关,淘汰使用次数最少的。
LRU (Least Recently Used):最近最少使用,跟使用的最后一次时间有关,淘汰最近使用时间离现在最久的。
LFU算法在Redis中是通过一个计数器来实现的,每个key都有一个计数器,访问频度越高,计数器的值就越大,Redis就根据计数器的值来淘汰key,当然计数器的值也是会随着时间减少的。
用户可配置的LFU算法参数一共有两个分别是:lfu-log-factor和lfu-decay-time