前言

Redis作为当下最受欢迎的NoSQL数据库之一,在很多场景下都会使用到;Redis的存储分为内存存储、磁盘存储和log文件三部分,重启后,Redis可以从磁盘重新将数据加载到内存中,这些可以通过配置文件对其进行配置,正因为这样,Redis才能实现持久化

而内存存储向来都离不开内存管理的问题,本文就从Redis的内存过期策略和内存淘汰机制展开来讲

需要了解的几个词

  • LRU:LRU是Least Recently Used的缩写,即最久没有访问的内容作为替换对象,是一种常用的页面置换算法,其它常见的还有FIFO、LFU、NMRU等;值得一提的是,Redis中的LRU实现与常规的LRU并不相同,我在以后的文章中可能会总结(咕咕咕)
  • 定时器:这里的定时器指通过各种方式(如管道)实现的定时任务触发器,Go中的定时器可见我的另一篇文章:Go 并发编程与定时器
  • 持久化存储:一般对比于缓存存储,即cache-only模式,此模式下若服务停止 / 停机,则会造成数据丢失;而持久化存储则会为内存中的数据持久备份到磁盘文件,在服务重启后可以恢复,此模式下数据相对安全

内存过期策略

内存过期策略主要的作用就是,在缓存过期之后,能够及时的将失效的缓存从内存中删除,以减少内存的无效暂用,达到释放内存的目的

过期策略分类

Redis内存过期策略分为三类,定时策略、惰性策略和定期策略

定时策略

含义:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除

优点:保证内存被尽快释放,减少无效的缓存暂用内存

缺点:若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key;定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重,所以一般来说不会选择该策略模式
定时策略

惰性策略

含义:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。

优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的key了)。

缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,此时的无效缓存是永久暂用在内存中的,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
惰性策略

定期策略

含义:每隔一段时间对设置了缓存时间的key进行检测,如果可以已经失效,则从内存中删除,如果未失效,则不作任何处理。

优点:通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用–处理"定时删除"的缺点
定期删除过期key–处理"惰性删除"的缺点。

缺点:在内存友好方面,不如"定时删除",因为是随机遍历一些key,因此存在部分key过期,但遍历key时,没有被遍历到,过期的key仍在内存中。在CPU时间友好方面,不如"惰性删除",定期删除也会暂用CPU性能消耗。

难点:合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况和实际需求来决定)

该方式不会遍历所有的key,而是随机抽取一些key做过期检测

策略注意事项

过期策略对持久化存储的影响

持久化存储,指的是将内存的缓存永久存在磁盘中。也就是说我们的AOF和RDB持久化存储方式。因为该两种方式,将内存中的数据写入磁盘,这时候就需要考虑到我们过期的缓存是否会被写入到磁盘中?如果写入磁盘又是怎么处理的?

RDB持久化

持久化key之前,会检查是否过期,过期的key不进入RDB文件

数据载入数据库之前,会对key先进行过期检查,如果过期,不导入数据库(主库情况)

AOF持久化

当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)

当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)

因为AOF方式,向存储文件追加的是Redis的操作命令,而不是具体的数据,然而RDB确是存储的安全的二进制内容

重写时,会先判断key是否过期,已过期的key不会重写到aof文件

即使在重写时,不验证是否过期,然而追加了del命令,测试无效的key同样会被删除。判断的情况是为了防止没有加入del命令的key

内存淘汰策略

内存淘汰机制针对是内存不足的情况下的一种Redis处理机制。例如,当前的Redis存储已经超过内存限制了,然而我们的业务还在继续往Redis里面追加缓存内容,这时候Redis的淘汰机制就起到作用了

淘汰策略一般在redis.conf中设置

Redis常见的六种淘汰策略

  • noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。 大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL )
  • allkeys-lru: 所有key通用; 优先删除最近最少使用(less recently used ,LRU) 的 key
  • volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key
  • allkeys-random: 所有key通用; 随机删除一部分 key
  • volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key
  • volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key

使用场景

  • 如果分为热数据与冷数据, 推荐使用 allkeys-lru 策略;也就是, 其中一部分key经常被读写. 如果不确定具体的业务特征, 那么 allkeys-lru 是一个很好的选择
  • 如果需要循环读写所有的key, 或者各个key的访问频率差不多, 可以使用 allkeys-random 策略, 即读写所有元素的概率差不多
  • 假如要让 Redis 根据 TTL 来筛选需要删除的key, 请使用 volatile-ttl 策略

volatile-lru 和 volatile-random 策略主要应用场景是: 既有缓存,又有持久key的实例中。一般来说, 像这类场景, 应该使用两个单独的 Redis 实例

值得一提的是, 设置 expire 会消耗额外的内存, 所以使用 allkeys-lru 策略, 可以更高效地利用内存, 因为这样就可以不再设置过期时间了

淘汰的内部实现

淘汰过程可以这样理解:

  • 应用执行一个命令, 导致 Redis 中的数据增加,占用更多内存
  • Redis 检查内存使用量, 如果超出 maxmemory (redis.conf中配置)限制,根据策略清除部分 key
  • 继续执行下一条命令, 以此类推

在这个过程中, 内存使用量会不断地达到 limit 值, 然后超过, 然后删除部分 key, 使用量又下降到 limit 值之下

如果某个命令导致大量内存占用(比如通过新key保存一个很大的set), 在一段时间内, 可能内存的使用量会明显超过 maxmemory 限制

总结

本文讲的都是基本的过期策略和内存淘汰策略,但具体实现还是要看实际需求,具体问题具体分析