面试必问:Redis过期Key删除和内存淘汰策略

 

本文转载自微信公众号「虫爸说说」,作者虫爸 。转载本文请联系虫爸说说公众号。

众所周知,Redis是一种内存级kv数据库,所有的操作都是在内存里面进行,定期通过异步操作把数据库数据flush到硬盘上进行保存。因此它是纯内存操作,Redis的性能非常出色,每秒可以处理超过10万次读写操作。虽然是内存数据库,但是其数据可以持久化,而且支持丰富的数据类型。

正因为是内存级操作,那么其受限于物理内存,所以Redis提供了过期key的删除以及内存淘汰策略,从而在一定程度上,能够避免达到内存上限。

在本文中,我们首先介绍下如何对某个key设置过期时间,然后再次介绍对于这些过期key都有哪些处理策略,随后分析下在内存达到上限时候,redis采取的策略。

设置过期

redis中设置过期时间有四种方式:

expire key seconds:设置key在N秒后过期; pexpire key milliseconds:设置key在n毫秒后过期; expire key timestamp:设置key在某个时间戳后过期(精确到秒) pexpireat key millisecondstimestamp:设置key在一个时间戳后过期(精确到毫秒)

下面,我们来看看具体命令的用法。

expire: N秒后过期
127.0.0.1:6379> set key value OK 127.0.0.1:6379> expire key 100 (integer) 1 127.0.0.1:6379> ttl key (integer) 93 

其中命令TTL的全称是 time to live,意思是key在N秒后过期。比如上面的结果93表示key在93s后过期。

pexpire: N毫秒后过期
127.0.0.1:6379> set key2 value2 OK 127.0.0.1:6379> pexpire key2 100000 (integer) 1 127.0.0.1:6379> pttl key2 (integer) 94524 

pexpire key2 100000 表示 key2 设置为在 100000 毫秒(100 秒)后过期。

expireat: 在某个时间戳过期(精确到秒)
127.0.0.1:6379> set key3 value3 OK 127.0.0.1:6379> expireat key3 1630644399 (integer) 1 127.0.0.1:6379> ttl key3 (integer) 67 

expired Key3 1630644399(精确到秒)之后过期。使用TTL查询,可以发现Key3会在67s后过期。

在redis中,可以使用time命令查询当前时间的时间戳(精确到秒),例如:

127.0.0.1:6379> time

1) "1630644526"

2) "239640"

pexpireat: 在某个时间戳过期(精确到毫秒)
127.0.0.1:6379> set key4 value4 OK 127.0.0.1:6379> pexpireat key4 1630644499740 (integer) 1 127.0.0.1:6379> pttl key4 (integer) 3522 

其中,pexpireat key4 1630644499740表示key4在时间戳1630644499740(精确到毫秒)之后过期。使用TTL查询可以发现key4会在3522ms后过期。

value为string时候的过期设置

直接操作value为string的过期时间有几种方法,如下所示:

set key value ex seconds:N秒后过期 set key value ex milliseconds:设置key在n毫秒后过期; setex key seconds value:为指定的 key 设置值及其过期时间,如果 key 已经存在, SETEX 命令将会替换旧的值。 设置kv对在N秒后过期
127.0.0.1:6379> set k v ex 100 OK 127.0.0.1:6379> ttl k (integer) 97 
设置kv对在N毫秒后过期
127.0.0.1:6379> set k2 v2 px 100000 OK 127.0.0.1:6379> pttl k2 (integer) 92483 
使用setex来设置
127.0.0.1:6379> setex k3 100 v3 OK 127.0.0.1:6379> ttl k3 (integer) 91 
取消过期

使用命令:persist key去除key值的过期时间,如下代码所示:

127.0.0.1:6379> ttl k3 (integer) 97 127.0.0.1:6379> persist k3 (integer) 1 127.0.0.1:6379> ttl k3 (integer) -1 

可以看出,第一次使用TTL查询K3,97s后就会过期。使用persist命令查询K3的生命周期的结果是-1,表示K3永不过期。

过期策略

redis对过期key的删除策略,有定时删除、定期删除和惰性删除三种。

定时删除

创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务执行对key的删除操作。

优点:节约内存,到时就删除,快速释放掉不必要的内存占用 缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量 定期删除

redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?假如redis存了几十万个key,每隔100ms就遍历所有的设置过期时间的key的话,就会给CPU带来很大的负载。

优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。 缺点:难以确定删除操作执行的时长和频率。

如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好,如果执行的太少,那又和惰性删除一样了,冷静与热情之间过期键占用的内存不会及时得到释放。

另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。

惰性删除

定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个key,才会被redis给删除掉。这就是所谓的惰性删除。expireIfNeeded(),检查数据是否过期,执行get的时候调用。

优点:节约CPU性能,发现必须删除的时候才删除。 缺点:内存压力很大,出现长期占用内存的数据

换句话说,惰性删除就是用存储空间换取处理器性能

结合上述三种策略的优缺点,redis采取了折中的删除策略,即采用的是定期删除+惰性删除策略。

1、定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略

定期删除+惰性删除是如何工作的呢?

2、定期删除,redis默认每个100ms检查,产品中心是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。

3、惰性删除,也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。

但是这种方案,仍然存在缺点: 如果定期删除没删除key。然后你也没及时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。

内存淘汰策略

maxmemory 用于指定 Redis 能使用的最大内存。既可以在 redis.conf 文件中设置, 也可以在运行过程中通过 CONFIG SET 命令动态修改。

例如, 要设置 100MB 的内存限制, 可以在 redis.conf 文件中这样配置:

maxmemory 100mb 

上述命令设置了redis内存上限,当内存中的数据量达到其设置的上限的时候,就需要采取一定的淘汰策略,否则会影响redis的正常访问。

为了更好的实现这一点,必须针对不同的应用场景提供不同的策略,下面,我们将介绍下redis支持的几种内存淘汰策略。

Redis 提供了以下几种策略供用户选择,其中noeviction 策略的默认策略为。

noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。 allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。 allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。 volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。 volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。 volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

需要注意的是,如果没有设置 expire 的key, 不满足先决条件,那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

Redis 使用的并不是完全LRU算法。自动驱逐的 key , 并不一定是最满足LRU特征的那个. 而是通过近似LRU算法, 抽取少量的 key 样本, 然后删除其中访问时间最古老的那个key。

驱逐算法, 从 Redis 3.0 开始得到了巨大的优化, 使用 pool(池子) 来作为候选. 这大大提升了算法效率, 也更接近于真实的LRU算法。

在 Redis 的 LRU 算法中, 可以通过设置样本(sample)的数量来调优算法精度。

maxmemory-samples 5

以上就是Redis的六种淘汰策略。关于这六种策略的使用,使用者需要根据自身实际需要,选择合理的淘汰策略。读者可以根据自身需求,再结合下面的笔者经验,进行策略选择。

当部分数据访问频率较高而其余部分访问频率较低,或者数据的使用频率无法预测时,设置allkeys-lru比较合适。 如果所有数据访问概率大致相等,可以选择allkeys-random。 如果开发者需要通过设置不同的ttls来确定数据过期的顺序,此时可以选择volatile-ttl策略。 如果你想让一些数据长期保存,而一些数据可以消除,最好选择volatile-lru或volatile-random。 由于设置expire会消耗额外的内存,如果你打算避免Redis内存浪费在这一项上,可以选择allkeys-lru策略,这样就可以不再设置过期时间,高效利用内存。 经验之谈

对于redis的操作,我们应该慎之又慎。

不要放垃圾数据,及时清理无用数据。 key尽量都设置过期时间。对具有时效性的key设置过期时间,通过redis自身的过期key清理策略来降低过期key对于内存的占用,同时也能够减少业务的麻烦,不需要定期手动清理了。 单Key不要过大,这种key在get的时候网络传输延迟会比较大,需要分配的输出缓冲区也比较大,在定期清理的时候也容易造成比较高的延迟. 最好能通过业务拆分,数据压缩等方式避免这种过大的key的产生。 不同业务如果公用一个业务的话,最好使用不同的逻辑db分开。这是因为Redis的过期Key清理策略和强制淘汰策略都会遍历各个db。将key分布在不同的db有助于过期Key的及时清理。另外不同业务使用不同db也有助于问题排查和无用数据的及时下线。