当前位置: 首页 > news >正文

【Redis从入门到精通】第38篇:serverCron——Redis的“心跳“定时任务干了哪些活

上一篇【第37篇】Redis服务器启动全流程——从redis-server到ready to accept
下一篇【第39篇】Redis主从复制——数据如何在主从节点间同步


如果你的心脏停止跳动3秒就会被拉去抢救,那么Redis的"心脏"——serverCron如果停止执行又会发生什么?答案是:客户端超时不会检查、过期Key不会删除、内存统计不会更新、AOF和RDB不会自动触发……整个Redis会从"活蹦乱跳"变成"行尸走肉"。今天我们就来解剖这颗Redis的心脏,看看它每100毫秒跳一次,每次跳完都干了哪些事。

serverCron是什么

serverCron是一个通过"时间事件"(Time Event)机制周期性触发的函数。在initServer()阶段,它被注册到事件循环中:

// 注册serverCron为周期性时间事件// 参数:事件循环、首次执行毫秒数、回调函数、私有数据、析构函数if(aeCreateTimeEvent(server.el,1,serverCron,NULL,NULL)==AE_ERR){serverPanic("Can't create event loop timers.");exit(1);}

它的调度机制非常简单:每次执行完后,根据server.hz计算下一次什么时候再跑:

serverCron 调度机制 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ server │ │ server │ │ server │ │ Cron │ │ Cron │ │ Cron │ │ @t=0ms │ │@t=100ms │ │@t=200ms │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ ┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐ │ 执行所有 │───→│ 执行所有 │───→│ 执行所有 │───→ ... │ 子任务 │ │ 子任务 │ │ 子任务 │ └──────────┘ └──────────┘ └──────────┘ │ │ │ └──100ms────────┴──100ms────────┘ (hz=10时)

hz参数控制的就是这个间隔hz=10意味着每秒执行10次,即每100ms一次。hz越高,serverCron执行越频繁,定时任务的精度越高,但CPU开销也越大。

serverCron的完整任务列表

serverCron不只是一两件事,它扛起了Redis的"内政外交"。下面是按执行顺序排列的完整任务清单:

serverCron() 执行全景图 ┌──────────────────────────────────────────────────────┐ │ 入口:serverCron(eventLoop, id, clientData) │ ├──────────────────────────────────────────────────────┤ │ │ │ 1. 更新服务器时间缓存 每100ms │ │ updateCachedTime(1) │ │ │ │ 2. 更新LRU时钟 每100ms │ │ server.lruclock = getLRUClock() │ │ │ │ 3. 更新每秒命令数统计 每100ms │ │ trackInstantaneousMetric() │ │ │ │ 4. 更新内存峰值 每100ms │ │ if (zmalloc_used > peak) peak = zmalloc_used │ │ │ │ 5. 处理SIGTERM信号 每100ms │ │ if (server.shutdown_asap) prepareForShutdown() │ │ │ │ 6. 客户端超时检查 每100ms │ │ clientsCron() │ │ │ │ 7. 数据库定期过期删除 每100ms │ │ activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW) │ │ │ │ 8. AOF/RDB调度 条件触发 │ │ if (hasActiveChildProcess()) {} │ │ else { rdbSaveBackground() / rewriteAppend... } │ │ │ │ 9. BGSAVE/BGREWRITEAOF完成回调 条件触发 │ │ checkChildrenDone() │ │ │ │ 10. 从库复制检查 每1000ms │ │ replicationCron() │ │ │ │ 11. Sentinel模式检查 有Sentinel时 │ │ sentinelTimer() │ │ │ │ 12. 集群模式检查 有Cluster时 │ │ clusterCron() │ │ │ │ 13. 返回下一次执行间隔 计算ms │ │ return 1000/server.hz │ │ │ └──────────────────────────────────────────────────────┘

我们逐一拆解其中最关键的几个任务。

任务1:更新服务器时间缓存

