周口市网站建设_网站建设公司_Java_seo优化
2025/12/23 20:26:53 网站建设 项目流程

前天跟一个老读者复盘美团二面,他跟我吐槽:“面试官问了个送分题,Redis 的 Key 过期时间到了,内存是立马释放的吗?我回答‘是’,结果他脸色变了,追问了一堆细节,直接给我问挂了。”

说实话,这题看着像送分,其实是送命

很多人的理解还停留在:“TTL 归零 -> 数据消失 -> 内存释放”。 但在高并发生产环境下,如果 Redis 真敢这么做,你的系统早崩了。试想一下,如果大促整点有 500 万个 Key 同时过期,Redis 为了“立刻释放内存”而疯狂扫描删除,CPU 瞬间 100%,主线程卡死,所有线上请求全部超时——这就是灾难。

今天咱们扒开 Redis 的底裤,从源码逻辑生产陷阱,彻底讲透为什么“过期了却还在占用内存”。

一、 别幻想了,Redis 根本没有“准时删除”

首先,把“定时器”这个概念从脑子里扔出去。 Redis 是基于 Reactor 模式的单线程模型(6.0 之前完全单线程,6.0 后网络 IO 多线程,但核心指令执行依然是单线程)。

如果你给几千万个 Key 每个都挂一个定时器,CPU 光是处理回调和上下文切换就得累死,哪还有空处理你的 Get/Set 请求?

Redis 采用的是一种“懒惰”+“贪婪”的混合策略:

1. 惰性删除(Lazy Expiration):被动清理

这是 Redis 最“鸡贼”的地方。Key 过期了?它根本不主动管。 只有当你访问这个 Key(执行GET/TTL等命令)时,Redis 才会检查:

  • Check:这货过期没?

  • Action:过期了 ->立刻在主线程执行删除-> 返回nil

P7 级陷阱提示:这就带来一个严重的隐性 OOM 问题——冷数据堆积。 如果一大批数据设了过期时间,但从此再也没人查过它,那它就永远不会触发惰性删除,一直赖在内存里。这就是为什么你的 Redis 经常莫名其妙内存报警。

2. 定期删除(Active Expiration):主动抽查

为了清理冷数据,Redis 必须主动出击。但请注意,这里是面试最大的坑

误区:“Redis 会开一个后台线程去删数据。”

真相:定期删除是跑在主线程(Main Thread)里的!

源码位置src/server.c->serverCron->activeExpireCycle

// 文件:src/server.cintserverCron(structaeEventLoop *eventLoop,longlongid,void*clientData){ // ... 省略其他杂七杂八的代码 ... // 处理数据库相关的后台任务(注意:这里的“后台”是指逻辑上的后台,依然在主线程跑) databasesCron(); // ... 省略 ... return1000/server.hz;} voiddatabasesCron(void){ // 如果是主节点(Master),执行主动过期策略 if(server.active_expire_enabled && server.masterhost ==NULL) { // 【关键点在这里】 // 调用主动过期循环,注意这里没有任何 thread create 的操作 activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); } // ... 省略碎片整理等逻辑 ...} voidactiveExpireCycle(inttype){ // ... // 循环遍历每一个 DB for(j =0; j < dbs_per_call && timelimit_exit ==0; j++) { // ... // 【死循环开始】只要过期比例超过阈值,就一直卡在这里删! // 这就是为什么我说它会阻塞主线程的原因! do{ // 1. 也就是在这里,从 expires 字典里随机拿 key // 2. 检查是否过期 // 3. 如果过期,调用 activeExpireCycleTryExpire -> delete() // 如果执行时间超过了 timelimit(默认 25ms),强制 break if((iteration &0xf) ==0) {/* check time every 16iterations */ elapsed =ustime()-start; if(elapsed > timelimit) { timelimit_exit =1; server.stat_expired_time_cap_reached_count++; break;// 只有超时了才放过主线程 } } }while(expired > config_keys_per_loop * config_cycle_acceptable_stale_cpu_percent /100); }}

Redis 每 100ms(默认 hz=10)触发一次时间事件,逻辑如下:

  1. 从设置了过期的 Key 字典中,随机抽取 20 个 Key

  2. 检查并删除其中已过期的。

  3. 核心博弈:如果这 20 个里,过期的超过 5 个(>25%),Redis 会判定“过期数据太多了”,于是立刻重来一次步骤 1

  4. 止损机制:为了防止主线程卡死,这个循环有一个时间上限(默认 25ms)。一旦超时,强制停止,把 CPU 权交还给正常的读写请求。

