Redis是当前一款较受欢迎的NoSQL数据库,其基于内存运行,性能高效。既然是基于内存运行的,那么它就会有存储上限,最高也就是物理内存的容量。当超出设定的Redis内存时,要么释放内存,那么报OOM(内存溢出)的异常了。那么Redis 是如何处理过期数据的?当内存不够用时 Redis 又是如何处理的?
在本篇博客文章中,我将带着这些问题详细的讲解Redis的过期策略和内存淘汰机制。
预备小知识
最大可用内存maxmemory
maxmemory是Redis中一个非常重要的参数,用于设置Redis实例可用的最大内存。
具体来说,maxmemory参数用于设置Redis实例能够使用的最大内存大小。当Redis使用的内存达到maxmemory时,Redis会根据一定的策略来释放部分内存,以保证Redis不会超出可用内存大小。
maxmemory参数的默认值为0,这里的0不是说它分配的内存大小是0,如果是的话那数据都不用存了。它实质上表示Redis实例不会限制可用内存大小(32位系统有限制3GB),就是我的上限取决于物理内存。但是,在生产环境中,我们通常需要手动设置maxmemory参数,以避免Redis使用过多的内存而导致系统崩溃。
那么问题又来了,如何设置maxmemory的大小呢?
两种方式
在Redis配置文件中设置
可以在Redis的配置文件中设置maxmemory参数的值,以限制Redis可用的最大内存空间。具体来说,可以在Redis配置文件(redis.conf)中添加以下配置:
maxmemory
其中,
maxmemory 104857600
表示Redis可用的最大内存大小为100MB。
使用Redis命令动态设置
可以使用Redis的CONFIG命令,在运行时动态设置maxmemory参数的值。具体来说,可以使用以下命令:
CONFIG SET maxmemory
其中,
CONFIG SET maxmemory 104857600
表示Redis可用的最大内存大小为100MB。
需要注意的是,当使用CONFIG SET命令动态设置maxmemory参数时,需要确保Redis的运行状态正常。否则,如果Redis已经超出了新设置的最大内存大小,可能会导致Redis崩溃或数据丢失。
一般推荐Redis设置内存为最大物理内存的四分之三
如何查看Redis内存使用情况呢?
info memory
config get maxmemory
如果Redis内存使用真超出设定的最大值会怎么样呢?
当没有加上过期时间就会导致数据写满maxmemory,为了避免类似情况,就有了下面的过期策略和内存淘汰策略,我们继续往下看吧!
我们知道,redis中缓存的数据是有过期时间的,当缓存数据失效时,如何一直不清理,就会堆满整个内存。就像垃圾桶里的东西已经没用了,但如果不到掉的话就会满出来,这显然是不合理的。
那么Redis是如何处理过期数据的呢?
这就要讲到Redis的过期策略,它是指在Redis中对过期键值的处理方式,当一个key过期后,Redis会自动将其删除,以节省内存空间。
那么什么是过期时间和过期键呢?
Redis的过期策略中,有两个重要的概念:过期时间和过期键。
过期时间:Redis中的每个key都有一个过期时间,它表示该key的存活时间。过期时间可以通过EXPIRE命令来设置,例如:EXPIRE key 10表示让key在10秒后过期。
过期键:当一个key的过期时间到达后,它就成为了一个过期键,Redis会在定期扫描和惰性删除中将这些过期键进行清理。
对于处理过期数据,我们首先会想到什么方案?
立即删除
最简单的就是到期后立刻删除,啥也不用想,你到期我就扇你,哦不,是删你。但会存在一些问题。
Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的。
因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。这会产生大量的性能消耗,同时也会影响数据的读取操作。这会产生大量的性能消耗,同时也会影响数据的读取操作。
总结: 对CPU不友好,用处理器性能换取存储空间(拿时间换空间)
惰性删除
上面立即删除是过期就删除,现在我数据过期了不立马响应,即数据到达过期时间,不做处理,等下次访问该数据时,如果未过期,返回数据;发现已过期,删除,返回不存在。
惰性删除策略的缺点是,它对内存是最不友好的。如果一个键已经过期,而这个键又仍然保留在redis中,那么只要这个过期键不被删除,它所占用的内存就不会释放。在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏–无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息。
总结: 对memory不友好,用存储空间换取处理器性能(拿空间换时间)
如果要开启惰性淘汰,lazyfree-lazy-eviction=yes
上面两种方案都比较极端,那如果折中一下是不是会更好,所以引出了第三种策略:定期删除
Redis通过使用一个定期扫描和惰性删除的机制,来实现过期键的清理。即Redis会每隔一段时间扫描一定数量的过期键,将这些过期键进行删除。通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。
周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
特点1: CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结: 周期性抽查存储空间(随机抽查,重点抽查)
举例:
redis默认每隔100ms检查是否有过期的key,有过期key则删除。
注意: redis不是每隔100ms将所有的key检查一次, 而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis直接进去ICU)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
定期删除策略的难点是确定删除操作执行的时长和频率: 如果删除操作执行得太频繁或者执行的时间太长,定期删除策略就会退化成立即删除策略,以至于将CPU时间过多地消耗在删除过期键上面。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。
总结: 定期抽样Key,判断是否过期,有漏网之鱼
Redis提供了多种命令来支持过期策略的实现,其中比较常用的命令包括:
EXPIRE key seconds:为指定key设置过期时间(单位为秒)。
TTL key:返回指定key的剩余过期时间(单位为秒),如果key不存在或没有设置过期时间,则返回-1。
PTTL key:返回指定key的剩余过期时间(单位为毫秒),与TTL命令不同的是,它返回的是毫秒级别的时间戳,如果key不存在或没有设置过期时间,则返回-1。
既然上面的方案都有缺陷,那么我们又该如何解决呢?这时候就得引入我们的内存淘汰机制了,用于在内存不足时淘汰一些数据,以腾出更多的内存空间。
前置小知识
在Redis配置中有八种淘汰策略,默认使用的是 maxmemory-policy noeviction
LRU 和 LFU
相信学过操作系统的朋友并不陌生这两种页面置换算法吧,下面简单介绍一下:
LRU: 最近最少使用页面置换算法,淘汰最长时间未被使用的页面,看页面最后一次被使用到发生调度的时间长短,首先淘汰最长时间未被使用的页面。
LFU: 最近最不常用页面置换算法,淘汰一定时期内被访问次数最少的页,看一定时间段内页面被使用的频率,淘汰一定时期内被访问次数最少的页。
举个栗子:
某次时期Time为10分钟,如果每分钟进行一次调页,主存块为3,若所需页面走向为2 1 2 1 2 3 4
假设到页面4时会发生缺页中断
若按LRU算法, 应换页面1(1页面最久未被使用),但按LFU算法应换页面3(十分钟内,页面3只使用了一次)
可见LRU关键是看页面最后一次被使用到发生调度的时间长短, 而LFU关键是看一定时间段内页面被使用的频率!
noeviction是Redis的默认内存淘汰策略,当Redis的内存使用量达到最大限制时,Redis将停止写入并返回错误。这种策略适用于对内存使用量有严格限制的场景,例如嵌入式设备中的Redis。
allkeys-lru策略会使用LRU算法进行删除,从所有的key中选择最近最少使用的数据进行淘汰。这种策略适用于缓存类型的应用场景,可以保留最常用的数据,同时淘汰很久没使用过的数据。
volatile-lru策略会从设置了过期时间的key中选择最近最少使用的数据进行淘汰。这种策略适用于需要限制缓存时效性的应用场景,可以保留最近使用的数据,同时淘汰过期的数据。
allkeys-random策略会从所有的key中随机选择一些数据进行淘汰。这种策略适用于对数据不做任何保留要求的应用场景,可以随机淘汰一些数据,从而腾出更多的内存空间。
volatile-random策略会从设置了过期时间的key中随机选择一些数据进行淘汰。这种策略适用于需要限制缓存时效性的应用场景,可以随机淘汰一些过期的数据,从而腾出更多的内存空间。
删除马上要过期的key
对所有key使用LFU算法进行删除
对所有设置了过期时间的key使用LFU算法进行删除
总结
介绍完这八种策略,要么要命的来了,面试官问你,你平常用哪种……(露出小鸡脚了吧)
在所有的 key都是最近最经常使用,那么就需要选择 allkeys-lru进行置换最近最不经常使用的key,如果你不确定使用哪种策略,那么推荐使用allkeys-Iru
如果所有的 key 的访问概率都是差不多的,那么可以选用allkeys-random策略去置换数据
如果对数据有足够的了解,能够为 key 指定 hint(通过expire/ttl指定),那么可以选择volatile-ttl进行置换
Redis的内存淘汰机制可以用于缓存、计数器、消息队列等场景。下面以缓存为例,介绍Redis的内存淘汰机制的应用。 假设我们有一个需要频繁查询的函数get_data(),该函数的计算成本很高,我们希望将其计算结果缓存到Redis中,以提高性能。为了避免缓存占用过多内存,我们可以使用Redis的内存淘汰机制,在内存空间不足时自动淘汰一些数据。 下面是一个简单的伪代码:
import redis
import time
# 连接Redis数据库
r = redis.Redis(host='localhost', port=6379)
# 定义缓存过期时间为1小时
EXPIRE_TIME = 3600
def get_data(key):
# 从缓存中获取数据
value = r.get(key)
if value:
# 如果数据存在,则直接返回
return value.decode('utf-8')
else:
# 如果数据不存在,则重新计算并存入缓存
time.sleep(1) # 模拟计算成本高的情况
data = "data for " + key
r.set(key, data, ex=EXPIRE_TIME)
return data
在上述代码中,我们首先连接Redis数据库,并定义了一个缓存过期时间为1小时的常量EXPIRE_TIME。然后定义了一个函数get_data(),用于从缓存中获取数据。如果数据存在于缓存中,则直接返回;否则重新计算并存入缓存,并设置过期时间。
在这个缓存中,我们可以使用内存淘汰策略。例如,如果我们希望在内存空间不足时,优先淘汰一些设置了过期时间的key,可以使用volatile-lru策略,即从设置了过期时间的key中,选择最近最少使用的数据进行淘汰。
下面是设置volatile-lru策略的示例代码:
# 将内存淘汰策略设置为volatile-lru
r.config_set('maxmemory-policy', 'volatile-lru')
在这个示例代码中,我们使用config_set()方法将Redis的内存淘汰策略设置为volatile-lru。
除了以上几种内存淘汰策略之外,Redis还支持使用Lua脚本自定义内存淘汰策略,可以根据具体的场景和需求,编写符合自己需求的内存淘汰脚本。
我们可以使用EVAL命令执行Lua脚本,来实现自定义的内存淘汰策略。
下面,我们通过一个示例来介绍如何使用Lua脚本来实现自定义的内存淘汰策略。假设我们有一个缓存,里面存储了一些数据,这些数据有一个score属性,代表了数据的重要程度。我们希望在内存空间不足时,优先淘汰score较低的数据。
首先,我们需要在Redis中注册一个Lua脚本,该脚本的内容如下:
redis.call('SELECT', ARGV[1])
local keys = redis.call('KEYS', '*')
local lowest_score = 1000000000
local lowest_score_key = ''
// 遍历每个键
for _, key in ipairs(keys) do
local score = tonumber(redis.call('HGET', key, 'score'))
if score and score < lowest_score then
lowest_score = score
lowest_score_key = key
end
end
//删除该键
if lowest_score_key ~= '' then
redis.call('DEL', lowest_score_key)
end
在这个脚本中,我们首先使用SELECT命令选择数据库,然后使用KEYS命令获取所有的key。接着,我们遍历所有的key,找到score属性最小的那个key,并将其删除。
然后,我们就可以使用EVAL命令来执行该脚本了。当内存空间不足时,我们可以使用以下命令来调用Lua脚本:
redis-cli EVAL "redis.call('SELECT', ARGV[1]); redis.call('SCRIPT', 'LOAD', '/path/to/script.lua'); redis.call('EVALSHA', sha1, 0)" 0 0
其中,ARGV[1]是数据库的编号,/path/to/script.lua是我们注册的Lua脚本的路径,sha1是脚本的SHA1值。
总的来说,自定义Lua脚本是一种非常灵活的内存淘汰方式,可以根据具体的业务需求,灵活定制淘汰规则,从而达到更好的性能和效果。需要注意的是,自定义Lua脚本的性能可能会比Redis原生的内存淘汰策略低一些,因为需要在Lua虚拟机中执行。因此,在编写自定义Lua脚本时,需要考虑脚本的性能和复杂度,以及业务需求是否真正需要使用自定义Lua脚本。
尚硅谷Redis7-Redis过期策略和内存淘汰机制