Redis剖析 — 过期策略和内存淘汰机制

前 言

Redis是当前一款较受欢迎的NoSQL数据库,其基于内存运行,性能高效。既然是基于内存运行的,那么它就会有存储上限,最高也就是物理内存的容量。当超出设定的Redis内存时,要么释放内存,那么报OOM(内存溢出)的异常了。那么Redis 是如何处理过期数据的当内存不够用时 Redis 又是如何处理的

在本篇博客文章中,我将带着这些问题详细的讲解Redis的过期策略内存淘汰机制

📝 预备小知识

  1. 最大可用内存maxmemory

maxmemoryRedis中一个非常重要的参数,用于设置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 <bytes> 

其中,<bytes>表示可用的最大内存大小,例如:

maxmemory 104857600

表示Redis可用的最大内存大小为100MB。

  • 使用Redis命令动态设置

可以使用RedisCONFIG命令,在运行时动态设置maxmemory参数的值。具体来说,可以使用以下命令:

CONFIG SET maxmemory <bytes> 

其中,<bytes>表示可用的最大内存大小,例如:

CONFIG SET maxmemory 104857600

表示Redis可用的最大内存大小为100MB。

需要注意的是,当使用CONFIG SET命令动态设置maxmemory参数时,需要确保Redis的运行状态正常。否则,如果Redis已经超出了新设置的最大内存大小,可能会导致Redis崩溃或数据丢失。

一般推荐Redis设置内存为最大物理内存的四分之三

如何查看Redis内存使用情况呢?

  1. info memory

  1. config get maxmemory

如果Redis内存使用真超出设定的最大值会怎么样呢?

当没有加上过期时间就会导致数据写满maxmemory,为了避免类似情况,就有了下面的过期策略和内存淘汰策略,我们继续往下看吧!

一、Redis过期策略

我们知道,redis中缓存的数据是有过期时间的,当缓存数据失效时,如何一直不清理,就会堆满整个内存。就像垃圾桶里的东西已经没用了,但如果不到掉的话就会满出来,这显然是不合理的。

那么Redis是如何处理过期数据的呢?

这就要讲到Redis的过期策略,它是指在Redis中对过期键值的处理方式,当一个key过期后,Redis会自动将其删除,以节省内存空间

那么什么是过期时间和过期键呢?

Redis的过期策略中,有两个重要的概念:过期时间和过期键。

  • 过期时间:Redis中的每个key都有一个过期时间,它表示该key的存活时间。过期时间可以通过EXPIRE命令来设置,例如:EXPIRE key 10表示让key在10秒后过期。

  • 过期键:当一个key的过期时间到达后,它就成为了一个过期键,Redis会在定期扫描和惰性删除中将这些过期键进行清理。

对于处理过期数据,我们首先会想到什么方案?

  1. 立即删除

最简单的就是到期后立刻删除,啥也不用想,你到期我就扇你,哦不,是删你🤣。但会存在一些问题。

Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放。但是立即删除对cpu是最不友好的

因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,让CPU心累,时时需要删除,忙死。这会产生大量的性能消耗,同时也会影响数据的读取操作。这会产生大量的性能消耗,同时也会影响数据的读取操作。

总结: 对CPU不友好,用处理器性能换取存储空间(拿时间换空间)

  1. 惰性删除

上面立即删除是过期就删除,现在我数据过期了不立马响应,即数据到达过期时间,不做处理,等下次访问该数据时,如果未过期,返回数据;发现已过期,删除,返回不存在

惰性删除策略的缺点是,它对内存是最不友好的如果一个键已经过期,而这个键又仍然保留在redis中,那么只要这个过期键不被删除,它所占用的内存就不会释放。在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏–无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好消息

总结: 对memory不友好,用存储空间换取处理器性能(拿空间换时间)

💡 如果要开启惰性淘汰,lazyfree-lazy-eviction=yes

上面两种方案都比较极端,那如果折中一下是不是会更好,所以引出了第三种策略:定期删除

