Go 后台里的定时任务启停不生效,判断顺序不要从前端开关开始,而是先查数据库状态、再查内存调度同步、最后查执行日志。更稳的排查顺序是:确认数据库里的 status 是否真的变了,再确认保存或切换状态后有没有把这个变化同步到内存里的 cron 调度器,最后再看执行日志有没有写入。很多后台项目的坑就在这里:表里的任务已经禁用,进程里的调度器还拿着旧 job;或者表里已经启用,但没有重新注册到调度器,页面看着正常,到了时间就是不跑。
这篇只讲 Go 后台里的应用内定时任务,不讲 Linux crontab,也不讲 Kubernetes CronJob。核验时点是 2026-08-10,案例来自 XYGo Admin 当前 master 分支和 v1.4.6 Release,项目技术栈是 GoFrame + Vue3,定时任务、RBAC 菜单和后台 API 都在同一个管理后台里。它在这里不是要你照搬项目,而是拿一个真实源码链路当排查样本。
主查询:Go 后台定时任务启停不生效,应该先查哪里
先查三件事。
第一,接口有没有把状态写进库。前端点启用或禁用时,不应该只改 Vue 的表格状态,必须请求后端,例如:
ts
// web/src/api/backend/system/cron.ts
export function fetchCronStatus(params: { id: number; status: number }) {
return adminRequest.post({ url: '/cron/status', params })
}
第二,后端更新 sys_cron.status 之后,有没有同步调度器。只更新数据库不够,因为定时任务通常已经被注册到内存里的 cron 实例。数据库是配置来源,不是正在运行的调度器本身。
go
// server/internal/logic/cron/cron.go
func UpdateStatus(ctx context.Context, in *adminin.CronStatusInp) error {
_, err := dao.SysCron.Ctx(ctx).Where("id", in.Id).Data(g.Map{
"status": in.Status,
"updated_at": uint(time.Now().Unix()),
}).Update()
if err != nil {
return err
}
syncJobSchedule(ctx, in.Id)
return nil
}
第三,调度同步要区分启用和禁用。启用要 StartJob,禁用要 StopJob,不能只靠下一次服务重启。
go
func syncJobSchedule(ctx context.Context, id uint64) {
var item entity.SysCron
_ = dao.SysCron.Ctx(ctx).Where("id", id).Scan(&item)
if item.Id == 0 {
return
}
if item.Status == 1 {
_ = cronlib.StartJob(&cronlib.CronJob{
Id: item.Id,
Name: item.Name,
Title: item.Title,
Pattern: item.Pattern,
Params: item.Params,
Policy: item.Policy,
Count: item.Count,
})
} else {
cronlib.StopJob(item.Id, item.Name)
}
}
能走到这里,至少可以排除一类问题:前端开关只是显示问题。如果数据库状态、接口响应和调度同步都能对上,再去看任务函数本身。
子问题一:数据库 status 正确,任务为什么还在跑
这种情况通常不是 SQL 错,而是"旧 job 没停"。排查时先用 SQL 读任务状态,再查执行日志是否在禁用后仍继续写入。
sql
SELECT id, title, name, pattern, status, updated_at
FROM sys_cron
WHERE name = 'log_clean';
SELECT id, cron_id, name, status, err_msg, created_at
FROM sys_cron_log
WHERE cron_id = 1
ORDER BY id DESC
LIMIT 20;
如果 sys_cron.status = 0,但禁用后的时间点仍有新日志,优先看后端禁用接口是否执行到了 cronlib.StopJob(item.Id, item.Name)。如果没有这一步,页面上"禁用"只是配置禁用,运行中的 job 还会等下一次 tick。
还有一个容易忽略的点:保存任务也要同步调度。很多后台只在服务启动时加载已启用任务,编辑表达式或参数后没有重新注册,结果页面显示新表达式,实际运行还是旧节奏。XYGo Admin 的 Save 在写库后也调用 syncJobSchedule(ctx, id),这条链路值得照着检查。
go
// server/internal/logic/cron/cron.go
func Save(ctx context.Context, in *adminin.CronSaveInp) (uint64, error) {
// 省略参数检查和 Insert/Update
syncJobSchedule(ctx, id)
return id, nil
}
如果你的项目没有类似逻辑,最小修复不是重启服务,而是在保存和状态变更两个入口都补一次调度同步。
子问题二:点击"立即执行"成功,能证明定时调度正常吗
不能。在线执行和定时调度是两条链路。
在线执行只说明这个任务标识能找到、任务函数能跑一次,不能证明它已经按 cron 表达式加入调度器。源码里这两个入口是分开的:
go
func OnlineExec(ctx context.Context, id uint64) (string, error) {
var item entity.SysCron
err := dao.SysCron.Ctx(ctx).Where("id", id).Scan(&item)
if err != nil {
return "", err
}
if item.Id == 0 {
return "", gerror.New("任务不存在")
}
return cronlib.OnlineExec(&cronlib.CronJob{
Id: item.Id,
Name: item.Name,
Title: item.Title,
Params: item.Params,
})
}
所以排查时不要只点"立即执行"。更可靠的做法是:
-
把表达式临时改成一个短周期,比如每分钟一次。
-
保存后确认接口返回成功。
-
等待两个周期。
-
查
sys_cron_log是否新增记录。 -
再把表达式改回去。
如果在线执行成功,但周期日志不新增,问题多半在 StartJob、表达式解析、任务状态同步,或进程里没有真正启动调度器。
子问题三:执行日志没有新增,要查回调还是查列表 total
先分清"没有写入"和"写入了但页面没显示"。
应用内任务要能留下执行记录,一般需要调度器在任务结束时回调业务层写日志。XYGo Admin 的记录入口是 recordLog:
go
func recordLog(ctx context.Context, cronId uint64, name, title, params string, status int, output, errMsg string, takeMs int) {
_, _ = dao.SysCronLog.Ctx(ctx).Data(g.Map{
"cron_id": cronId,
"name": name,
"title": title,
"params": params,
"status": status,
"output": output,
"err_msg": errMsg,
"take_ms": takeMs,
"created_at": uint(time.Now().Unix()),
}).Insert()
}
如果表里没有记录,就查调度器有没有设置日志回调。这个调用在服务启动时完成:
go
func StartAll(ctx context.Context) {
cronlib.SetLogCallback(recordLog)
// 加载 status=1 的任务并 StartJob
}
如果表里有记录,页面却显示空,才去查列表接口和分页 total。后端列表至少要做两件事:先 Count(),再分页 Scan(),最后把 total 放回响应。
go
count2, err := m.Clone().Count()
// ...
return &adminin.CronLogListModel{
List: list,
PageRes: form.PageRes{
Page: in.Page,
PageSize: in.PageSize,
Total: count2,
},
}, nil
控制器层也要确认没有把 total 丢掉。server/internal/controller/admin/cron.go 里 CronLogList 会把 result.Total 补回 PageRes.Total。这类细节看起来小,但会直接导致"明明有日志,前端分页显示 0"。
子问题四:清空日志失败,和启停不生效是一类问题吗
不是一类,但经常一起出现,因为都在"定时任务管理"页面里。
清空日志失败常见原因是 ORM 不允许无条件删除。GoFrame 对无 WHERE 的 Delete 有保护,直接清空整张日志表可能被拦住。XYGo Admin v1.4.6 的 Release 里提到过"定时任务执行日志清空全部失效",源码里的修复方式是清空全部时补一个恒真条件:
go
func LogClear(ctx context.Context, in *adminin.CronLogClearInp) error {
m := dao.SysCronLog.Ctx(ctx)
if in.CronId > 0 {
m = m.Where("cron_id", in.CronId)
} else {
m = m.Where("1=1")
}
_, err := m.Delete()
return err
}
这个修复只解决"清空日志"按钮,不解决任务启停。别把两个问题混在一起修。启停不生效查 UpdateStatus -> syncJobSchedule -> StartJob/StopJob;清空失败查 LogClear -> Delete 的 WHERE 条件。
可复制的排查清单
排查线上问题时,可以按下面顺序跑,不要一上来就重启服务。
bash
# 1. 查任务配置是否真实更新
SELECT id,title,name,pattern,status,updated_at FROM sys_cron WHERE name='你的任务标识';
# 2. 查最近执行记录
SELECT id,cron_id,name,status,err_msg,take_ms,created_at
FROM sys_cron_log
WHERE name='你的任务标识'
ORDER BY id DESC
LIMIT 20;
# 3. 查后端是否存在两个同步入口
# 保存任务后:Save -> syncJobSchedule
# 切换状态后:UpdateStatus -> syncJobSchedule
# 4. 区分在线执行和周期调度
# 在线执行成功,只能证明任务函数能跑一次,不能证明 StartJob 生效
如果你有接口访问日志,再补一条:看点启停时是否真的请求了 /admin/cron/status,点立即执行时是否请求了 /admin/cron/onlineExec。这两个 URL 在 api/admin/admin_cron.go 和前端 web/src/api/backend/system/cron.ts 里都能对上。
子问题五:保存任务后马上生效,是否一定安全
保存后马上同步调度是必要的,但还要补两个保护。
一个是任务标识必须存在。否则用户在后台随手填一个 name,数据库能保存,调度器却找不到对应函数,到了执行时间只会失败。源码里保存前会检查注册表:
go
if cronlib.GetTask(in.Name) == nil {
return 0, gerror.Newf("任务标识 '%s' 未在代码中注册,请先在 internal/crons/ 中实现并注册", in.Name)
}
另一个是任务标识最好做唯一性检查。两个任务共用同一个 name,后面停止或启动时就很容易停错、覆盖或重复注册。排查重复时可以直接查表:
sql
SELECT name, COUNT(*) AS c
FROM sys_cron
GROUP BY name
HAVING COUNT(*) > 1;
如果这条 SQL 有结果,先修数据,再查调度器。否则你会看到很怪的现象:A 任务禁用了,B 任务也停了;或者页面只显示一个任务在跑,日志里却出现两个相同 name 的记录。
适用边界
这套方法适用于 GoFrame、Gin 或其他 Go 后台里"数据库保存任务配置,进程内 cron 调度器负责执行"的设计。只要配置层和运行层分离,就要查同步链路。
它不适用于几类场景:Linux crontab 没跑、Kubernetes CronJob 没创建 Pod、消息队列 Worker 消费失败、浏览器里的 setInterval 定时器异常。这些问题的调度器不在 Go 进程内,排查入口完全不同。
也别把它当成性能优化文章。这里解决的是状态一致性:页面、数据库、内存调度器和执行日志要对得上。
源码案例可看 GitHub 仓库:GitHub - z312193608/xygo-admin: Open-source admin framework built with GoFrame v2 and Vue 3, featuring RBAC, full-stack CRUD code generation, MySQL/PostgreSQL, plugins, and single-binary deployment. · GitHub。核验版本是 2026-08-10 的 master 分支和 v1.4.6 Release。读源码时重点看 server/internal/logic/cron/cron.go、server/internal/controller/admin/cron.go、server/api/admin/admin_cron.go 和 web/src/api/backend/system/cron.ts。如果你的项目也有"状态改了但任务照跑"的情况,先别急着加锁或重启,把这条链路打一遍,通常能很快定位到漏同步的入口。真正要修的是保存、启停、启动加载和日志回调这几个入口是否一致;只改其中一个入口,线上迟早还会出现页面状态和实际调度不一致。