cron,相信玩过Linux的朋友应该都对这个很熟悉吧,这不就是计划任务嘛,当你需要某个时间段去执行某一件事的时候,普通的执行方式已经不能满足我们的需求了,这个时候就需要一个定时任务了,那么下面我就来介绍一个Go的开源定时任务框架。

一、安装

# 下载
go get github.com/robfig/cron/v3@v3.0.0

# 导入:这里后面加个v3意味着这是v3版本,注意不要搞错
import "github.com/robfig/cron/v3"

新建一个Go项目,直接在项目中执行命令下载依赖,这里下载的是官网的最新版本,我们介绍也是采用的v3版本,注意这里不能搞错了,v3版本跟之前的版本表达式写法还是有一点点不同的,此处笔者就曾踩坑,其实就是自己没有看清楚版本就开始用,导致在这个地方翻车了。 并且官网给出的说明中突出体现了由于使用Go模块,它需要Go 1.11或更高版本的环境,如果你的环境不满足这个要求的话,那你就无法使用这个,建议使用之前的版本。不过推荐还是使用v3版本,这个版本相较之前的版本做了不少的改动,还是很值得推荐的。具体的话我这里就不过多描述了,文档中写的很详细,可以看下v3的文档https://pkg.go.dev/github.com/robfig/cron/v3#section-readme

二、用法

c := cron.New()
c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") })
c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") })
c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") })
c.AddFunc("@hourly",      func() { fmt.Println("Every hour, starting an hour from now") })
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty, starting an hour thirty from now") })
c.Start()
..
// Funcs are invoked in their own goroutine, asynchronously.
...
// Funcs may also be added to a running Cron
c.AddFunc("@daily", func() { fmt.Println("Every day") })
..
// Inspect the cron job entries' next and previous run times.
inspect(c.Entries())
..
c.Stop()  // Stop the scheduler (does not stop any jobs already running).

这是官网给出的例子,如果你使用v3的话,不妨可以直接试试看,但你注意,如果你是直接用的之前的cron表达式的话,那么在这里执行的时候就会报错,因为v3跟之前以及其他cron框架不同的一点在于它的cron表达式只有5个参数,它默认的是从分开始解析的,而不是秒,这个如果不看文档直接执行的话,那么就会有一个报错抛给你,这一点需要注意。 cron 表达式表示一组时间,使用 5个空格分隔的字段,这是每个字段具体的含义,跟之前的就少了个秒而已,其实无伤大雅。

Field name   | Mandatory? | Allowed values  | Allowed special characters
----------   | ---------- | --------------  | --------------------------
Minutes      | Yes        | 0-59            | * / , -
Hours        | Yes        | 0-23            | * / , -
Day of month | Yes        | 1-31            | * / , - ?
Month        | Yes        | 1-12 or JAN-DEC | * / , -
Day of week  | Yes        | 0-6 or SUN-SAT  | * / , - ?

那么这个时候有的小伙伴可能就会问了,v3版本的从分开始,那我之前的都是从秒开始,并且我现在还是有精确到秒的定时任务需要处理,那我还可以用这个框架嘛?答案是可以的,人家官方就知道这种肯定一时间无法全部摒弃秒级表达式,就告诉你可以使用自定义解析器来实现它。

cron.New(
	cron.WithParser(
		cron.NewParser(
			cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)))

这东西乍一看是不是感觉很懵,不要着急,这个看不懂也无妨,官方知道由于添加秒数是对标准 cron 规范最常见的修改,因此提供了一个内置函数来执行此操作,这相当于上面看到的自定义解析器,只是它的秒数字段是必需的。

cron.New(cron.WithSeconds())

这个时候cron表达式就是6个字段了,不能再少秒了,否则也会报错。 特殊字符的用法跟其他的cron表达式是一致的,这个没有区别,这里简单介绍一下。

Asterisk ( * )
  • 表示 cron表达式将匹配该字段的所有值;例如,在第 5 个字段(月份)中使用星号将表示每个月。
Slash ( / )