1.1 概念

Redis通过使用一个定期扫描和惰性删除的机制,来实现过期键的清理。即Redis会每隔一段时间扫描一定数量的过期键,将这些过期键进行删除通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。

周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度

特点1: CPU性能占用设置有峰值,检测频度可自定义设置

特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理

总结: 周期性抽查存储空间(随机抽查,重点抽查)

📝 举例:

redis默认每隔100ms检查是否有过期的key,有过期key则删除。

注意: redis不是每隔100ms将所有的key检查一次, 而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis直接进去ICU)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。

定期删除策略的难点是确定删除操作执行的时长和频率: 如果删除操作执行得太频繁或者执行的时间太长,定期删除策略就会退化成立即删除策略,以至于将CPU时间过多地消耗在删除过期键上面。如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。

总结: 定期抽样Key,判断是否过期,有漏网之鱼

1.2 基本命令

Redis提供了多种命令来支持过期策略的实现,其中比较常用的命令包括:

  • EXPIRE key seconds:为指定key设置过期时间(单位为秒)。

  • TTL key:返回指定key的剩余过期时间(单位为秒),如果key不存在或没有设置过期时间,则返回-1。

  • PTTL key:返回指定key的剩余过期时间(单位为毫秒),与TTL命令不同的是,它返回的是毫秒级别的时间戳,如果key不存在或没有设置过期时间,则返回-1。

二、Redis内存淘汰机制

既然上面的方案都有缺陷,那么我们又该如何解决呢?这时候就得引入我们的内存淘汰机制了,用于在内存不足时淘汰一些数据,以腾出更多的内存空间。

📝前置小知识

  • 在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关键是看一定时间段内页面被使用的频率!

2.1 淘汰策略详解

2.1.1 noeviction

noevictionRedis默认内存淘汰策略当Redis的内存使用量达到最大限制时,Redis将停止写入并返回错误。这种策略适用于对内存使用量有严格限制的场景,例如嵌入式设备中的Redis。

2.1.2 allkeys-lru

allkeys-lru策略会使用LRU算法进行删除,从所有的key中选择最近最少使用的数据进行淘汰。这种策略适用于缓存类型的应用场景,可以保留最常用的数据,同时淘汰很久没使用过的数据。

2.1.3 volatile-lru

volatile-lru策略会从设置了过期时间的key中选择最近最少使用的数据进行淘汰。这种策略适用于需要限制缓存时效性的应用场景,可以保留最近使用的数据,同时淘汰过期的数据。

2.1.4 allkeys-random

allkeys-random策略会从所有的key中随机选择一些数据进行淘汰。这种策略适用于对数据不做任何保留要求的应用场景,可以随机淘汰一些数据,从而腾出更多的内存空间。

2.1.5 volatile-random

volatile-random策略会设置了过期时间的key中随机选择一些数据进行淘汰。这种策略适用于需要限制缓存时效性的应用场景,可以随机淘汰一些过期的数据,从而腾出更多的内存空间。

2.1.6 volatile-ttl

删除马上要过期的key

2.1.7 alkeys-lfu

对所有key使用LFU算法进行删除

2.1.8 volatile-lfu

对所有设置了过期时间的key使用LFU算法进行删除

💡总结

介绍完这八种策略,要么要命的来了,面试官问你,你平常用哪种……(露出小鸡脚了吧)
  • 在所有的 key都是最近最经常使用,那么就需要选择 allkeys-lru进行置换最近最不经常使用的key,如果你不确定使用哪种策略,那么推荐使用allkeys-Iru

  • 如果所有的 key 的访问概率都是差不多的,那么可以选用allkeys-random策略去置换数据

  • 如果对数据有足够的了解,能够为 key 指定 hint(通过expire/ttl指定),那么可以选择volatile-ttl进行置换

2.2 应用场景

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。

2.3 Lua脚本自定义策略

除了以上几种内存淘汰策略之外,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过期策略和内存淘汰机制

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值