Redis运行原理-过期删除(上篇)

文摘   2024-07-31 22:07   上海  
一、引言

在业务系统中,Redis作为一个高性能的内存数据库,广泛应用于缓存、会话存储、实时分析、消息队列等多个领域。我们经常需要删除过期的key,以释放内存空间并保持数据的有效性,然而,若未能及时删除过期key,可能会导致Redis内存使用率偏高,甚至内存用满,影响系统的性能与稳定性。本文将从Redis的运行原理出发,深入探讨过期删除机制,并提供全面的优化方案。

二、Redis过期删除机制

Redis的过期删除机制主要有三种:惰性删除、定期删除和主动删除。

  • 请求时清除(被动删除)

    读写(scan、get)已过期 key 时,触发惰性删除策略,直接删除这个过期 key

  • 定期删除(主动删除)

    惰性删除无法保证冷数据及时清理,redis 会定期主动淘汰部分已过期 key,默认每 100ms 一次。因为只是淘汰部分已过期 key,会出现部分 key 过期未被清理情况,导致内存并未释放

  • 主动清理策略(主动删除)

  • 当内存使用超过 maxmemory 限定时,触发主动清理策略

  • lazy_free(异步线程)

    主线程根据性能损耗,将待删除 key 扔到 lazy_free 队列,并唤醒对应后台线程

为了方便找出已过期key,redis 用了额外字典专门存储带 expire 过期时间的key,这样一来,只需遍历该字典即可找出过期的 key。

2.1 定期删除(主动删除)

