Skip to content

Latest commit

 

History

History
134 lines (93 loc) · 10.1 KB

using-redis-as-an-lru-cache.md

File metadata and controls

134 lines (93 loc) · 10.1 KB

Redis 做为 LRU 缓存

目录

当 Redis 用作缓存时, 当你添加新数据时它通常会将旧数据进行自动回收处理. 这个行为在社区开发者中是众所周知的, 因为这是当下流行的内存缓存系统默认的行为.

LRU 实际上只是支持的回收算法之一. 本页涉及更多关于 Redis maxmemory 指令的普通话题, 这个指令限制了最大使用内存, 然后也会涉及 Redis 所使用的 LRU algorithm 的内部细节, 实际上是近似于真正的 LRU.

从 Redis 4.0 版本开始, 引入了 LFU (最近频繁使用) 这个新的回收策略. 文档中有单独的章节会涉及到.

最大内存配置指令

maxmemory 配置指令用于配置 Redis 数据集的限定内存使用大小. 可以在 redis.conf 配置文件中配置, 或者过后使用 CONFIG SET 命令在运行时进行配置也可以.

例如限制内存为 100 MB, 在 redis.conf 配置文件中使用以下指令.

maxmemory 100mb

设置 maxmemory 为 0 的话表示不限制内存. 64 位操作系统上默认设置为 0, 而 32 位操作系统上可以显示地指定为 3GB.

当达到限定的内存大小时, 可以在不同行为之间进行选择, 也就是所谓的策略. 内存不够用时 Redis 只是在执行命令时返回错误, 或者每次新增数据时回收一些旧数据.

回收策略

当内存达到限制值时 Redis 的实际行为会遵循 maxmemory-policy 配置命令的配置项. 有以下这些策略可以使用:

  • noeviction: 客户端尝试执行会导致需要使用更多内存的命令时(大部分的写命令, 除了 DEL 命令以及其他异常), 这个时候如果内存达到限制值则会返回错误.
  • allkeys-lru: 首先通过尝试移除最近最少使用的 (LRU) keys 进行 keys 回收, 为新增数据腾出空间.
  • volatile-lru: 首先通过尝试移除最近最少使用的 (LRU) keys 进行 keys 回收, 但仅在那些有过期的 set 的 keys 之间进行, 为新增数据腾出空间.
  • allkeys-random: 对 keys 进行随机回收然后为新增数据腾出空间.
  • volatile-random: 对 keys 进行随机回收然后为新增数据腾出空间, 但仅在那些有过期的 set 的 keys 之间进行.
  • volatile-ttl: 在那些有过期的 set 的 keys 之间进行回收, 然后首先尝试回收那些存活时间(TTL)较短的 keys, 为新增数据腾出空间.

Volatile-lru, volatile-random 以及 volatile-ttl 这个三个策略在没有 keys 可以回收的前提下所表现的行为是跟 noeviction 这个策略类似的.

根据应用的访问方式来选择正确的回收策略是很重要的, 不过你可以在应用运行的时候对策略进行重新配置, 然后使用 Redis INFO 输出来监控缓存击穿和命中的次数进行配置调优.

一般可以这么做:

  • 使用 allkeys-lru 策略, 如果你期望大部分的请求按幂次定律分布的话, 也就是说, 你期望元素的子集比个别的更经常被访问. 如果你不是很有把握的话这会是一个好的选择.
  • 使用 allkeys-random 策略, 如果你有周期性循环地持续扫描所有的 keys 的情况, 或者你期望均匀分布 (所有元素被访问的概率可能是一样的).
  • 使用 volatile-ttl 策略, 如果你想要在创建缓存对象时, 通过使用不同的 TTL 值, 然后可以提供合适的关于过期的候选方案的建议给 Redis 的话.

Volatile-lru 和 volatile-random 这两个策略主要用于既做为缓存同时又有一组持久化的 keys 的单一实例的场景. 不过通常更好的是运行两个 Redis 实例来解决这个问题.

还有值得注意的一点是设置 key 的过期时间需要消耗内存, 所以可以使用像 allkeys-lru 这样高效使用内存的策略, 因为不需要迫于内存压力为 key 设置过期时间 来进行回收.

回收进程是如何工作的

了解回收进程是如何像下面这样工作的是必要的:

  • 客户端运行一个新的命令, 导致数据增加.
  • Redis 检查内存使用情况, 然后如果超出 maxmemory 的限制值 , 它会根据策略回收 keys.
  • 一个新的命令执行完毕, 诸如此类的.

这样我们反复超出内存的限制, 通过比对这个值, 然后通过回收 keys 回到这个限制以下.

如果一个命令导致内存大量使用的话 (例如对一个大的 set 取交集后存到到一个新的 key 中) 有时内存限制会被明显超过.

近似的最近最少访问算法

Redis LRU 算法算不上真正的实现. 这意味着 Redis 无法获得回收的最优解, 也就是说, 这些访问是在过去一段时间内的访问最多的. 换句话说, 会执行一个近似的 LRU 算法, 通过对小部分的 keys 进行采样, 然后回收采样的 keys 中最优的一个 (访问时间最早的).

