跳转到内容

4. 内存管理

内存管理主要是指两部分内容:

  1. 键的驱逐策略;
  2. 当内存不够用时,内存回收策略;

4.1. 键的驱逐策略(删除策略)

4.1.1. Redis 中键的过期时间的管理

Redis提供了四个命令来设置过期时间(生存时间)。

  1. EXPIRE <key> <ttl> :表示将键 key 的生存时间设置为 ttl 秒。
  2. PEXPIRE <key> <ttl> :表示将键 key 的生存时间设置为 ttl 毫秒。
  3. EXPIREAT <key> <timestamp> :表示将键 key 的生存时间设置为 timestamp 所指定的秒数时间戳。
  4. PEXPIREAT <key> <timestamp> :表示将键 key 的生存时间设置为 timestamp 所指定的毫秒数时间戳。

PS:在Redis内部实现中,前面三个设置过期时间的命令最后都会转换成最后一个 PEXPIREAT 命令来完成。

移除过期时间:

  • PERSIST <key> :表示将key的过期时间移除。

返回剩余生存时间:

  • TTL <key> :以秒的单位返回键 key 的剩余生存时间。
  • PTTL <key> :以毫秒的单位返回键 key 的剩余生存时间。

4.1.2. 删除带过期时间的 key 的通用做法

软件工程学中,删除带过期时间的 key 的通用做法主要有以下三种:

  1. 定时删除: 设置过期时间时,创建一个定时器,过期时间一到,操作系统自动触发定时器,立马执行删除方法。
    • 优点:定时删除对内存是最友好的,能够保证内存的key一旦过期就能立即从内存中删除。
    • 缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应时间和吞吐量造成影响。并且 key 很多时就需要很多的定时任务来完成,这个也是一个不小的性能开销;
  2. 惰性删除: 用到时,先判断一下是否过期了,没过期就允许操作,过期了就执行删除方法。
    • 特点:这种删除方式有两种实现思路
      • 同步删除: 即哪个线程用到,就由哪个线程来完成删除动作,这个过程是阻塞的;
      • 异步删除: 即碰到过期情况时,用到这个 key 的线程会通知另外的删除线程,让另外的删除线程来完成删除动作,之后直接返回过期的响应结果;
    • 优点:对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。
    • 缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放。从而造成内存泄漏。
  3. 定期删除: 并不是为每一个key都设置一个定时器,而是设置一个额外的定时器,这个定时器会定期扫描部分数据,然后删除过期的。
    1. 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。
    2. 缺点:难以确定删除操作执行的时长和频率。
      1. 如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。
      2. 如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。
      3. 另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。

4.1.3. redis 中的键的驱逐策略(删除策略)

eviction: 美: [ɪˈvɪkʃən], 英: [ɪˈvɪkʃ(ə)n], **n.**赶出;(租地,租房等的)收回;**网络释义:**驱逐;逐出;驱逐租客

Redis 中键的驱逐策略(删除策略)是组合使用的,即使用「惰性删除+定期删除」两种策略配合使用的方式。

  1. 键的过期的判断过程

    在Redis内部,每当我们设置一个键的过期时间时,Redis就会将该键带上过期时间存放到一个过期字典中。当我们查询一个键时,Redis便首先检查该键是否存在过期字典中,如果存在,那就获取其过期时间。然后将过期时间和当前系统时间进行比对,比系统时间大,那就没有过期;反之判定该键过期。

  2. 惰性删除

    惰性删除:Redis的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有键读写命令执行之前都会调用 expireIfNeeded 函数对其进行检查,如果过期,则删除该键,然后执行键不存在的操作;未过期则不作操作,继续执行原有的命令。

    大多情况下惰性删除都是阻塞式的,也就是说删除过程是由发起命令的线程来执行的,这就会出现另外一个问题,那就是有时候碰到大 key 时,可能会导致主线程秒级的阻塞,这对 redis 服务端的吞吐量是个巨大的影响;因此 redis 提供了另外一种删除方式:非阻塞式惰性删除,也可以称作异步删除,其大概原理是:在主线程的基础上再开启一个新线程来完成删除动作。

    惰性删除是默认开启的,但是有一些业务场景中并不需要惰性删除,而是需要立即删除,例如:

    1. 在为新的 key 分配内存空间时,发现内存空间不足以分配给新 key 了,那就不能够再使用惰性删除了,而是应该立刻删除;
    2. 使用 EXPIRE 命令让一个 key 过期时,也需要立刻删除这个 key;
    3. 再比如 RENAME 命令需要删除旧的键名,这个过程也是要立刻删除旧 key 的;
    4. 再比如,主从复制运行模式下,主节点执行了 flusdb 等清库的命令时,也是需要立刻同步到从库的;

    上面这四种情况下,是不需要惰性删除的,而是需要立即删除的,在配置文件中的LAZY FREEING部分,可以对这四种情况进行配置,使它们不使用惰性删除,no 表示不使用惰性删除:

    properties
    lazyfree-lazy-eviction no     # 表示键驱逐时,不使用惰性删除,而是直接删除;
    lazyfree-lazy-expire no      # 表示使用一些过期命令时,不使用惰性删除;
    lazyfree-lazy-server-del no    # 表示 当server发送一些删除式命令时,不使用惰性删除;【server和客户端是同一角色,它们都可以给存储引擎部分发送命令】
    replica-lazy-flush no       # 表示主从运行模式下不会使用惰性删除;
  3. 定期删除

    由redis.c/activeExpireCycle 函数实现,函数以一定的频率运行,每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。

    注意:并不是一次运行就检查所有的库,所有的键,而是随机检查一定数量的键。 定期删除函数的运行频率,在Redis2.6版本中,规定每秒运行10次,大概100ms运行一次。在Redis2.8版本后,可以通过修改配置文件redis.conf 的 hz 选项来调整这个次数。

    看上面对这个参数的解释,建议不要将这个值设置超过 100,否则会对CPU造成比较大的压力。

  4. 键的驱逐策略的问题

    我们看到,通过过期删除策略,对于某些永远使用不到的键,并且多次定期删除也没选定到并删除,那么这些键同样会一直驻留在内存中,又或者在Redis中存入了大量的键,这些操作可能会导致Redis内存不够用,这时候就需要Redis的内存淘汰策略了。