redis 提供了两种删除策略用于定期删除:
  • ACTIVE_EXPIRE_CYCLE_FAST:快处理。主事件循环中,由 server.c#beforeSleep 触发
  • ACTIVE_EXPIRE_CYCLE_SLOW:慢处理。由时间事件控制进行 key 删除(server.c#serverCron),处理频率和 server.hz 有关,频率越快,两次执行间隔越短。

beforeSleep 和 serverCron 都是触发定期删除的入口,负责实际删除的是server.c#activeExpireCycle 函数,其工作模式如下:
  • 函数每次运行时,从数据库随机选取键检查(轮询字典表),并删除其中过期键。

  • 全局变量 current_db 会记录当前 activeExpireCycle 函数检查进度

  • 随着 activeExpireCycle 函数不断执行,服务器中的所有数据库会被检查一遍

由于是随机选择,可能存在一些过期 key 长时间未被清除,为了避免这种情况,在 redis 6.0 及之后版本,改成顺序遍历字典表的方式,同时会记录下标。

2.2 主动清理策略(主动删除)
如果Redis内存达到使用上限maxmemory还继续向Redis添加key的话,会触发Redis的内存淘汰机制。在 redis 4.0 之前一共实现了 6 种内存淘汰算法,4.0 之后,又增加了 2 ,共 8 种。可以按照针对 key 是否设置过期时间分为两大类:

针对过期 key

  • volatile-ttl: 根据过期时间进行清理,越早过期越先删除

  • volatile-random: 随机选择删除

  • volatile-lru: 会使用 lru 算法淘汰很久没被访问数据

  • volatile-lfu: 会使用 lfu 算法淘汰最近时间访问次数最少数据

针对过期 key
  • allkeys-random: 从所有的键值对中随机选择并删除

  • allkeys-lru: 从所有的键值对中使用 lru 算法选择并删除

  • allkeys-lfu: 从所有的键值对中使用 lfu 算法选择并删除

不处理

  • noeviction:不删除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时 Redis 只响应读操作。

  1. 当存在热点数据时,LRU 效率很好。

  2. 偶发性的、周期性的批量操作会导致 LRU 命中率急剧下降,缓存污染情况比较严重,这时使用 LFU 可能更好点。

  3. 根据自身业务类型,配置好 maxmemory-policy(默认是 noeviction),推荐使用 volatile-lru。如果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降。

2.3 lazy_free(异步线程)

在 redis 实现中,会评估具体命令的损耗来判断究竟是选择立即处理还是延迟处理

#define LAZYFREE_THRESHOLD 64


惰性删除包含三步操作:

  • 调用 dictDelete() 从过期表中删除数据,从过期表中删除数据只会减小引用计数,不会真正删除数据;

  • 调用 dictUnlink() 从全局表中删除数据,该函数只执行步骤一,不执行步骤二,即不会释放待删除数据的占用内存;

  • 调用 lazyfreeGetFreeEffort() 评估释放内存开销,如果开销超过门限值 LAZYFREE_THRESHOLD(默认 64),则通过 bioCreateBackgroundJob() 创建一个新的删除任务,由后台线程来执行内存释放;如果开销较小,则直接调用 dictFreeUnlinkedEntry() 函数执行内存释放。

可以看到,即使开启了惰性删除,在实际执行过程中,redis 也会先评估删除数据的开销,然后再决定是执行异步删除还是同步删除

redis通过 lazyfreeGetFreeEffort() 函数评估删除开销

比如,对于 键值对类型属于List、Hash、Set 和 Sorted Set 等等,元素个数超过 LAZYFREE_THRESHOLD 阀值才会使用 lazy_free,不属于以上情况的删除开销就等于1。也就是说,即使开启了惰性删除,针对 String(不管内存占用多大)、List(少量元素)、Set(int 编码存储)、Hash/ZSet(ziplist 编码存储)这些情况下的 key,在对删除数据做开销评估时因为小于门限值,依旧会在主线程中进行同步删除。

可以看出,从 DB 删除过期 key 时,只是从 DB 字典中将关系删除,内存没有真正释放,而是交给后台异步线程去处理。

主线程将待删除 key 扔到 lazy_free 队列,并唤醒对应后台线程,此时 bio_lazy_free 后台线程就从队列中取出对应 key 进行内存清理。这是典型 生产者 - 消费者 模型。

其实,只要是删除 key 场景,都可考虑使用lazy_free线程删除,redis 删除场景有:

// 当 redis 内存达到阈值 maxmemory 时,将执行内存淘汰lazyfree-lazy-eviction no// 过期 key 自动删除lazyfree-lazy-expire no// 用户提交 del 删除指令lazyfree-lazy-server-del no// 主要用于复制过程中,全量同步的场景,从节点需要删除整个 dbreplica-lazy-flush no// del 指令删除关系,不释放内存, 功能同unLinklazyfree-lazy-user-del no

三、内存碎片整理
进入Redis命令行界面,使用 info memory 命令(集群使用 cluster info 命令) 即可查看当前Redis相关内存信息,部分信息展示如下:
127.0.0.1:6379> info memory
# Redis 保存数据申请的内存空间used_memory:9469412118used_memory_human:8.82G
# 操作系统分配给 Redis 进程的内存空间used_memory_rss:11351138316used_memory_rss_human:10.57G
# Redis 进程在运行过程中占用的内存峰值used_memory_peak:12618222522used_memory_peak_human:11.75G
# 内存碎片率,used_memory_rss / used_memorymem_fragmentation_ratio:1.20
# Redis 最大可用内存,0表示不限制maxmemory:0maxmemory_human:0B
# 内存分配器mem_allocator:jemalloc-5.1.0

mem_fragmentation_ratio:内存碎片率,used_memory_rss / used_memory。大于1的部分为redis碎片占用大小,建议值大于 1 但小于 1.5,大于1.5说明碎片过多需要清理了。

需要注意的是,通常情况下 used_memory_rss 是大于 used_memory 的;但也有例外,当used_memory_rss 小于 used_memory 时,说明操作系统分配给Redis进程的数据,不足以满足实际存储数据的需求,此时Redis部分内存数据会转换到Swap中,随之引发的问题是,当Redis访问Swap中的数据时,性能会下降 。

内存碎片整理开关(需同时满足才执行):

  • activedefrag:内存碎片整理总开关,开启后才有可能执行碎片整理

  • active-defrag-ignore-bytes:内存碎片到此阀值(默认100MB)时,允许整理;

  • active-defrag-threshold-lower:内存碎片空间占操作系统分配给 Redis 的总空间比例达到此阀值(默认10%)时,允许整理;

此外,还有几个参数用于控制内存碎片整理的力度:

  • active-defrag-cycle-min:清理内存碎片占用 CPU 时间的比例不低于此阀值(默认5%),保证清理能正常开展;

  • active-defrag-cycle-max:清理内存碎片占用CPU 时间的比例不高于此阀值(默认75%),一旦超过则停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致其他请求延迟。

  • activedefrag:自动整理内存碎片,其原理是通过scan迭代整个Redis数据,通过一系列的内存复制、转移操作完成内存碎片整理,由于此操作使用的是主线程,故会影响Redis对其他请求的响应。
四、优化方案
依赖redis惰性删除hz参数控制定时删除拉取的数据量:
线上性能需要观测调优
定期scan:
当大量key过期时,redis过期机制无法及时释放内存(触发大量释放内存会导致线程阻塞,低频释放内存不足以及时清理过期key),需要在请求低谷触发请求时删除。
待确认问题
  • hz参数是否生效,以及对服务影响
  • redis高版本有没有修复问题
    欢迎关注公众号:DBA札记,一起交流数据库技术。欢迎觉得读完本文有收获,可以转发给其他朋友,大家一起学习进步!谢谢大家。

DBA札记
dba 数据库 知识科普 踩坑指南 经验分享 原理解读
 最新文章