本文已参与「新人创作礼」活动,一起开启掘金创作之路。
高效利用 Redis 内存 :
- 内存消耗在哪里?
- ✔️内存怎么管理?
- 如何优化内存使用?
内存管理 - 控制内存上限
Redis 主要通过控制内存上限和内存回收策略实现内存管理.
设置内存上限
Redis 默认无限使用服务器内存, 为防止极端情况下导致系统内存耗尽, 建议所有的 Redis 进程都要在 redis.conf 中配置 maxmemory 设置 Redis 可使用的最大内存大小.
注意, maxmemory 限制的是 Redis 存储数据使用的内存量, 也就是 used_memory 参数对应的内存. 由于内存碎片率的存在, 实际消耗的内存可能会比 maxmemory 设置的更大, 使用时要小心这部分内存溢出.
Redis 限制最大可使用内存的目的:
- 防止所用内存超过服务器物理内存.
- 用于缓存场景, 当超出内存上限 maxmemory 时, 使用 LRU 等删除策略释放空间.
- 可以实现一台服务器部署多个 Redis 进程的内存控制. 比如一台 24GB 内存的服务器, 为系统预留 4GB 内存, 预留 4GB 空闲内存给其他进程或 Redis fork 进程, 留给 Redis 16GB 内存, 这样可以部署 4 个 maxmemory=4GB 的 Redis 进程. 得益于 Redis 单线程架构和内存限制机制, 即使没有采用虚拟化, 不同的 Redis 进程之间也可以很好地实现 CPU 和内存的隔离性.
动态调整内存上限
在保证物理内存可用的情况下, Redis 支持通过修改 maxmemory 参数, 实现自由伸缩内存.
Redis 的内存上限可以通过 config set maxmemory 进行动态修改.
例如, 当发现 Redis-2 没有做好内存预估, 实际只用了不到 2GB 内存, 而 Redis-1 实例需要扩容到 6GB 内存才够用, 这时可以分别执行如下命令进行调整:
Redis-1>config set maxmemory 6GB
Redis-2>config set maxmemory 2GB
当然了这个例子过于理想化, 如果此时 Redis-3 和 Redis-4 实例也需要分别扩容到 6GB, 这时超出系统物理内存限制就不能简单的通过调整 maxmemory 来达到扩容的目的, 需要采用在线迁移数据或者通过复制切换服务器来扩容.
内存管理 - 内存回收策略
Redis 的内存回收机制主要通过两种方式:
删除过期的键.- 内存使用达到 maxmemory 上限时, 触发
内存溢出控制策略.
删除过期键
主键失效机制
作为缓存系统都要定期清理无效数据, 就需要一个主键失效和淘汰策略.
在 Redis 中, 有生存期的 key 被称为 volatile. 如果没有设置时间, 那 key 就是永不过期. 当 key 过期的时候 ( 生存期为 0 ), 它将会被自动删除. 这就是主键失效机制.
影响键的生存时间的一些操作
- 使用 DEL 命令, 删除整个 key, 可以移除其失效时间.
- 在一个设置了失效时间的 key 被更新覆盖时. 该 key 的失效时间也会被撤销. 注意 : 这里所说的是 key 被更新覆盖, 而不是 key 对应的 value 被更新覆盖. 因此 SET, MSET 或者是 GETSET 可能会导致 key 被更新覆盖, 影响失效时间. 而像 INCR, DECR, LPUSH, HSET 等都是更新主键对应的值, 这类操作是不会影响 key 的失效时间的.
- 使用 RENAME 对一个 key 进行改名, 不影响其生存时间. 但是如果一个主键是被 RENAME 所覆盖的话(如主键 hello 可能会被命令 RENAME world hello 所覆盖), 这时被覆盖主键的失效时间会被自动撤销, 而新的主键则继续保持原来主键的特性.
- 使用 PERSIST 命令, 移除 key 的生存时间, 可以让 key 成为一个 永不过期的 key.
三种过期删除策略
(1) 定时删除
定时删除:在设置 key 的过期时间的同时, 为该 key 创建一个内部定时器, 让定时器在 key 的过期时间到来时, 对 key 进行主动删除.
优点:能保证内存中数据的最大新鲜度, 保证内存被尽快释放. 内存友好
缺点:若过期 key 很多, 删除这些 key 会占用很多的 CPU 时间. CPU不友好
定时器的创建耗时, 若为每一个设置过期时间的 key 创建一个定时器, 将会有大量的定时器产生, 性能影响严重.
(2) 惰性删除
惰性删除:不主动去删除过期的 key, 只有每次从内存中获取 key 的时候, 才会去检查是否过期, 若过期, 则将其删除, 然后返回 nil.
优点:删除操作只会在从内存中取出 key 的时候发生, 而且只删除当前 key, 所以对 CPU 时间的占用是比较少的. ( 此时的删除是必须的, 否则会返回过期数据. ) CPU 友好
缺点:若大量的 key 在超出超时时间后, 很久一段时间内都没有被获取过, 将要占用大量的内存, 那么可能发生内存泄露. 内存不友好
启发 : key 到期后, 如果不放心 Redis 是否成功删除了该 key, 可以在 key 到期后再次访问一下 key 就可以确保 Redis 一定会删除该过期键.
(3) 定期删除
定期删除:定时删除会短时间内占用大量 cpu, 惰性删除会在一段时间内浪费内存, 而定期删除是一个折中的办法. 每隔一段时间执行一次删除过期 key 的操作.
优点: 通过限制删除操作的时长和频率, 可以减少删除操作对 CPU 时间的占用. ( 弥补 "定时删除" 的缺点 ), 定期删除过期 key ( 弥补 "惰性删除" 的缺点 )
缺点 : 在内存友好方面, 不如定时删除. 在 CPU 时间友好方面, 不如惰性删除.
难点 : 如何合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除, 定期删除的时间间隔默认为 100ms.).
对于定期删除, 在程序中有一个全局变量 current_db 来记录下一个将要遍历的库, 假设有 16 个库, 我们这一次定期删除遍历了 10 个, 那此时的 current_db 就是 11, 下一次定期删除就从第 11 个库开始遍历, 假设 current_db 等于 15 了, 那么之后遍历就再从 0 号库开始, 即此时 current_db 是 0.
(4) Redis 采用的删除策略
Redis 所有的键都可以设置过期属性, 内部保存在过期字典中. 由于进程内保存大量的键, 维护每个键精准的过期删除机制会导致消耗大量的 CPU, 对于单线程的 Redis 来说成本过高, 因此 Redis 采用惰性删除和定期删除实现过期键的内存回收.
具体流程如下:
- 惰性删除流程 : 在进行 get 或 setnx 等操作时, 先检查 key 是否过期. 若过期, 删除 key, 然后执行相应操作. 返回 nil. 若没过期, 直接执行相应操作.
- 定期删除流程(简单而言, 对指定个数个库的每一个库随机删除小于等于指定个数的过期 key)
- 遍历每个数据库, 检查当前库中的指定数量个 key(默认是每个库检查 20 个 key, 注意相当于该循环执行 20 次, 循环体是下边的描述)
- 如果当前库中所有 key 都没有设置过期时间, 直接执行下一个库的遍历. 否则随机获取一个设置了过期时间的 key, 检查该 key 是否过期, 如果过期, 删除该 key.
- 判断定期删除操作是否已经达到指定时长, 若已经达到, 直接退出定期删除.
采用定期删除+惰性删除就没其他问题了么?
- 不是的, 也有可能存在
漏网之鱼, 定期删除没有删除掉该 key. 也没及时去请求这个 key, 即惰性删除也没生效. 这样, Redis 占用的内存会越来越高. 那么就会触发内存淘汰机制. - 所以, 我们应该注意的是,
Redis 的键过期机制不能百分百的保证过期的键一定会被立马删除. 在键实际过期之后不一定会被立马删除, 可能会继续存留, 具体存留的时间可能是 1~2 分钟, 可能会更久.
内存溢出控制策略
当 Redis 所用内存达到 maxmemory 上限时就会触发相应的溢出策略. ( 比如 Redis 只能存 5G 数据, 可是写了 5G 数据后,还继续往里面写入数据,Redis 要怎么办呢?是拒绝写入?还是删除数据?怎么删?)
具体溢出策略受 maxmemory-policy 参数控制, 如下 :
maxmemory 1000M #设置最大使用内存大小
maxmemory-policy volatile-lru #配置内存淘汰策略
Redis 支持 6 种策略, 如下:
allkeys-lru:当内存不足时, 移除最近最少使用的 key.推荐使用. ( 把 Redis 变为纯缓存服务器使用 )volatile-lru:当内存不足时, 在设置了过期时间的 key 中, 移除最近最少使用的 key. ( 把 Redis 既当缓存, 又做持久化存储的时候才用.推荐)- no-eviction:当内存不足时, 新写入操作会报错. 应该没人用吧. ( 拒绝写入 )
- allkeys-random:当内存不足时, 随机移除某个 key. 应该也没人用吧, 不删最少使用 Key, 去随机删?.
- volatile-random:当内存不足时, 在设置了过期时间的 key 中, 随机移除某个 key. 不推荐
- volatile-ttl:当内存不足时, 在设置了过期时间的 key 中, 有更早过期时间的 key 优先移除. 不推荐
当 Redis 因为内存溢出而删除 key 时, info stats命令的evicted_keys是当前 Redis 已剔除的 key 的数量.
每次 Redis 执行命令时, 如果设置了 maxmemory 参数, 都会尝试执行回收内存操作. 当 Redis 一直工作在内存溢出(used_memory > maxmemory)的状态下且设置非 no-eviction 策略时, 会频繁地触发回收内存的操作, 影响 Redis 服务器的性能. 频繁执行回收内存成本很高, 主要包括查找可回收键和删除键的开销, 如果当前 Redis 有从节点, 回收内存操作对应的删除命令会同步到从节点, 导致写放大的问题. 建议线上 Redis 内存工作在 maxmemory > used_memory 状态下, 避免频繁内存回收开销.
对于需要收缩 Redis 内存的场景, 可以通过调小 maxmemory 来实现快速回收. 比如对一个实际占用 6GB 内存的进程设置 maxmemory=4GB, 之后第一次执行命令时, 如果使用非 no-eviction 策略, 它会一次性回收到 maxmemory 指定的内存量, 从而达到快速回收内存的目的. 注意, 此操作会导致数据丢失和短暂的阻塞问题, 一般在缓存场景下使用.