这可能是最简单的优化,但也是最精妙的之一:

voidupdateCachedTime(intupdate_daylight_info){server.ustime=ustime();// 微秒级精度server.mstime=server.ustime/1000;server.unixtime=server.mstime/1000;// 每60秒更新一次时区信息(开销较大)if(update_daylight_info){// ... 获取时区和夏令时信息}}

为什么要在serverCron里缓存时间?因为gettimeofday()是一个系统调用,有内核态开销。Redis单线程模型中,如果一个请求处理流程里调了3次gettimeofday(),每秒10万请求就是30万次系统调用——这不是一笔小钱。通过缓存,所有模块都用server.unixtime/server.mstime获取当前时间,精度虽然只有100ms,但对大多数场景(TTL判断、LRU更新)足够了。

任务2:更新LRU时钟

Redis的LRU淘汰策略依赖一个全局时钟:

// LRU时钟精度是秒级unsignedintgetLRUClock(void){return(mstime()/LRU_CLOCK_RESOLUTION)&LRU_CLOCK_MAX;}// LRU_CLOCK_RESOLUTION = 1000(1秒精度)// LRU_CLOCK_MAX = 2^24 - 1(约194天回绕一次)

每个Redis对象的lru字段(24位)存的是创建或上次访问时的LRU时钟值。当需要淘汰key时,Redis通过当前时钟 - 对象lru计算出空闲时间,淘汰最久未使用的。

任务3:更新每秒执行命令数(ops_per_sec)

voidtrackInstantaneousMetric(intmetric,longlongcurrent_reading){// 使用指数加权移动平均// 保存最近16个采样点(1秒一个)longlongavg=0;for(intj=0;j<STATS_METRIC_SAMPLES;j++)avg+=samples[j];avg/=STATS_METRIC_SAMPLES;// 更新到server.instantaneous_ops_per_sec}

这就是INFO statsinstantaneous_ops_per_sec的来源。它取最近16秒的平均值,给运维人员一个平滑的QPS视图——不会因为瞬时波动而惊吓到你。

任务5:处理SIGTERM信号(优雅退出)

信号处理是一个有趣的"异步转同步"设计。信号处理函数中只设置一个标志位:

staticvoidsigtermHandler(intsig){server.shutdown_asap=1;}

真正的退出逻辑在serverCron中执行:

if(server.shutdown_asap){// 1. 停止接收新连接// 2. 尝试进行最后一次RDB保存// 3. 关闭AOF文件// 4. 移除pid文件// 5. 退出进程prepareForShutdown(SHUTDOWN_NOFLAGS);}

这样设计的原因是信号处理函数运行在信号上下文,不能执行复杂的操作(持锁、IO等)。serverCron作为常规执行路径,可以安全地完成所有清理工作。

任务6:客户端超时检查

voidclientsCron(void){// 遍历客户端链表,检查空闲时间if(server.maxidletime&&// timeout > 0now-c->lastinteraction>server.maxidletime){freeClient(c);}// 检查输出缓冲区是否超过软/硬限制// 释放内存中的空闲客户端对象}

上一篇我们了解到超时的诸多"死法",这里的clientsCron()就是执行死刑的刽子手。

activeExpireCycle —— 过期键删除的"扫地僧"

这是serverCron中工作量最大、最容易出问题的任务。

两种模式:FAST vs SLOW

Redis的主动过期检查有快慢两种模式:

主动过期删除的双模式 SLOW模式 FAST模式 ┌─────────────────┐ ┌─────────────────┐ │ serverCron中调用 │ │ beforeSleep中调用 │ │ 每次serverCron │ │ 每次事件循环前 │ │ 执行一次 │ │ 执行一次 │ │ │ │ │ │ 有充足的时间预算 │ │ 时间预算很小 │ │ 25%的CPU时间 │ │ 1ms快速扫描 │ │ (默认100ms中占25ms)│ │ │ │ │ │ │ │ 可遍历多个数据库 │ │ 只扫描有超时key的DB │ │ 深度扫描 │ │ 浅层扫描 │ └─────────────────┘ └─────────────────┘

关键是SLOW模式的实现细节:

voidactiveExpireCycle(inttype){// 计算本次可用的CPU时间预算// 默认:1000000/hz * ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/100// = 1000000/10 * 25/100 = 25000微秒 = 25mslonglongstart=ustime();timelimit=1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;// 遍历每个数据库(最多16次循环)for(j=0;j<dbs_per_call;j++){// 随机抽取20个带过期时间的key// 删除其中已过期的// 如果超过25%的key过期了(即5个以上),继续这个数据库do{num=dictGetSomeKeys(db->expires,&data,20);// 删除过期的key// ...// 更新计数}while(expired>ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);// 删除数 > 20/4 = 5,说明这个库垃圾较多,继续扫// 检查是否超时if((ustime()-start)>timelimit){return;// 时间预算用完,下次再继续}}}

算法精髓:随机采样 + 自适应

与很多人想象的不同,Redis不会遍历所有key来检查过期。它的策略是:

  1. 随机采样:每次从带过期时间的key中随机抽取20个。
  2. 判断比例:如果这20个中有超过25%(5个)已经过期,说明这个库"垃圾"较多,继续扫描。
  3. 时间预算:无论扫了多少,只要超过时间预算就停下来,把剩余工作留给下一次serverCron

这个设计非常务实:过期删除既要及时,又不能阻塞正常请求。如果某个库有过期key堆积,Redis会多花时间清理;如果都很干净,就快速跳过。

# 观察过期键删除情况redis-cli INFO stats|grepexpired# expired_keys:1234567 ← 已过期删除的key总数# evicted_keys:0 ← 因内存不足驱逐的key数# keyspace_hits:9876543 ← 命中次数# keyspace_misses:123456 ← 未命中次数

一个血淋淋的案例

真实踩坑簿:某电商大促期间,Redis突然出现周期性的"卡顿"——每隔100ms就有几十毫秒的响应延迟。排查发现,运营活动设置了几十万个key同时过期(搞了一个限时秒杀,所有商品缓存设了相同的TTL)。在过期那一秒,activeExpireCycle在SLOW模式下疯狂扫key,每次serverCron执行都要花上30-50ms清理过期键,而正常请求就只能等。更糟的是,FAST模式在beforeSleep中也触发了,每次事件循环前都要花1ms扫描,进一步恶化了延迟。

解决方案:在设置TTL时加入随机偏移量,把同时过期变成分散过期:

# Python示例:给TTL加上随机抖动ttl=3600+random.randint(0,300)# 1小时 + 0~5分钟的随机值redis.setex(key,ttl,value)

持久化调度——AOF和RDB的自动触发

serverCron还承担着持久化的"调度员"角色:

// serverCron中的持久化调度逻辑(简化)// 检查是否需要触发RDBfor(j=0;j<server.saveparamslen;j++){// saveparams = [{seconds: 900, changes: 1}, {seconds: 300, changes: 10}]if(server.dirty>=sp->changes&&now-server.lastsave>sp->seconds){rdbSaveBackground(server.rdb_filename,NULL);break;}}// 检查是否需要触发AOF重写if(server.aof_state==AOF_ON&&server.aof_rewrite_perc&&server.aof_current_size>server.aof_rewrite_min_size){longlongbase=server.aof_rewrite_base_size?:1;longlonggrowth=(server.aof_current_size*100/base)-100;if(growth>=server.aof_rewrite_perc){rewriteAppendOnlyFileBackground();}}// 检查子进程是否完成if(server.rdb_child_pid!=-1||server.aof_child_pid!=-1){// 子进程运行中,检查它是否结束了if(waitpid(...)&&WIFEXITED(status)){// 子进程完成,处理结果backgroundSaveDoneHandler(...);}}

hz参数的调优艺术

hz是一把双刃剑:

hz值间隔CPU开销定时任务精度适用场景
11000ms极低嵌入式、IoT设备
10(默认)100ms绝大多数场景
5020ms较高大量短TTL key
10010ms较高频繁过期场景
5002ms很高极端场景(慎用)
# 查看和调整hzredis-cli CONFIG GET hz# 1) "hz"# 2) "10"redis-cli CONFIG SET hz50# OK# 动态hz(Redis 5.0+自动根据客户端连接数调整)redis-cli CONFIG GET dynamic-hz# 1) "dynamic-hz"# 2) "yes" ← 默认开启

dynamic-hz是Redis 5.0引入的智能特性。当客户端连接数较多时,serverCron中有些任务的复杂度与连接数成正比(比如客户端超时检查需要遍历所有客户端)。dynamic-hz会根据客户端数量动态降低hz,减少CPU消耗:

// 动态hz计算(简化)intserverCron(structaeEventLoop*eventLoop,longlongid,void*clientData){// 如果开启了dynamic-hzif(server.dynamic_hz){// 根据客户端数调整hz// 客户端越多,hz越低while(listLength(server.clients)/server.hz>CONFIG_DEFAULT_HZ*10){server.hz=server.hz*2;// ???// 实际逻辑比这复杂,这里只是示意}}return1000/server.hz;}

踩坑提示:如果业务中大量使用了短TTL(如几秒到几十秒的key),并且对过期精度要求较高,可以考虑上调hz。但注意,hz提升后,activeExpireCycle的CPU预算也会等比增加。1000个key分布在1秒内过期和分布在100ms内过期,对Redis的影响是完全不同的。

监控serverCron的执行延迟

Redis提供了专业的延迟监控工具来观测serverCron的健康状况:

# 查看延迟监控redis-cli LATENCY LATEST# 1) 1) "command"# 2) (integer) 1711440000# 3) (integer) 350 ← 350ms延迟# 4) (integer) 5000 ← 最大5000ms# 2) 1) "expire-cycle"# 2) (integer) 1711440000# 3) (integer) 1200 ← 过期扫描造成了1200ms延迟!# 4) (integer) 1500# 查看延迟历史redis-cli LATENCY HISTORYcommand# 0) (integer) 1711440000# 1) (integer) 350# 2) (integer) 5000# 1) (integer) 1711440100# 1) (integer) 280# 2) (integer) 310# 查看延迟诊断redis-cli LATENCY DOCTOR# 输出建议,比如"过期周期花费了太长时间,请检查你的过期策略"

LATENCY命令的工作原理是在多个关键路径上记录时间戳,当执行时间超过阈值时触发事件。serverCron本身也在监控范围内。如果某次serverCron执行时间超过了server.hz的倒数(即1000/hz 毫秒),Redis会记录一条cron事件。

推荐的监控脚本

#!/bin/bash# 监控Redis延迟事件REDIS_CLI="redis-cli"whiletrue;doevents=$($REDIS_CLI LATENCY LATEST2>/dev/null)if[-n"$events"];thenecho"===$(date)==="echo"$events"echo""fisleep5done

总结

serverCron作为Redis的"心跳",每次跳动都执行了少则十几种任务:

任务频率重要性风险
时间缓存更新每次★★☆极低
LRU时钟每次★★☆极低
命令统计每次★☆☆极低
内存峰值每次★☆☆极低
SIGTERM处理每次★★★
客户端超时每次★★★
过期键删除每次★★★
持久化调度每次★★★
子进程检查每次★★★
复制/哨兵/集群较低频★★★

核心要点:

  1. hz平衡精度与性能,一般场景10Hz足够,短TTL密集场景可适当增大。
  2. 同时大量key过期是Redis延迟的"头号公敌",给TTL加随机偏移是简单有效的对策。
  3. 监控activeExpireCycle的执行时间,它是serverCron中最大的不确定因素。
  4. LATENCY DOCTOR诊断延迟问题,Redis自带的诊断工具比自己猜靠谱得多。
  5. **dynamic-hz**在连接数多时自动降低CPU开销,属于开了就省心的特性。

下次当你用INFO stats看到expired_keys数字稳步增长时,可以会心一笑——那是serverCron里的activeExpireCycle在默默打扫卫生。


上一篇【第37篇】Redis服务器启动全流程——从redis-server到ready to accept
下一篇【第39篇】Redis主从复制——数据如何在主从节点间同步


http://www.jsqmd.com/news/938936/

相关文章:

  • 湘潭CMA甲醛检测治理公司深度测评:绿居净环保稳居榜首 - 五金回收
  • AI | ollama - [入门]
  • 旧项目安装QtFusion找不到IMcore:补wheel依赖还是迁移VibeFlux
  • 告别模板化论文困局:okbiye 分层式毕设创作体系,从资料上传到终稿排版全链路落地
  • 基于LM324运放的土壤湿度监测电路设计与实践
  • BetterGI AI自动化游戏辅助工具完整教程:从新手到专家的原神自动化指南
  • STM32F103硬件SPI直驱GC9A01芯片1.28寸240×240 TFT屏,含GUI与测试例程
  • 基于Arduino与HC-SR04的超声波表情显示系统设计与实现
  • 如何轻松地将 iTunes 备份传输到三星
  • 宠物帮扶信息平台宠物领养寻宠登记Java整套源码部署
  • Linux内核启动探秘:Ramdisk从解压到执行init的完整流程解析
  • 2026年硅灰厂家选型指南:微硅粉多少钱一吨、微硅粉市场价格、微硅粉生产厂家、硅灰价格、硅灰多少钱一吨、硅灰粉生产厂家选择指南 - 优质品牌商家
  • 湘潭母婴除甲醛CMA甲醛检测治理公司2026深度测评:森氧家环保稳居榜首 - 五金回收
  • 英伟达Vera Rubin芯片:Blackwell直接过时?Agentic AI时代的硬件革命
  • 7个技巧:让你的普通鼠标在Mac上超越苹果触控板
  • 从开题立项逻辑拆解到文稿落地:深度解析 okbiye AI 开题报告模块的学术工程化设计与实战价值
  • 谷歌云的这套“真相探测仪“彻底揭穿了它们的把戏
  • 基于Arduino的智能烟雾报警器DIY:从传感器原理到嵌入式系统实战
  • SpringBoot开发宠物帮扶系统领养认领信息管理源码详解
  • 智能优化算法论文适合投哪些期刊?遗传算法、粒子群、灰狼算法、鲸鱼算法投稿方向分析
  • 芜湖母婴除甲醛CMA甲醛检测治理公司深度测评:清醛卫士稳居榜首 - 五金回收
  • 通化母婴除甲醛CMA甲醛检测治理公司2026深度测评:森氧家环保稳居榜首 - 五金回收
  • 梧州CMA甲醛检测治理公司深度测评:绿居净环保稳居榜首 - 五金回收
  • 通化母婴除甲醛CMA甲醛检测治理公司深度测评:清醛卫士稳居榜首 - 五金回收
  • 基于Arduino与MPU-6050的体感游戏手套DIY全攻略
  • 赛博朋克2077存档编辑器:解锁夜之城的无限可能
  • 基于树莓派的智能叠衣机器人:从传感器到伺服电机的闭环系统实践
  • AI 视频生成进入工作流阶段:Runway Agent、Aleph 2.0、Adobe Gemini 连接器盘点
  • 如何用WeChatMsg颠覆你的数字记忆管理:3步打造个人AI数据银行
  • 30岁大龄转行不踩坑!行政转网络安全的逆袭攻略