生产事故重现:如果你在业务代码里写了个循环,让 100 万个 Key 在同一秒过期(比如缓存了今天的热门新闻,TTL 设为今晚 24:00)。 Redis 每一轮抽查,过期率都是 100%,触发“贪婪循环”,虽然有 25ms 限制,但高频的 CPU 占用依然会导致接口响应耗时(RT)出现明显的毛刺(Spike),甚至造成短暂的请求阻塞。

二、 致命盲区:主从架构下的“幽灵内存”

如果你面的是 P7/L8 岗位,只讲单机策略是不够的。面试官的必杀技通常是:“为什么我的主库内存正常,从库(Slave)内存却爆了?”

这触及到了 Redis 主从同步的机制:

  1. 从库绝不主动删除过期数据。即便在从库上触发了“定期删除”逻辑(高版本),它也只是标记,不会执行物理删除。

  2. 从库的惰性删除是“逻辑删除”。你在从库查一个过期 Key,它会回你nil(骗你没了),但物理内存里它还在!

  3. 从库必须等主库指令。只有主库真正删除了这个 Key,并生成一条DEL命令通过 Replication 流同步给从库,从库才会释放内存。

结论:如果主库压力过大(过期清理跑不过来),或者主从网络延迟高,DEL命令没及时传过去,从库就会囤积大量已过期的“尸体数据”,导致从库 OOM。

三、 终极拷问:既然都有兜底了,为什么内存还是 OOM?

即使有“惰性+定期”双保,内存依然可能被打爆,原因只有两个:

  1. 写入速度 > 清理速度:你写数据的速度太快,Redis 来不及删。

  2. 大 Key 问题:删一个几百 MB 的 Key,主线程会卡顿,导致清理效率下降。

这时候,Redis 最后的防线就是内存淘汰策略(Maxmemory Policy)

这里有个极其危险的默认配置:noeviction。 大多数云厂商或默认安装,都是这个策略。意思是:内存满了?我死都不删!谁写我就报错!

P7 级最佳实践(建议背诵):

  • 纯缓存场景(Cache): 建议配置allkeys-lru。不管 Key 有没有设置 TTL,只要内存满了,就把最近最少使用的数据踢走。保证热点数据一直可用。

  • 存储场景(Store/DB): 如果你的 Redis 里混杂了“必须要持久化的数据”(没设 TTL)和“缓存数据”。必须配置volatile-lru。 意思就是:只杀那些设置了过期时间的数据。千万别用allkeys-lru,否则你的持久化配置数据可能会被误删!

四、 总结:面试怎么答才像专家?

如果面试官再问“过期释放”问题,按这个逻辑层层递进,降维打击:

  1. 破题(纠正认知): “面试官,Redis 的过期删除并非‘准时’,而是惰性删除(Lazy)定期删除(Active)配合完成的。而且,定期删除是运行在主线程中的。”

  2. 剖析机制(展示深度): “定期删除本质是概率抽查。Redis 限制了执行时长(默认 25ms),防止阻塞主线程。 但在主从架构下,从库是被动的,必须等待主库同步DEL指令。如果主从延迟高,会出现从库内存不释放的现象。”

  3. 解决方案(实战经验): “生产环境中,为了避免 OOM 和主线程卡顿,我们一般做三层防御:

  • 业务层:TTL 必须加随机值(如Random(300s)),打散过期时间,防止‘过期风暴’。

  • 配置层:根据业务场景选择正确的淘汰策略。纯缓存用allkeys-lru,混合存储用volatile-lru,坚决不能用默认的noeviction

  • 版本层:对于大 Key(BigKey)删除,我们使用 Redis 4.0+ 的Lazy Free(异步删除)特性,将释放内存的耗时操作移到后台线程,避免阻塞主线程。”

写在最后技术没有玄学,全是权衡。 Redis 之所以这么设计,是在CPU 算力(不搞定时器)、内存空间(允许少量残留)和系统稳定性(不阻塞主线程)之间做的极致妥协。

懂了这些,面试官想坑你都难。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询