外观
定时任务
字数
6230 字
预计阅读
24 分钟
"每天凌晨清理一次过期日志"、"每 5 分钟检查一次超时的待办"——这类按时间自动执行的工作,交给定时任务。后台有两个页面:
| 页面 | 用途 |
|---|---|
| 系统监控 → 定时任务 | 新增、修改、启用或停用任务,立即执行一次,查看接下来的执行时间 |
| 系统监控 → 调度日志 | 每一次执行的结果、输出和错误信息 |
先认识两个词:
- 处理器:开发者写在代码里的一段程序,比如"清理过期日志"。它决定做什么;
- 任务:在后台页面上配置的一条记录,决定这个处理器什么时候做、带什么参数做。同一个处理器可以建多个任务,用不同的时间和参数。
谁能看到这两个菜单
默认只有超级管理员能看到。要交给别人管理,在 系统管理 → 角色管理 里给角色分配菜单和按钮权限,见下文权限。
新增一个任务
在 系统监控 → 定时任务 点"新增",填写下面这些项:
| 项目 | 说明 | 默认值和范围 |
|---|---|---|
| 任务名称 | 不能和其他任务重名 | 最多 64 个字 |
| 任务分组 | 用来归类和筛选。分组来自字典 scheduler.job_group,可以在 系统管理 → 字典管理 里添加 | 默认"默认",还有"系统" |
| 处理器 | 从下拉列表里选,见只能选择登记过的处理器 | |
| 参数 | 一个 JSON 对象(一种文本格式,比如 {"message":"hello"})。选择处理器时,如果参数框是空的,或者还是上一个处理器的默认参数,会换成新处理器的默认参数;手动改过的内容不会被覆盖。空着时按空对象 {} 处理:参数规则里有默认值的会自动补上(比如"示例:回显消息"空着时,message 仍是 hello);规则里有没设默认值的必填参数时,空着会保存失败 | 最多 4000 个字符 |
| Cron 表达式 | 什么时候执行,见Cron 表达式 | 新任务默认每天 00:00:00 |
| 启用状态 | 关闭时任务不会按时间执行 | 开 |
| 失败重试次数 | 见失败重试 | 0,最多 10 |
| 重试间隔(毫秒) | 两次尝试之间等多久 | 1000(1 秒),最多 1 小时 |
| 超时(毫秒) | 见超时。填 0 表示不限时 | 60000(1 分钟),最多 1 天 |
| 允许并发执行 | 见允许并发执行 | 否 |
| 错过策略 | 服务停机期间错过的执行怎么办,见错过策略 | 跳过 |
| 备注 | 最多 500 个字 |
保存后马上生效,不需要重启服务。
只能选择登记过的处理器
任务里只保存"处理器的名称 + 参数"。处理器是开发者写在代码里的程序,服务启动时自动登记成一份名单(白名单)。页面上的下拉列表就是这份名单,每一项显示中文名称和处理器名称(比如 audit.purge)。
服务端保存时还会再核对一次:名单里没有的处理器,直接拒绝,提示"处理器 xxx 不存在:只能选择系统已注册的处理器"。
为什么这样设计? 有些后台允许在页面上填写类名、方法名甚至一段脚本,再由程序在运行时去调用。这样做很灵活,但一个后台账号被盗,就等于服务器上可以执行任意代码。白名单把"能做什么"限定在开发者写好、经过评审的代码里,后台只能决定"什么时候做"和"用什么参数做"。整个过程没有反射调用,也不会把文字当成代码执行(见安全基线 · 注入与代码执行)。
参数也要检查。每个处理器都带有自己的参数规则,参数必须是 JSON 对象,并且符合这个规则,否则保存失败,并指出是哪个参数不对。
代码升级之后
如果新版本的代码删掉了某个处理器,或者改了它的参数规则,已有的任务照常触发,但每次都记为"失败",错误信息里写明原因,而且不会重试。修改任务(换处理器或改参数)后就恢复正常。
Cron 表达式
Cron 是一种用几段数字描述"什么时候执行"的写法。表单里的 Cron 编辑器分三部分:
- 选择器:先选周期(每分钟、每小时、每天、每周、每月、每年……),再点选具体的时、分、秒、日期或星期,表达式自动生成;
- 文本框:也可以直接输入表达式,选择器生成不了的写法在这里输入;
- 说明和预览:下面用文字说明这个表达式的意思(跟随界面语言),并列出服务端算出的接下来 5 次执行时间。表达式不对时,这里会显示原因。
规则:
- 6 段(从秒开始:秒 分 时 日 月 星期)或 5 段(从分钟开始),用空格分隔;
- 只能用数字和
*,-/四个符号。不支持?、L、W、#、@daily,也不支持MON、JAN这样的英文缩写; - 星期用 0–6 表示,0 是周日;
- 永远不会执行的表达式(比如 2 月 30 日)保存时会被拒绝。
常用写法:
| 表达式 | 含义 |
|---|---|
0 30 3 * * * | 每天 03:30:00 |
0 */10 * * * * | 每 10 分钟 |
0 0 9 * * 1-5 | 周一到周五 09:00 |
0 0 0 1 * * | 每月 1 日 00:00 |
*/30 * * * * * | 每 30 秒 |
30 3 * * * | 每天 03:30(5 段写法) |
从 Quartz 写法迁移
Quartz 习惯在"日"或"星期"上写 ?,这里换成 * 即可,比如 0 0 2 ? * * 写成 0 0 2 * * *。英文缩写换成数字,第 7 段"年"去掉。
时区
执行时间按服务器(操作系统)的时区计算,和 系统管理 → 参数设置 里的"默认时区"(core.default_timezone)无关,改那个参数不会改变任务的执行时间。页面上的"接下来 5 次执行"已经换算成你电脑的时区显示,以它为准。比如服务器用 UTC 时区、你在北京,0 30 3 * * * 实际在北京时间 11:30 执行。
执行规则
超时
一次执行超过"超时(毫秒)"设定的时间,就会被判定为超时:
- 系统通知处理器停止,这一次记为"超时";
- 同时给所有启用的超级管理员发一条站内信,标题是"任务超时:任务名称",内容包括处理器、超时时间、开始时间和第几次尝试。站内信模板是"任务超时告警",可以在 系统管理 → 消息中心 → 站内信模板 里修改;
- 处理器要配合"停止"通知才能真正停下来。内置的处理器都会配合;自己写的处理器怎么配合,见开发指南 · 第二个参数 ctx。
不配合停止的处理器
没有允许并发执行时,系统会一直等到超时的那次真正结束,才开始下一次。如果处理器不理会停止通知,它后面的执行都会被记为"跳过"。
失败重试
处理器出错或者超时后,按"失败重试次数"再试几次,每两次之间等"重试间隔"。
- 每一次尝试都在调度日志里留一条记录,"第几次尝试"依次是 1、2、3……
- 同一次触发的所有尝试算作一个整体:没有允许并发执行时,重试期间到了下一次执行时间,下一次会被跳过;
- 这些情况不重试:处理器已经不存在或参数不再符合规则、任务锁丢失(见部署多个实例时)、服务正在关闭。
允许并发执行
上一次还没执行完,下一次的时间又到了,怎么办?
| 设置 | 结果 |
|---|---|
| 否(默认) | 这一次不执行,在调度日志里记一条"跳过",输出里写明原因 |
| 是 | 两次同时执行。只有处理器同时运行多份也不会出问题时才打开 |
不管选哪个,同一个执行时间只会执行一次,部署了多个实例也一样。
错过策略
服务停机(比如发版重启)期间,到了执行时间却没能执行,叫做"错过"。服务启动时,会检查每个启用的任务有没有错过:
| 错过策略 | 启动后做什么 |
|---|---|
| 跳过(默认) | 不补做,只在调度日志里记一条"跳过",输出里写明错过的是哪个时间 |
| 补跑一次 | 启动后立即补做一次 |
- 不管错过了几次,都只记一条或只补做一次;
- 从"上次触发时间"和"最后一次修改任务"中较晚的那个开始算。所以任务停用期间、刚改过执行时间之前的时段,都不算错过;
- 补做还没开始服务就又崩溃了,下次启动会接着补做;
- 部署了多个实例时,只有一个实例负责补做。
怎么选:执行很频繁的任务(比如每分钟一次)用"跳过",反正马上又会执行;每天一次、必须做的工作(比如清理、日报)用"补跑一次"。
启用和停用
任务列表的"启用状态"一列是一个开关,点一下就能停用或重新启用(需要"修改"权限)。
- 停用后立即不再按时间执行,所有实例同时生效。正在执行的那一次不会被打断;
- 重新启用后,从下一个执行时间继续。停用期间的时间不算错过,不会补做;
- 停用的任务仍然可以点"执行一次",适合在正式启用之前先试一试。
执行一次
在任务那一行点"执行一次",确认后任务立即在后台执行一次,页面提示"已开始执行,结果见执行日志"。
- 使用已经保存的参数,不能临时改参数。这样只有"执行一次"权限、没有"修改"权限的人,不能借机用别的参数运行任务;
- 和按时间触发的执行一样,遵守超时、重试和并发的设置。比如没有允许并发执行、上一次还没结束时,这一次会记为"跳过";
- 结果在 系统监控 → 调度日志 里查看,或者在任务详情的"最近执行"里查看。
任务详情
点任务名称,打开任务详情:
- 任务的全部设置,以及 Cron 表达式的文字说明;
- 接下来 5 次执行的时间。任务停用时也会列出,并提示"任务已停用:以下为启用后的触发时间。";
- 最近执行:这个任务最近 10 条执行记录(每次尝试、每次跳过各算一条)的开始时间、结果、第几次尝试、耗时和错误信息。这一栏需要调度日志的"浏览"权限。
任务列表还可以按任务名称、任务分组、处理器和启用状态搜索,可以导出 Excel。"创建时间"和"备注"两列默认隐藏,可以在列设置里打开。
调度日志
系统监控 → 调度日志 记录了每一次尝试(包括重试和跳过),确认对话框里有时也叫它"执行日志"。
- 列表显示任务名称、处理器、第几次尝试、结果、开始时间和耗时(毫秒);
- 可以按任务 ID、任务名称、处理器、结果和开始时间筛选。处理器要填完整的名称,比如
audit.purge; - 点任务名称打开详情,可以看到输出和错误信息。输出包括处理器执行过程中写的日志和最后的返回值,最多保留 16000 个字符;错误信息最多 4000 个字符,超出部分会被截掉;
- 任务被删除后,它的执行记录仍然保留,任务名称也照常显示。
结果有四种:
| 结果 | 什么时候出现 |
|---|---|
| 成功 | 处理器正常结束 |
| 失败 | 处理器出错;处理器已不存在或参数不再符合规则;任务锁丢失 |
| 跳过 | 上一次还没执行完(没有允许并发执行);或者服务停机错过了执行时间,错过策略是"跳过" |
| 超时 | 超过了任务设置的超时时间 |
删除和保留时间
- 页面上可以删除一条、批量删除,或者点"清空"删除全部记录。删除后页面上不再显示,也无法在页面上恢复;
- 执行记录默认保留 180 天,到期后由内置任务"清理过期日志"彻底删除,包括页面上已经删除的记录;
- 保留天数在 系统管理 → 参数设置 里修改,参数键是
audit.retention_days(取值 1–36500,填写无效时按 180 天)。这个参数同时控制操作日志、登录日志、消息记录等的保留时间,见操作日志 · 保留时间。
内置任务
初始化数据时会创建下面五个任务。它们的名称跟随界面语言显示。
| 任务名称 | 处理器 | 默认执行时间 | 默认状态 | 错过策略 |
|---|---|---|---|---|
| 清理过期日志 | audit.purge | 每天 03:30(0 30 3 * * *) | 启用 | 补跑一次 |
| 清理过期在线会话 | session.sweep | 每 10 分钟(0 */10 * * * *) | 启用 | 跳过 |
| 派发待发送通知 | notify.dispatch | 每分钟(0 * * * * *) | 启用 | 跳过 |
| 发送待办超时提醒 | wf.task.remind | 每 5 分钟(0 */5 * * * *) | 启用 | 跳过 |
| 示例:回显消息 | demo.echo | 每小时整点(0 0 * * * *) | 停用 | 跳过 |
前四个属于"系统"分组,示例属于"默认"分组。"清理过期日志"的超时是 10 分钟,其余都是 1 分钟;都不重试,都不允许并发执行。
它们分别做什么:
清理过期日志:按
audit.retention_days(默认 180 天)彻底删除过期的数据,包括操作日志、登录日志、API 访问日志、API 错误日志(只删已处理、已忽略或已删除的)、调度日志、站内信、邮件记录、短信记录、短信验证码,以及删除时间超过保留期的文件(文件本身和记录一起删)。每批删 5000 条,避免长时间锁表;清理过期在线会话:把已经结束的会话从"在线用户"列表里清掉。有人登录时也会顺便清理,这个任务负责长时间没人登录的情况;
派发待发送通知:补发还没发出或者发送失败的站内信、邮件和短信。每条消息最多尝试 3 次。服务在发送途中崩溃时,消息也不会丢,原理见开发指南 · 消息通知;
发送待办超时提醒:审批节点设置了"超时提醒"时,待办超过办理时限后,按节点的"到期后"处理:
- 仅提醒(默认):给办理人发一次提醒;设置了重复提醒的,之后按间隔重复提醒;
- 自动通过、自动驳回、转给上级:执行这个动作,在审批时间线上记一笔,并通知发起人、流程管理员和原来的办理人。有两种情况改为提醒办理人(时间线照样记一笔,发起人和流程管理员照样收到处理通知):转给上级时既找不到合适的上级,也没有可转的流程管理员;或者处理失败,比如自动通过后下一个审批节点没有审批人。每个待办只自动处理一次,和人工审批同时发生时也只处理一次。规则见工作流 · 超时自动处理。
因为每 5 分钟检查一次,提醒和自动处理最多晚几分钟。调度日志的输出里有这一次的统计,比如
reminded 2, timed out 1, timeout failed 0, errors 0 of 3,依次是:只发了提醒的、按"到期后"处理过的(包括没人可转、改为提醒的)、处理失败的、出错的待办数,最后是这一次检查的待办数。出错的待办不做任何改动,下次执行再试;示例:回显消息:什么都不改,只是等一会儿再把参数里的文字原样返回,用来熟悉这两个页面。
用示例任务试一试
"示例:回显消息"有两个参数:message(返回的文字,最多 200 个字,默认 hello)和 delayMs(先等多少毫秒,0–600000,默认 0)。
- 把
delayMs设得比超时还长,点"执行一次",就能看到"超时"和超级管理员收到的站内信; - 执行时间设为每 10 秒(
*/10 * * * * *),delayMs设为 20000,启用后就能看到"跳过"。试完记得停用。
内置任务只在第一次初始化数据时写入默认值。之后你修改的执行时间、启用状态等,以后升级版本、再次执行初始化数据(种子)时都不会被覆盖。删除了的内置任务也不会被重新创建,所以暂时不需要时,停用比删除好。另外,不要修改内置任务的名称:改名后名称不再跟随界面语言;而且初始化数据是按"处理器 + 原来的名称"查找内置任务的,找不到就会再新建一个(比如又多出一个启用的"清理过期日志")。
不要随意停用
"清理过期日志"停用后,日志会一直增长;"派发待发送通知"停用后,发送失败的消息不会再补发;"发送待办超时提醒"停用后,审批的超时提醒不会再发出,到期待办的自动通过、自动驳回和转给上级也不会再执行。
部署多个实例时
为了分担访问量或者避免一台机器出故障就停止服务,同一个后端程序可以运行多份,叫做多个实例。它们共用一个 MySQL 和一个 Redis。定时任务在多实例下这样工作:
- 同一个执行时间只执行一次。每个实例都有自己的计时器,但谁先抢到就由谁执行,其他实例什么都不做,也不会多记"跳过";
- 没有允许并发执行的任务,在所有实例之间也不会重叠。正在执行的实例持有一把存在 Redis 里的任务锁,执行期间不断续期;
- 页面上的修改马上在所有实例生效:当前实例立即更新,再通过 Redis 通知其他实例。万一通知丢了,每个实例每 60 秒会和数据库核对一次;每次触发之前也会再查一次数据库,确认任务仍然启用、执行时间没变,所以不会按旧的设置执行;
- 错过的执行只由一个实例补做;
- 任务锁丢失:锁的续期没有得到确认时(比如 Redis 短暂不可用),实例会认为锁已经丢了,马上通知处理器停止,这一次记为"失败",并且不再重试。这样在锁过期、被别的实例拿到之前,这边已经停下来,避免两份同时执行(前提是处理器会配合停止通知);
- 服务关闭:正在执行的任务会收到停止通知,服务最多等 10 秒再退出。
时区要一致
所有实例的时区要设成一样。否则同一个 Cron 表达式在不同实例上会算出不同的执行时刻,同一个任务可能被执行两次。
权限
| 页面 | 按钮权限 | 权限码 | 说明 |
|---|---|---|---|
| 定时任务 | 浏览 | scheduler.task.browse | 打开页面、搜索 |
| 查看 | scheduler.task.view | 打开任务详情 | |
| 新增 | scheduler.task.create | ||
| 修改 | scheduler.task.modify | 编辑任务(同时需要"查看")、启用和停用 | |
| 删除 | scheduler.task.remove | 删除、批量删除 | |
| 导出 | scheduler.task.export | ||
| 执行一次 | scheduler.task.run | ||
| 调度日志 | 浏览 | scheduler.run.browse | 打开页面;任务详情里的"最近执行"也需要它 |
| 查看 | scheduler.run.view | 打开执行记录的详情 | |
| 导出 | scheduler.run.export | ||
| 删除 | scheduler.run.remove | 删除、批量删除、清空 |
任务的新增、修改、启用停用、删除、导出和执行一次,以及调度日志的删除、清空和导出,都会记入 系统管理 → 日志管理 → 操作日志。
谨慎授予"修改"权限
有"修改"权限的人可以停用任何任务,包括清理日志和补发通知这些系统任务。
开发指南
- 定时任务:写一个自己的处理器,用种子预置任务;
- 参数设置:处理器里读取后台可修改的配置(比如保留天数);
- 操作日志:日志的保留时间;
- 消息通知:"派发待发送通知"背后的原理;
- Redis:任务锁存在哪里;
- 相关功能:工作流(超时提醒和超时自动处理)、权限与数据范围、安全基线。