在业务系统中,Redis作为一个高性能的内存数据库,广泛应用于缓存、会话存储、实时分析、消息队列等多个领域。我们经常需要删除过期的key,以释放内存空间并保持数据的有效性,然而,若未能及时删除过期key,可能会导致Redis内存使用率偏高,甚至内存用满,影响系统的性能与稳定性。本文将从Redis的运行原理出发,深入探讨过期删除机制,并提供全面的优化方案。
Redis的过期删除机制主要有三种:惰性删除、定期删除和主动删除。
请求时清除(被动删除)
读写(scan、get)已过期 key 时,触发惰性删除策略,直接删除这个过期 key
定期删除(主动删除)
惰性删除无法保证冷数据及时清理,redis 会定期主动淘汰部分已过期 key,默认每 100ms 一次。因为只是淘汰部分已过期 key,会出现部分 key 过期未被清理情况,导致内存并未释放
主动清理策略(主动删除)
lazy_free(异步线程)
当内存使用超过 maxmemory 限定时,触发主动清理策略
主线程根据性能损耗,将待删除 key 扔到 lazy_free 队列,并唤醒对应后台线程
为了方便找出已过期key,redis 用了额外字典专门存储带 expire 过期时间的key,这样一来,只需遍历该字典即可找出过期的 key。
ACTIVE_EXPIRE_CYCLE_FAST:快处理。主事件循环中,由 server.c#beforeSleep 触发 ACTIVE_EXPIRE_CYCLE_SLOW:慢处理。由时间事件控制进行 key 删除(server.c#serverCron),处理频率和 server.hz 有关,频率越快,两次执行间隔越短。
函数每次运行时,从数据库随机选取键检查(轮询字典表),并删除其中过期键。
全局变量 current_db 会记录当前 activeExpireCycle 函数检查进度
随着 activeExpireCycle 函数不断执行,服务器中的所有数据库会被检查一遍
由于是随机选择,可能存在一些过期 key 长时间未被清除,为了避免这种情况,在 redis 6.0 及之后版本,改成顺序遍历字典表的方式,同时会记录下标。
针对过期 key
volatile-ttl: 根据过期时间进行清理,越早过期越先删除
volatile-random: 随机选择删除
volatile-lru: 会使用 lru 算法淘汰很久没被访问数据
volatile-lfu: 会使用 lfu 算法淘汰最近时间访问次数最少数据
allkeys-random: 从所有的键值对中随机选择并删除
allkeys-lru: 从所有的键值对中使用 lru 算法选择并删除
allkeys-lfu: 从所有的键值对中使用 lfu 算法选择并删除
不处理
noeviction:不删除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时 Redis 只响应读操作。
当存在热点数据时,LRU 效率很好。
偶发性的、周期性的批量操作会导致 LRU 命中率急剧下降,缓存污染情况比较严重,这时使用 LFU 可能更好点。
根据自身业务类型,配置好 maxmemory-policy(默认是 noeviction),推荐使用 volatile-lru。如果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降。
在 redis 实现中,会评估具体命令的损耗来判断究竟是选择立即处理还是延迟处理。
惰性删除包含三步操作:
调用 dictDelete() 从过期表中删除数据,从过期表中删除数据只会减小引用计数,不会真正删除数据;
调用 dictUnlink() 从全局表中删除数据,该函数只执行步骤一,不执行步骤二,即不会释放待删除数据的占用内存;
调用 lazyfreeGetFreeEffort() 评估释放内存开销,如果开销超过门限值 LAZYFREE_THRESHOLD(默认 64),则通过 bioCreateBackgroundJob() 创建一个新的删除任务,由后台线程来执行内存释放;如果开销较小,则直接调用 dictFreeUnlinkedEntry() 函数执行内存释放。
可以看到,即使开启了惰性删除,在实际执行过程中,redis 也会先评估删除数据的开销,然后再决定是执行异步删除还是同步删除。
可以看出,从 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
// 主要用于复制过程中,全量同步的场景,从节点需要删除整个 db
replica-lazy-flush no
// del 指令删除关系,不释放内存, 功能同unLink
lazyfree-lazy-user-del no
127.0.0.1:6379> info memory
# Redis 保存数据申请的内存空间
used_memory:9469412118
used_memory_human:8.82G
# 操作系统分配给 Redis 进程的内存空间
used_memory_rss:11351138316
used_memory_rss_human:10.57G
# Redis 进程在运行过程中占用的内存峰值
used_memory_peak:12618222522
used_memory_peak_human:11.75G
# 内存碎片率,used_memory_rss / used_memory
mem_fragmentation_ratio:1.20
# Redis 最大可用内存,0表示不限制
maxmemory:0
maxmemory_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对其他请求的响应。
hz参数是否生效,以及对服务影响 redis高版本有没有修复问题 欢迎关注公众号:DBA札记,一起交流数据库技术。欢迎觉得读完本文有收获,可以转发给其他朋友,大家一起学习进步!谢谢大家。