今天是个好天气 logo 今天是个好天气
  • Home
  • Go
  • MySQL
  • Redis
  • LeetCode
  • Hello World
↩️README 数据结构 Redis是怎么运行的 持久化 缓存过期与内存淘汰 集群模式 缓存 分布式锁 事务机制 Hot Key / Big Key

缓存过期删除和缓存淘汰

过期删除策略

Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。

如何设置过期时间

  • 常用的四种命令,设置秒、毫秒以及设置在时间戳时间过期。
  • 也可以在设置字符串时,同时对 key 设置过期时间。
  • 查询key剩余存活时间。
  • 取消过期时间

判定key已经过期

每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。

过期字典的key是指向某个键对象的指针,value是保存key的过期时间。

字典实际上是一个哈希表,能够在O1下查找。当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:

  • 如果不在,则正常读取键值;
  • 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。

过期删除策略

常见的三种过期删除策略:

  • 定时删除;
  • 惰性删除;
  • 定期删除;

定时删除

定时删除策略的做法是,在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。

定时删除策略的优点:

  • 可以保证过期 key 会被尽快删除,也就是内存可以被尽快地释放。因此,定时删除对内存是最友好的。

定时删除策略的缺点:

  • 在过期 key 比较多的情况下,删除过期 key 可能会占用相当一部分 CPU 时间,在内存不紧张但 CPU 时间紧张的情况下,将 CPU 时间用于删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。所以,定时删除策略对 CPU 不友好。

惰性删除

惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。

惰性删除策略的优点:

  • 因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。

惰性删除策略的缺点:

  • 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。

定期删除

每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。

定期删除策略的优点:

  • 通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。

定期删除策略的缺点:

  • 内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。
  • 难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。

Redis过期删除策略

Redis 选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。

惰性删除

Redis 在访问或者修改 key 之前,都会调用 expireIfNeeded 函数对其进行检查,检查 key 是否过期:

  • 如果过期,则删除该 key,至于选择异步删除,还是选择同步删除,根据 lazyfree_lazy_expire 参数配置决定(Redis 4.0版本开始提供参数),然后返回 null 客户端;
  • 如果没有过期,不做任何处理,然后返回正常的键值对给客户端;

定期删除

默认每秒进行 10 次过期检查一次数据库,每次抽查20个(写死)。

  1. 从过期字典中随机抽取 20 个 key;
  2. 检查这 20 个 key 是否过期,并删除已过期的 key;
  3. 如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。

Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。

内存淘汰策略

Redis 的运行内存已经超过 Redis 设置的最大内存之后,则会使用内存淘汰策略删除符合条件的 key,以此来保障 Redis 高效的运行。

Redis是基于内存存储的,在64位机器默认不会限制内存的使用,在 32 位操作系统中,maxmemory 的默认值是 3G。

实际中通过maxmemory来设置,超过这个配置值,会触发Redis内存淘汰。

当内存不足时,会腾出部分内存给新数据,因此Redis支持多种淘汰策略:

  • 不进行数据淘汰的策略,默认是noeviction,如果内存达到了maxmemory,写入操作会失败,但是不会淘汰已有的数据。

  • 针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。

    在设置了过期时间的数据中进行淘汰:

    • volatile-random:随机淘汰设置了过期时间的任意键值;
    • volatile-ttl:优先淘汰更早过期的键值。
    • volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
    • volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;

    在所有数据范围内进行淘汰:

    • allkeys-random:随机淘汰任意键值;
    • allkeys-lru:淘汰整个键值中最久未使用的键值;
    • allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。

实际当中根据业务选择淘汰算法:

  • 当数据比较重要,不能丢失,那么就选择不淘汰。但是这种情况会导致写入失败,需要完善的告警机制配合人工介入。
  • 如果是缓存场景,业务一般使用LRU/LFU灵活的粗略。

淘汰时机:

每次运行读写命令时,都会调用processCommand函数,其又会调用freeMemoryIfNeed,这时候会尝试去释放一定的内存,根据上述的策略。

LRU算法