然而从 Redis 3.0 开始这个算法有提升, 可以进行批量回收. 这次算法性能的提升, 使得它的行为更接近真正的 LRU 算法.

关于 Redis LRU 算法的关键是你可以测试算法的精度, 每次回收可以通过修改样本数量来检查. 这个参数通过以下配置指令控制:

maxmemory-samples 5

Redis 不使用正真的 LRU 实现的原因是它会消耗更多内存. 然而对于使用 Redis 的应用来说这个近似是等价的. 下图是 Redis 使用近似的 LRU 和 使用真正的 LRU 的一个对比.

测试时, 给定一定数量的 keys 写入到 Redis 服务器来生成上图. 从第一个到最后一个访问这些 keys, 这样的话, 对于使用 LRU 算法的回收策略来说第一个 keys 是最好的候选者. 为了强制回收半数的旧的 keys, 随后加入大于 50% 的 keys.

从三个不同的带状图, 你可以看到三种类型的点.

  • 浅灰带是被回收的对象.
  • 灰带是没有被回收的对象.
  • 绿带是增加的对象.

正如我们所期望的理论上的 LRU 实现, 在旧的 keys 中, 第一个半数的 keys 会被回收. 相反 Redis LRU 算法只会概率性地回收过期的比较旧的 keys.

正如你所见, 在样本数为 5 时 Redis 3.0 比 Redis 2.8 做得好, 然而最近访问的大部分对象依然被 Redis 2.8 保留. 样本数为 10 时 Redis 3.0 非常近似 Redis 3.0 理论上的性能.

需要注意的是 LRU 只是一个预测一个给定的 key 未来有多大可能性会被访问到的模型. 而且, 如果你的数据访问方式很接近幂次定律的话, 大部分访问将会落在 LRU 近似算法会很好处理的 keys 集合中.

通过使用幂次定律方式进行模拟访问我们发现, the difference between 真正的 LRU 和 Redis 近似的 LRU 之间的差别是很小的或者不存在.

不过你可以增大样本数到 10 来近似真正的 LRU, 只是需要消耗额外的 CPU, 然后确认在缓存击穿率上是否不一样.

在生产环境上使用不同的样本数进行试验可以通过使用 CONFIG SET maxmemory-samples [count] 这个命令, 非常简单.

新模式-最不经常访问

从 Redis 4.0 开始, 有个 最不经常访问回收模式 可以使用. 这个模式在某些场景下可能会工作的更好 (提供一个更好的 命中/击穿 率), 由于使用 LFU Redis 将会记录 items 的访问频率, 这样的话, 很少使用的那些会被回收, 相比之下那些经常使用的更有机会留在内存中.

如果你想想看 LRU, 一个最近访问过的但是实际几乎没有请求过的 item, 则不会过期, 这样的风险在于回收将来更有机会被请求的 key. LFU 没有这个问题, 而且 一般能够更好地适配不同的访问方式.

那些采样信息和 LRU 的结果一样 (正如本文档前面一节所解释的), 是为了选择一个可以回收的候选者.

然而和 LRU 不同, LFU 有一些可调参数: 例如, 一个频繁访问的 item 可以多快降到低级别, 如果它不再访问的话? 为了更好的将算法适配于特定场景还可以 微调 Morris 计数器范围.

Redis 4.0 默认配置是:

  • 大约100万个请求可以达到计数器饱和值.
  • 计数器按分钟递减.

这些值应该是合理的,并且经过实验测试,但用户可能希望使用这些配置来选择最佳值.

关于如何微调这些参数的介绍可以在发布的源码中的 redis.conf 示例配置文件找到, 但是简而言之, 也就是:

lfu-log-factor 10
lfu-decay-time 1

递减时间是效果显而易见的一个参数, 它是计数器递减的间隔分钟数, 当采样后然后发现当前值比原来的旧时. 特殊值 0 表示: 每次扫描时计数器总是递减, 不过基本不会用到.

为了填满频率计数器, 计数器的对数因子的变化决定了需要命中的次数, 范围在 0-255 之间. 因子越大, 就需要更多的访问次数来达到最大值. 因子越小, 对于低频访问的情况计数器的分辨率就越好, 通过以下表格可以看出来:

+--------+------------+------------+------------+------------+------------+
| factor | 100 hits   | 1000 hits  | 100K hits  | 1M hits    | 10M hits   |
+--------+------------+------------+------------+------------+------------+
| 0      | 104        | 255        | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 1      | 18         | 49         | 255        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 10     | 10         | 18         | 142        | 255        | 255        |
+--------+------------+------------+------------+------------+------------+
| 100    | 8          | 11         | 49         | 143        | 255        |
+--------+------------+------------+------------+------------+------------+

因此基本因素就是更好区分的但是低频访问的 items 和可以区分的但是高频访问的 items 之间的一个权衡. 更多信息 redis.conf 示例配置文件的注释中可以找到.