Egg + tegg 定时任务(Schedule Controller)完全指南:装饰器声明、interval/cron 模式与 worker/all 执行策略
2026/9/21 16:26:09 网站建设 项目流程
  • 后端
  • Web框架

【免费下载链接】egg

🥚🥚🥚🥚 Born to build better enterprise frameworks and apps with Node.js & Koa. https://307.run/eggcode

项目地址:https://gitcode.com/gh_mirrors/eg/egg
点击查看免费下载

导读

本文围绕 tegg 提供的Schedule装饰器定时任务能力展开,讲解如何在 Egg 应用中以声明式方式注册普通定时任务:包括interval(固定间隔)与cron(表达式)两种触发模式、workerall两种执行策略、immediate/disable/env三个任务参数,以及其底层"agent 调度 + messenger 派发 + worker 执行"的实现原理。读完本文,你将能够在任意 Egg + tegg 项目中安全、规范地编写可灰度、可开关、可多环境控制的定时任务,并理解其与框架自带app/schedule定时任务的差异。

使用场景:普通定时任务

tegg 的Schedule装饰器支持普通定时任务,其核心特征是:在每一台部署机器上都会执行。例如一个应用在生产环境最少部署 2 台机器,那么该定时任务在这 2 台机器上都会运行调度逻辑(具体由worker/all模式决定每个 worker 是否执行)。

典型适用场景包括:每台机器各自的数据清理、本地缓存预热、按机器维度执行的心跳上报等。如果需要"全局只执行一次"的分布式任务,则不属于本文讨论的普通定时任务范畴。

快速上手

开启插件

tegg 的定时任务插件是egg 内置插件,默认开启。若需显式声明,可在config/plugin.ts(或config/plugin.js)中配置:

export default { teggSchedule: true, };

目录约定与重要警告

:::warning ⚠️不要将代码写在app/schedule路径下。egg 框架默认会扫描该路径并自动注册为传统格式的定时任务,与 tegg 的装饰器方式会产生冲突。 :::

tegg 的定时任务控制器应放在app/port/schedule目录(或其他 tegg 约定的业务目录)下,通过Schedule装饰器声明。

控制器结构要求

使用Schedule装饰器标识一个类为定时任务控制器后,该类必须包含一个名称为subscribe的方法。框架调度定时任务执行时,会调用该注解类的subscribe方法。方法签名对应 tegg 定义的ScheduleSubscriber接口:

export interface ScheduleSubscriber { subscribe(data?: any): Promise<any>; }

subscribe应为 async 方法。从 load_schedule.ts 的实现可见,传统方式下 generator function 形式的任务会被直接拒绝("schedule" generator function is not support, should use async function instead),装饰器方式同样建议遵循异步函数约定。

interval 模式:按固定间隔执行

普通定时任务可配置为interval模式,即每间隔指定的时间在每台机器上执行一次。间隔时间通过Schedule装饰器的scheduleData.interval参数设置:

  • interval传值数字类型时,单位为毫秒数,例如100表示每 100ms 执行一次;
  • interval传值字符类型时,会通过ms工具转换为毫秒数,例如'5s'表示每 5 秒执行一次。
// app/port/schedule/Demo.ts import { Inject, Logger } from 'egg'; import { IntervalParams, Schedule, ScheduleType } from 'egg/schedule'; @Schedule<IntervalParams>({ type: ScheduleType.WORKER, scheduleData: { interval: 100, // 每 100ms 执行一次 // interval: '5s', // 每 5s 执行一次 }, }) export class IntervalScheduler { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }

底层实现:interval 如何被解析

从 timer.ts 的实现可以看到,intervalgetNextTick()中通过ms(this.scheduleConfig.interval)统一换算为毫秒;若同时配置了cron,则优先按 cron 计算下一次触发时间。换算后使用safeTimers进行调度:

protected safeTimeout(handler: () => void, delay: number, ...args: any[]): number | ReturnType<typeof setTimeout> { const fn = delay < safeTimers.maxInterval ? setTimeout : safeTimers.setTimeout; return fn(handler, delay, ...args) as number | ReturnType<typeof setTimeout>; }

当间隔超过safeTimers.maxInterval(约 24.8 天)时,会退化为使用safe-timers避免 setTimeout 的 32 位毫秒溢出问题——因此超长间隔(如数月一次)也能正确执行。

cron 模式:按 cron 表达式执行

普通定时任务同样支持 cron 表达式模式,表达式规则由cron-parser解析。cron 表达式共 6 段(秒为可选段):