LRU尝试回收最长时间未使用的数据。因此会记录每个Key的最近访问时间,维护一个访问时间的数据。

问题

  • 需要用链表管理所有的缓存数据,这会带来额外的空间开销;
  • 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。

近似LRU算法

redisObject对象中lru字段存储的是key被访问时Redis的时钟server.lruclock,当key被访问时,更新lru字段。

在redis当中缓存了Unix操作系统时钟,避免了每次调用函数获取。

近似LRU算法,采用随机采样的方法来淘汰元素,但内存不足时,执行一次近似LRU算法。

具体步骤是随机采样n个key,这个采样个数默认为5,然后根据时间戳淘汰掉最旧的那个key,如果淘汰后内存还是不足,就继续随机采样来淘汰。

采样范围:

  • allkeys-lru,所有key中
  • volatitle-lru,所有有过期时间的key

淘汰池优化:

近似LRU算法不能保证每次随机采样得到的结果,是全局真正最久未访问的,尤其在数据量大的情况下。

准备一个大小为16的候选池,池中根据访问时间进行排序。

  • 第一次随机选择的key都会放入池中,然后淘汰最久未被访问的。比如第一次选了5个,淘汰了1个,剩下4个,继续留在池子里。
  • 随后每次随机选取的key只有活性比池子里活性最小的key还小时才会放入池中,当池子装满 了,如果有新的key需要放入,则将池中活性最大的key移除。

这样能保证在不断加活性低的加入池中,活性高的移除。

  • 如果池子没满,不管活性大小,都会继续填充,没有位置了就挤走一个
  • 池子当中始终保持有序的
  • 最后淘汰的时候,固定从池子里淘汰一个活性最小的
  • 内存占用还超,继续重复操作。

6.2版本之后加了渐进式驱逐,超过一定时间会返回。

LFU算法

优先淘汰使用频率最低的数据。

使用LFU时,实际当中是复用了redisObject当中的lru字段。将其24位拆解为高16bit存储上一次访问的时间戳,低8bit存储访问次数。

这里访问时间精度是1分钟。如果上一次访问时间很久,访问次数就会衰减。

默认情况下新写的key的后8位计数器的值位5,防止因为访问频率过低被直接删除。

数据更新

  • 根据上次访问的时间间隔,减少访问次数。
  • 根据当前访问更新访问次数
    • 当前访问次数等于最大值 255(用了后八位记录访问次数)的情况。此时,LFULogIncr 函数不再增加访问次数。
    • 当前访问次数小于 255 的情况。此时,LFULogIncr 函数会计算一个阈值 p,以及一个取值为 0 到 1 之间的随机概率值 r。如果概率 r 小于阈值 p,那么 LFULogIncr 函数才会将访问次数加 1。否则的话,LFULogIncr 函数会返回当前的访问次数,不做更新。

这里r值是随机的,p值得设定决定了访问得次数增加的难度。

p的大小取决于当前访问次数和宏定义 LFU_INIT_VAL 的差值 baseval,另一个是 redis.conf 文件中定义的配置项 lfu-log-factor。

为什么使用一定概率增加:

  1. 避免热点数据占据过多的缓存空间,而其他非热门的数据很快淘汰出缓存。通过使用一定概率增加访问次数,可以让其他不那么热门但仍然有访问可能性的数据项有机会留在缓存中,提高整体缓存的命中率。
  2. 平衡不同数据项的访问次数,为不同的数据项提供更公平的缓存替换机会。
  3. 避免过度关注最近访问的数据项,可以更好地综合考虑历史访问情况和最近访问的权衡,更准确地反映数据项的访问频率。
  1. 过期删除策略
    1. 如何设置过期时间
    2. 判定key已经过期
    3. 过期删除策略
      1. 定时删除
      2. 惰性删除
      3. 定期删除
    4. Redis过期删除策略
      1. 惰性删除
      2. 定期删除
  2. 内存淘汰策略
    1. LRU算法
      1. 问题
      2. 近似LRU算法
    2. LFU算法
      1. 数据更新
Created by shixiaocaia | Powered by idoc
Think less and do more.