4.2. 内存回收机制

我们看到,通过过期删除策略,对于某些永远使用不到的键,并且多次定期删除也没选定到并删除,那么这些键同样会一直驻留在内存中,又或者在Redis中存入了大量的键,这些操作可能会导致Redis内存不够用,这时候就需要Redis的内存淘汰策略了。

这块对应于配置文件redis.conf 中的MEMORY MANAGEMENT部分。

  • maxmemory:
    • 描述:设置 Redis 使用的最大内存量。当 Redis 达到这个内存限制时,会触发内存淘汰策略。
    • 语法:maxmemory <bytes>
    • 示例:maxmemory 256mb
  • maxmemory-policy:
    • 描述:设置 Redis 达到内存限制后,使用哪种内存淘汰策略。可能的值包括:
      • volatile-lru:从设置了过期时间的键值对集合中,淘汰 「最久未使用」的;
      • allkeys-lru:从所有键值对集合中,淘汰「最久未使用」的;
      • volatile-lfu:从设置了过期时间的键值对集合中,淘汰「最不常用到」的;
      • allkeys-lfu:从所有键值对集合中,淘汰「最不常用到」的;
      • volatile-random:从设置了过期时间的键值对集合中,「随机」淘汰;
      • allkeys-random:从所有键值对集合中,「随机」淘汰;
      • volatile-ttl:从设置了过期时间的键值对集合中,淘汰「即将过期」的;
      • noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;redis只响应读命令,对需要申请内存的写命令回复error;
    • 语法:maxmemory-policy <policy>
    • 示例:maxmemory-policy allkeys-lru
  • maxmemory-samples:
    • 描述:设置 LRU 算法和最少频率使用(LFU)算法进行采样时的样本大小。较大的样本大小能提高精确度,但会增加 CPU 开销。
    • 语法:maxmemory-samples <number>
    • 示例:maxmemory-samples 5
  • lfu-log-factor:
    • 描述:设置 LFU 算法的对数因子,控制访问频率计数的增加速度。较高的值会使计数增加速度变慢。
    • 语法:lfu-log-factor <factor>
    • 示例:lfu-log-factor 10
  • lfu-decay-time:
    • 描述:设置 LFU 算法的衰减时间,以分钟为单位。计数会每隔这个时间减少一半。
    • 语法:lfu-decay-time <minutes>
    • 示例:lfu-decay-time 1

PS:

  1. volatile: 美: [ˈvɑlət(ə)l], 英: [ˈvɒlətaɪl], adj. ,易变的;无定性的;无常性的;可能急剧波动的; 网络释义:挥发性;挥发性的;不稳定的。这里表示设置了过期时间的 key
  2. 其中 allkeys-xxx 表示: 从所有的键值中淘汰数据;而 volatile-xxx 表示: 从设置了过期键的键值中淘汰数据。
  3. 淘汰时要从圈定的 key 的集合中,选择一部分进行淘汰, 这个选择有三种算法:
    1. LRULeast Recently Used): 最近最少使用的、最久没有使用的;这种算法有一个缺点,比如说很久没有使用的一个键值,如果最近被访问了一次,那么它就不会被淘汰,即使它是使用次数最少的缓存,那它也不会被淘汰。
    2. LFULeast Frequently Used): 使用频率最小的、最不常使用到的;这种算法弥补了 LRU 的弊端;
    3. random:随机的;

make it come true