* * * * * * ┬ ┬ ┬ ┬ ┬ ┬ │ │ │ │ │ | │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) │ │ │ │ └───── month (1 - 12) │ │ │ └────────── day of month (1 - 31) │ │ └─────────────── hour (0 - 23) │ └──────────────────── minute (0 - 59) └───────────────────────── second (0 - 59, optional)

例如下列代码将会每日 3 点在每台机器上执行一次

// app/port/schedule/CronDemo.ts import { Inject, Logger } from 'egg'; import { CronParams, Schedule, ScheduleType } from 'egg/schedule'; @Schedule<CronParams>({ type: ScheduleType.WORKER, scheduleData: { // 每日 3 点执行一次 cron: '0 0 3 * * *', // 每 5 秒执行一次 // cron: '*/5 * * * * *', }, }) export class CronSubscriber { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }

cron 解析与越界处理

在 timer.ts 的构造函数中,cron会通过cronParser.parseExpression(cron, cronOptions)预解析,解析失败会抛出带 key 信息的TypeError

throw new TypeError(`[@eggjs/schedule] ${this.key} parse cron instruction(${cron}) error: ${err.message}`, { cause: err, });

每次触发后,getNextTick()会循环调用cronInstance.next()找到下一个未来时间点;若表达式超出时间范围(Out of the timespan range),则记录日志并停止调度:

do { try { const nextInterval = this.cronInstance.next(); nextTick = nextInterval.getTime(); } catch (err) { this.logger.info(`[Timer] ${this.key} cron out of the timespan range, error: %s`, err); return; } } while (now >= nextTick); return nextTick - now;

同时,cronOptions支持透传cron-parserParserOptions(如currentDatestartDateendDatetz时区等),可通过scheduleData.cronOptions配置。

工作模式:worker 与 all

普通定时任务一般使用worker模式,框架也提供all模式,二者差异如下:

  • worker模式:每台机器上只有一个 worker会执行该定时任务,每次执行时 worker 的选择是随机的;
  • all模式:每台机器上的每个 worker 都会执行该定时任务。
