node-cron每日定时任务执行频率异常 未按每日一次触发
问题原因
- 核心问题是Heroku的dyno运行机制和内存级cron的特性不匹配:你写的cron表达式
0 0 0 * * *本身语法正确,对应每日纽约时间零点触发,问题不在表达式配置上。Heroku免费/基础档位的dyno在30分钟无访问时会自动休眠,被请求唤醒、平台例行维护时都会重启进程。你的代码在全局作用域直接调用了一次randomize(),每次进程重启都会先执行一次这个初始化选歌,和cron调度无关。你观测到的1小时间隔,基本是定时健康检查/平台探测每小时左右唤醒休眠dyno,触发重启导致的重复选歌。 - node-cron是内存内的调度器,调度状态完全存在当前进程内存里,进程一旦重启所有调度记录就会清空。你的dyno频繁重启的情况下,cron任务根本存活不到每天零点的触发时间点,你写的每日调度逻辑实际上从来没正常按预期执行过。
- 额外代码bug:
Math.random()不接收参数,你写的Math.random(0,1)是无效写法,虽然不影响返回结果但属于冗余错误写法。- 递归选歌的逻辑有缺陷:当随机到已使用的歌曲ID递归调用
randomize()后,代码依然会向下执行把当前重复的ID push进usedSongs数组,会导致数组出现重复值、长度异常的问题。
修复方案
- 不要把当前选中歌曲、已使用歌曲列表存在进程内存里:Heroku dyno的内存和本地文件系统都是临时的,重启就会清空,需要把这类持久化状态存在外接存储里,比如Heroku自带的Postgres、Redis插件,存储内容至少包含:当日选中歌曲ID、已使用歌曲ID列表、上次选歌的日期。
- 优先替换调度方案:不要用进程内的node-cron做每日定时任务,直接用Heroku平台官方的Scheduler插件,配置每日纽约时间零点触发选歌逻辑,平台级调度不受dyno休眠、重启影响,可靠性远高于进程内cron。
- 如果一定要保留node-cron的用法,先把dyno升级到不会自动休眠的付费档位,保证进程持续在线不被随意重启,同时修复现有代码的逻辑bug,修正后参考代码如下:
const cron = require('node-cron'); // 注意:实际生产不要把这两个值存在内存,替换成持久化存储读写 let usedSongs = []; let currentSongId; function randomize() { if(usedSongs.length === 169) { usedSongs = []; } const test = Math.round(Math.random() * 169); if (usedSongs.includes(test)) { return randomize(); } currentSongId = String(test); usedSongs.push(test); // 选完后把currentSongId、usedSongs、选歌日期写入持久化存储 } // 服务启动时先读持久化存储,如果当日已经选过歌就直接用存好的值,不要重复选 // 伪代码逻辑:const lastSelectDate = 存储读上次选歌日期; if(lastSelectDate !== 当日日期) randomize(); else 读取存好的currentSongId和usedSongs赋值 cron.schedule('0 0 0 * * *', () => { randomize(); }, { scheduled: true, timezone: 'America/New_York' });
- 兜底校验:不管是启动初始化还是cron触发选歌,都先判断上次选歌日期是不是当日,如果是当日就跳过选歌逻辑,直接返回已选中的歌曲,从根源避免重复选歌。
内容的提问来源于stack exchange,提问作者J H
相关产品推荐
相关产品推荐

