Node.js Express中实现支持定时+API触发的非并发异步任务
实现方案
利用Node.js单线程事件循环的特性,仅靠原生语法就能实现你要的效果,不需要引入任何第三方依赖、锁或者外部中间件:
import express from 'express'; const app = express(); const port = 3000; // 辅助sleep函数 const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // 你的业务task函数 async function task(reason) { console.log("do thing because %s...", reason); await sleep(1000); console.log("done"); } // 全局状态(Node.js单线程下操作不存在竞态,不需要加锁) let isTaskRunning = false; const pendingTaskReasons = []; let tickerTimerId = null; const TASK_INTERVAL = 60 * 1000; // 定时执行间隔,测试可改为5000 // 核心执行逻辑:按顺序处理队列中所有待执行的task async function processTaskQueue() { if (isTaskRunning) return; isTaskRunning = true; // 循环处理所有排队的任务 while (pendingTaskReasons.length > 0) { const currentReason = pendingTaskReasons.shift(); try { await task(currentReason); } catch (err) { // 这里自行处理task执行报错,避免阻塞整个队列 console.error('task执行失败:', err); } } isTaskRunning = false; // 所有任务处理完成后,重新调度下一次定时任务 rescheduleTicker(); } // 通用调度入口:定时/API触发都调用这个方法 function addTaskToQueue(reason) { pendingTaskReasons.push(reason); // 用nextTick避免同步连续多次触发重复执行process逻辑 process.nextTick(processTaskQueue); } // 定时任务调度逻辑:从上一次所有任务执行结束后开始算间隔 function rescheduleTicker() { if (tickerTimerId) clearTimeout(tickerTimerId); tickerTimerId = setTimeout(() => { addTaskToQueue('ticker'); }, TASK_INTERVAL); } // API触发接口 app.get("/task", async (req, res) => { // 方案1:加入队列后直接返回 addTaskToQueue('trigger'); res.send("task已加入调度队列"); // 方案2:需要等task执行完成再返回,就用下面的逻辑替换上面两行 /* addTaskToQueue('trigger'); const waitTaskFinish = () => { if (!isTaskRunning && pendingTaskReasons.length === 0) { res.send("ok"); } else { setTimeout(waitTaskFinish, 100); } }; waitTaskFinish(); */ }); // 初始化启动第一次定时调度 rescheduleTicker(); app.listen(port, () => { console.log(`服务运行在端口${port}`); });
方案特性
- 完全保证task不会并发执行,同一时间永远只有一个task实例在运行
- 定时触发、API触发的任务都会按触发顺序排队执行,不会丢失
- 定时间隔从上一轮所有任务执行完成后开始计算,不会因为task本身执行时间长导致堆积或者间隔不准
- 无任何第三方依赖,单进程内对象可以正常共享,符合你的要求
内容的提问来源于stack exchange,提问作者piec
相关产品推荐
相关产品推荐