import { Inject, Logger } from 'egg'; import { IntervalParams, Schedule, ScheduleType } from 'egg/schedule'; @Schedule<IntervalParams>({ type: ScheduleType.ALL, // 所有 worker 都会执行 scheduleData: { interval: 100, }, }) export class AllScheduler { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }

两种模式的派发差异(源码视角)

ScheduleType在 tegg/core/types/src/schedule.ts 中定义:

export const ScheduleType = { WORKER: 'worker', ALL: 'all', } as const;

其执行策略分别在 strategy/worker.ts 与 strategy/all.ts 中实现,二者都是TimerStrategy的子类,仅handler()的派发目标不同:

// worker.ts —— 随机派发给一个 worker export class WorkerStrategy extends TimerStrategy { handler(): void { this.sendOne(); } } // all.ts —— 派发给所有 worker export class AllStrategy extends TimerStrategy { handler(): void { this.sendAll(); } }

sendOne/sendAll位于 strategy/base.ts,通过 agent 的 messenger 分别调用sendRandom('egg-schedule', info)send('egg-schedule', info)完成消息广播,并生成唯一的 Job id:

getSeqId(): string { return `${Date.now()}${process.hrtime().join('')}${this.count}`; }

worker 侧收到消息后(见 app.ts 的egg-schedule事件处理),会等待app.ready(),然后创建一个匿名上下文执行任务:

const ctx = this.#app.createAnonymousContext({ method: 'SCHEDULE', url: `/__schedule?path=${key}&${schedule.scheduleQueryString}`, }); await this.#app.ctxStorage.run(ctx, async () => { return await schedule.task(ctx, ...info.args); });

执行完毕后,将successworkerIdrt(耗时毫秒)等结果通过sendToAgent('egg-schedule', ...)回传给 agent 收尾。

定时任务参数:immediate / disable / env

普通定时任务的Schedule装饰器支持第二个参数,用于指定定时任务运行参数:

  • immediate:配置为true时,该定时任务会在应用启动并 ready 后立刻执行一次(随后再按 interval/cron 周期执行);
  • disable:配置为true时,该定时任务不会被启动
  • env:数组,仅在指定的环境下才启动该定时任务。
import { Inject, Logger } from 'egg'; import { IntervalParams, Schedule, ScheduleType } from 'egg/schedule'; @Schedule<IntervalParams>( { type: ScheduleType.WORKER, scheduleData: { interval: 100, }, }, { immediate: true, // 在应用启动并 ready 后立刻执行一次 // disable: true, // 为 true 时,定时任务不会被启动 env: ['devserver', 'test'], // 仅在线下环境运行 }, ) export class ParamScheduler { @Inject() private logger: Logger; async subscribe() { this.logger.info('schedule called'); } }

三个参数的底层行为

ScheduleOptions类型定义同样位于 tegg/core/types/src/schedule.ts:

export interface ScheduleOptions { // default is false immediate?: boolean; // default is false disable?: boolean; // if env has value, only run in this envs env?: EggEnvType[]; }
  • disable:在 schedule.ts 的registerSchedule()中,schedule.disable为 true 的任务会被直接跳过注册,agent 启动时也不会打印其注册日志(见 app.ts 中对disable的过滤);worker 侧若意外收到已禁用任务的派发消息,也会记录disable警告后直接返回。
  • env:环境过滤发生在任务加载阶段(load_schedule.ts),当env为数组且不包含当前app.config.env时,任务会被忽略并记录日志:
const env = app.config.env; const envList = schedule.schedule.env; if (Array.isArray(envList) && !envList.includes(env)) { app.coreLogger.info(`[@eggjs/schedule]: ignore schedule ${fullpath} due to \`schedule.env\` not match`); continue; }
  • immediate:在 timer.ts 的start()中,配置了immediate的任务会通过setImmediate(() => this.handler())立即派发一次,否则进入#scheduleNext()等待第一个周期。

常用配置项速查

teggSchedule插件的完整配置(对应 config/config.default.ts)如下:

配置项类型默认值说明
schedule.type'worker' \| 'all'-任务执行策略(与装饰器参数一致)
schedule.intervalstring \| number-间隔,数字为毫秒,字符串经ms解析
schedule.cronstring-cron 表达式
schedule.cronOptionsCronOptions-透传给cron-parser的解析选项
schedule.immediatebooleanfalse启动 ready 后立即执行一次
schedule.disablebooleanfalse是否禁用该任务
schedule.envstring[]-仅在指定环境启动
schedule.directorystring[][]追加自定义定时任务目录(绝对路径)

其中schedule.directory用于扩展默认的app/schedule加载目录,加载时会合并loadUnits下的app/scheduleconfig.schedule.directory中的目录(见 load_schedule.ts)。

此外,插件内置了独立的scheduleLogger(输出到egg-schedule.logconsoleLevelNONE),任务注册、触发、执行成功/失败及耗时都会写入该日志,方便在生产环境排查定时任务问题。

执行链路小结

一次普通定时任务的完整执行链路可以概括为:

  1. 加载:agent 启动时,Scheduler.init()调用loadSchedule()扫描app/scheduleschedule.directory目录(load_schedule.ts);
  2. 注册Scheduler.registerSchedule()type找到对应的策略类(workerWorkerStrategyallAllStrategy),disable的任务被跳过(schedule.ts);
  3. 调度TimerStrategy.start()依据interval/cron/immediate计算下一次触发时间(timer.ts);
  4. 派发:到达触发点后,sendOne()随机选择一个 worker(messenger.sendRandom)或sendAll()广播给所有 worker(base.ts);
  5. 执行:worker 收到egg-schedule消息,等待app.ready()后创建匿名上下文调用subscribe(),并将执行结果回传 agent 记录日志(app.ts)。

注意事项

  • 不要将 tegg 定时任务代码放在app/schedule目录下,避免与框架默认的定时任务扫描注册冲突;
  • subscribe必须为 async 函数,且不要使用 generator function 形式;
  • worker模式下每次执行由 agent 随机选择 worker,任务逻辑不应依赖"具体由哪台机器执行";
  • 生产环境多机部署时,普通定时任务会在每台机器上各执行一次(worker模式每机一次、all模式每机每个 worker 一次),需要全局唯一执行时应采用分布式任务方案,而不是依赖本功能去重;
  • 任务执行耗时与调度间隔应合理设置,避免长时间阻塞型任务与高频率interval相互叠加造成资源占用。
  • 后端
  • Web框架

【免费下载链接】egg

🥚🥚🥚🥚 Born to build better enterprise frameworks and apps with Node.js & Koa. https://307.run/eggcode

项目地址:https://gitcode.com/gh_mirrors/eg/egg
点击查看免费下载

相关推荐

上一篇:Editor.js 使用指南:从基础操作到高级配置
下一篇:jsPDF基础教程:从入门到实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询