/ 用于描述范围的增量。例如,第一个字段(分钟)中的 3-59/15 表示该小时的第 3 分钟以及此后每 15 分钟一次。形式“*/...”相当于形式“first-last/...”,即在字段的最大可能范围内递增。 “N/...”形式被接受为“N-MAX/...”,即从 N 开始,使用增量直到该特定范围的末尾。它不会环绕。

Comma ( , )

, 用于分隔列表中的项目。例如,在第 5 个字段(星期几)中使用“MON,WED,FRI”将表示星期一、星期三和星期五。

Hyphen ( - )
  • 用于定义范围。例如,9-17 表示上午 9 点到下午 5 点之间的每小时(含)。
Question mark ( ? )

? 可以使用问号代替“*”来将月份中的某一天或星期几留空。

三、特性

预定义计划

它是cron定义的一些预定义计划,用来替代cron表达式。

Entry                  | Description                                | Equivalent To
-----                  | -----------                                | -------------
@yearly (or @annually) | Run once a year, midnight, Jan. 1st        | 0 0 1 1 *
@monthly               | Run once a month, midnight, first of month | 0 0 1 * *
@weekly                | Run once a week, midnight between Sat/Sun  | 0 0 * * 0
@daily (or @midnight)  | Run once a day, midnight                   | 0 0 * * *
@hourly                | Run once an hour, beginning of hour        | 0 * * * *

指定间隔

@every <duration>
以固定时间间隔运行,“@every 1h30m10s”表示在 1小时、30分钟、10秒后激活的计划,然后是此后的每个间隔。

时区

默认情况下,所有解释和调度都在计算机的本地时区 (time.Local) 中完成。您可以在构建时指定不同的时区。

cron.New(
    cron.WithLocation(time.UTC))

各个 cron 计划还可以通过在 cron 规范的开头提供额外的空格分隔字段(格式为“CRON_TZ=Asia/Tokyo”)来覆盖它们要解释的时区。

# Runs at 6am in time.Local
cron.New().AddFunc("0 6 * * ?", ...)

# Runs at 6am in America/New_York
nyc, _ := time.LoadLocation("America/New_York")
c := cron.New(cron.WithLocation(nyc))
c.AddFunc("0 6 * * ?", ...)

# Runs at 6am in Asia/Tokyo
cron.New().AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...)

# Runs at 6am in Asia/Tokyo
c := cron.New(cron.WithLocation(nyc))
c.SetLocation("America/New_York")
c.AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...)

作业包装器

Cron 运行程序可以配置一系列作业包装器,以向所有提交的作业添加横切功能。

  • 从作业中恢复所有异常(默认);
  • 如果上一次运行尚未完成,则延迟作业的执行;
  • 如果上一个运行尚未完成,则跳过作业的执行;
  • 记录每个作业的调用。

使用 cron.WithChain 选项为添加到 cron 的所有作业安装包装器:

cron.New(cron.WithChain(
	cron.SkipIfStillRunning(logger),
))

线程安全

由于 cron服务与调用代码同时运行,因此必须小心确保正确的同步。官方为cron设计成了线程安全的,因此只要调用者确保调用之间有明确的发生之前顺序。

日志

cron为我们定义了一个Logger接口,它是 github.com/go-logr/logr 中定义的接口的子集。它有两个日志记录级别(信息和错误),参数是键/值对。这使得 cron 日志记录能够插入结构化日志记录系统。提供了一个适配器 [Verbose]PrintfLogger 来包装标准库 *log.Logger。 为了进一步了解 Cron 操作,可以激活详细日志记录,这将记录作业运行、调度决策以及添加或删除的作业。

cron.New(
	cron.WithLogger(
		cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))))

实施

Cron 条目存储在一个数组中,按下次激活时间排序。Cron 会休眠,直到下一个作业运行为止。

至此,cron的介绍就到此为止了,看了这些,基本上你也可以应对普通的定时任务了,当然,有不懂的地方你也可以自己翻阅一下官方文档,描述还是很清晰的呢。