Go Cron 定时任务开发指南
本文内容基于官方文档翻译,为保证技术准确性关键术语保留英文原文,部分示例已进行本土化适配。
安装
要下载特定版本的发布包,请运行:
go get github.com/robfig/cron/v3@v3.0.0
在程序中导入:
import "github.com/robfig/cron/v3"
由于使用了 Go Modules,需要 Go 1.11 或更高版本。
使用方式
调用者可以注册函数在指定时间表上执行。Cron 将在自己的 goroutine 中运行这些函数。
c := cron.New()
c.AddFunc("30 * * * *", func() { fmt.Println("每小时半点执行") })
c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println("在凌晨3-6点和晚上8-11点范围内执行") })
c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() { fmt.Println("每天东京时间04:30执行") })
c.AddFunc("@hourly", func() { fmt.Println("每小时执行一次,从现在开始一小时后") })
c.AddFunc("@every 1h30m", func() { fmt.Println("每1小时30分钟执行一次,从现在开始1小时30分钟后") })
c.Start()
...
// 函数在它们自己的goroutine中异步执行
...
// 也可以向运行中的Cron添加函数
c.AddFunc("@daily", func() { fmt.Println("每天执行") })
...
// 检查cron任务的下次和上次运行时间
inspect(c.Entries())
...
c.Stop() // 停止调度器(不会停止已经运行的作业)
CRON 表达式格式
cron 表达式使用 5 个空格分隔的字段表示一组时间。
字段 | 是否必需 | 允许值 | 允许的特殊字符 |
---|---|---|---|
分钟 | 是 | 0-59 | * / , - |
小时 | 是 | 0-23 | * / , - |
每月中的某天 | 是 | 1-31 | * / , - ? |
月份 | 是 | 1-12 或 JAN-DEC | * / , - |
星期几 | 是 | 0-6 或 SUN-SAT | * / , - ? |
月份和星期几字段值不区分大小写。“SUN”、“Sun"和"sun"都是等效的。
格式的具体解释基于 Cron 维基百科页面:https://en.wikipedia.org/wiki/Cron
替代格式
替代的 Cron 表达式格式支持其他字段,如秒。您可以通过创建自定义解析器来实现:
cron.New(
cron.WithParser(
cron.NewParser(
cron.SecondOptional | cron.Minute | cron.Hour |
cron.Dom | cron.Month | cron.Dow | cron.Descriptor)))
由于添加秒是最常见的标准 cron 规范修改,cron 提供了一个内置函数来实现:
cron.New(cron.WithSeconds())
特殊字符
星号 (*)
星号表示 cron 表达式将匹配字段的所有值。
例如,在第 5 个字段(月份)中使用星号表示每个月。
斜杠 (/)
斜杠用于描述范围的增量。
例如,第 1 个字段(分钟)中的 3-59/15 表示每小时的第 3 分钟开始,之后每 15 分钟一次。"*/…“形式等同于"first-last/…",即字段最大可能范围的增量。“N/…“形式表示从 N 开始,使用增量直到该特定范围的结束。
逗号 (,)
逗号用于分隔列表项。
例如,在第 5 个字段(星期几)中使用"MON,WED,FRI"表示星期一、星期三和星期五。
连字符 (-)
连字符用于定义范围。
例如,9-17 表示上午 9 点到下午 5 点之间的每小时(含)。
问号 (?)
问号可以代替’*‘用于留空每月中的某天或星期几字段。
预定义调度
您可以使用几个预定义的调度来代替 cron 表达式。
条目 | 描述 | 等同于 |
---|---|---|
@yearly (or @annually) | 每年运行一次,1 月 1 日午夜 | 0 0 1 1 * |
@monthly | 每月运行一次,每月第一天午夜 | 0 0 1 * * |
@weekly | 每周运行一次,周六/周日午夜 | 0 0 * * 0 |
@daily (or @midnight) | 每天运行一次,午夜 | 0 0 * * * |
@hourly | 每小时运行一次,整点开始 | 0 * * * * |
间隔
您还可以安排作业以固定间隔执行,从添加时或 cron 运行时开始。这通过格式化 cron 规范如下实现:
@every <duration>
其中"duration"是 time.ParseDuration 接受的字符串。
例如:"@every 1h30m10s"表示一个调度,在 1 小时 30 分钟 10 秒后激活,然后每隔该间隔执行一次。
注意:间隔不考虑作业运行时间。例如,如果一个作业需要 3 分钟运行,并且计划每 5 分钟运行一次,那么每次运行之间只有 2 分钟的空闲时间。
时区
默认情况下,所有解释和调度都在机器的本地时区 ( time.Local ) 中完成。
您可以在构造时指定不同的时区:
cron.New(
cron.WithLocation(time.UTC))
单个 cron 调度也可以通过在其规范开头提供额外的空格分隔字段来覆盖它们被解释的时区,格式为"CRON_TZ=Asia/Tokyo”。
例如:
// 在time.Local时区早上6点运行
cron.New().AddFunc("0 6 * * ?", ...)
// 在America/New_York时区早上6点运行
nyc, _ := time.LoadLocation("America/New_York")
c := cron.New(cron.WithLocation(nyc))
c.AddFunc("0 6 * * ?", ...)
// 在Asia/Tokyo时区早上6点运行
cron.New().AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...)
// 在Asia/Tokyo时区早上6点运行
c := cron.New(cron.WithLocation(nyc))
c.SetLocation("America/New_York")
c.AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...)
前缀"TZ=(TIME ZONE)“也支持以实现向后兼容。
注意:在夏令时向前调整期间安排的作业将不会运行!
作业包装器
Cron 运行器可以配置一系列作业包装器,为所有提交的作业添加横切关注点功能。
例如,它们可用于实现以下效果:
- 从作业中恢复任何 panic(默认激活)
- 如果前一次运行尚未完成,则延迟作业执行
- 如果前一次运行尚未完成,则跳过作业执行
- 记录每个作业的调用
使用 cron.WithChain 选项为添加到 cron 的所有作业安装包装器:
cron.New(cron.WithChain(
cron.SkipIfStillRunning(logger),
))
通过显式包装为单个作业安装包装器:
job = cron.NewChain(
cron.SkipIfStillRunning(logger),
).Then(job)
线程安全
由于 Cron 服务与调用代码并发运行,必须采取一定措施确保适当的同步。
所有 Cron 方法都设计为正确同步,只要调用者确保调用之间具有明确的 happens-before 顺序。
日志记录
Cron 定义了一个 Logger 接口,它是 github.com/go-logr/logr 中定义接口的子集。它有两个日志级别(Info 和 Error),参数是键/值对。这使得 Cron 日志记录可以插入结构化日志系统。提供了一个适配器[Verbose]PrintfLogger 来包装标准库的*log.Logger。
为了深入了解 Cron 操作,可以激活详细日志记录,它将记录作业运行、调度决策以及添加或删除的作业。
使用一次性记录器激活它:
cron.New(
cron.WithLogger(
cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))))
实现
Cron 条目存储在数组中,按其下次激活时间排序。Cron 休眠直到下一个作业应该运行。
唤醒时:
- 它运行在该秒活动的每个条目
- 计算已运行作业的下次运行时间
- 按下次激活时间重新排序条目数组
- 休眠直到最早的作业