共计 3571 个字符,预计需要花费 9 分钟才能阅读完成。
Java 架构师成长直通车(40 周完结无密内附文档源码)
下载地址:百度网盘
解析 Golang 定时工作库 gron 设计和原理
简略说,每一个位都代表了一个工夫维度,* 代表选集,所以,下面的语义是:在每天早上的 4 点 05 分触发工作。
但 cron 毕竟只是一个操作系统级别的工具,如果定时工作失败了,或者压根没启动,cron 是没法提醒开发者这一点的。并且,cron 和 正则表达式都有一种魔力,不知道大家是否感同身受,这里引用共事的一句名言:
这世界上有些语言非常相似: shell 脚本, es 查问的那个 dsl 语言, 定时工作的 crontab, 正则表达式. 他们相似就相似在每次要写的时候基本都得从新现学一遍。
刚巧,最近看到了 gron 这个开源我的项目,它是用 Golang 实现一个并发安全的定时工作库。实现非常简略精美,代码量也不多。明天咱们就来一起拆散源码看一下,怎么基于 Golang 的能力做进去一个【定时工作库】。
Gron provides a clear syntax for writing and deploying cron jobs.
gron 是一个泰国小哥在 2016 年开源的作品,它的个性就在于非常简略和清晰的语义来定义【定时工作】,你不必再去记 cron 的语法。咱们来看下作为使用者怎么上手。
首先,咱们还是一个 go get 安装依赖:
$ go get github.com/roylee0704/gron
复制代码
假设咱们期望在【时机】到了当前,要做的工作是打印一个字符串,每一个小时执行一次,咱们就可能这样:
package main
import (
"fmt"
"time"
"github.com/roylee0704/gron"
)
func main() {
c := gron.New()
c.AddFunc(gron.Every(1*time.Hour), func() {fmt.Println("runs every hour.")
})
c.Start()
}
复制代码
非常简略,而且即便是在 c.Start 之后咱们依然可能增加新的定时工作进去。反对了很好的扩展性。
定时参数
留意到咱们调用 gron.New().AddFunc() 时传入了一个 gron.Every(1*time.Hour)。
这里其实你可能传入任何一个 time.Duration,从而把调度间隔从 1 小时调整到 1 分钟以至 1 秒。
除此之外,gron 还很贴心地封装了一个 xtime 包用来把常见的 time.Duration 封装起来,这里咱们开箱即用。
import “github.com/roylee0704/gron/xtime”
gron.Every(1 * xtime.Day)
gron.Every(1 * xtime.Week)
复制代码
很多时候咱们不只仅某个工作在当天运行,还心愿是咱们指定的时刻,而不是依赖程序启动工夫,机械地加 24 hour。gron 对此也做了很好的反对:
gron.Every(30 * xtime.Day).At(“00:00”)
gron.Every(1 * xtime.Week).At(“23:59”)
复制代码
咱们只需指定 At(“hh:mm”) 就可能实现在指定工夫执行。
源码解析
这一节咱们来看看 gron 的实现原理。
所谓定时工作,其实蕴含两个层面:
触发器。即咱们心愿这个工作在什么工夫点,什么周期被触发;
工作。即咱们在触发之后,心愿执行的工作,类比到咱们下面示例的 fmt.Println。
对这两个概念的封装和扩大是一个定时工作库必须考虑的。
而同时,咱们是在 Golang 的协程上跑程序的,意味着这会是一个长期运行的协程,否则你即便指定了【一个月后干 XXX】这个工作,程序两天后挂了,也就无奈实现你的诉求了。
所以,咱们还心愿有一个 manager 的角色,来治理咱们的一组【定时工作】,如何调度,什么时候启动,怎么停止,启动了当前还想加新工作是否反对。
Cron
在 gron 的体系里,Cron 对象(咱们下面通过 gron.New 创建进去的)就是咱们的 manager,而底层的一个个【定时工作】则对应到 Cron 对象中的一个个 Entry:
// Cron provides a convenient interface for scheduling job such as to clean-up
// database entry every month.
//
// Cron keeps track of any number of entries, invoking the associated func as
// specified by the schedule. It may also be started, stopped and the entries
// may be inspected.
type Cron struct {
entries []*Entry
running bool
add chan *Entry
stop chan struct{}
}
// New instantiates new Cron instant c.
func New() *Cron {
return &Cron{stop: make(chan struct{}),
add: make(chan *Entry),
}
}
复制代码
entries 就是定时工作的核心能力,它记录了一组【定时工作】;
running 用来标识这个 Cron 是否已经启动;
add 是一个 channel,用来反对在 Cron 启动后,新增的【定时工作】;
stop 同样是个 channel,留意到是空结构体,用来管制 Cron 的停止。这个其实是经典写法了,对日常开发也有借鉴意义,咱们待会儿会好好看一下。
咱们观察到,当调用 gron.New() 方法后,失去的是一个指向 Cron 对象的指针。此时只是初始化了 stop 和 add 两个 channel,没有启动调度。
Entry
重头戏来了,Cron 外面的 []*Entry 其实就代表了一组【定时工作】,每个【定时工作】可能简化理解为 < 触发器,工作 > 组成的一个 tuple。
// Entry consists of a schedule and the job to be executed on that schedule.
type Entry struct {
Schedule Schedule
Job Job
// the next time the job will run. This is zero time if Cron has not been
// started or invalid schedule.
Next time.Time
// the last time the job was run. This is zero time if the job has not been
// run.
Prev time.Time
}
// Schedule is the interface that wraps the basic Next method.
//
// Next deduces next occurring time based on t and underlying states.
type Schedule interface {
Next(t time.Time) time.Time
}
// Job is the interface that wraps the basic Run method.
//
// Run executes the underlying func.
type Job interface {
Run()
}
复制代码
Schedule 代表了一个【触发器】,或者说一个定时策略。它只蕴含一个 Next 方法,接受一个工夫点,业务要返回下一次触发调动的工夫点。
Job 则是对【工作】的抽象,只需要实现一个 Run 方法,没有入参出参。
除了这两个核心依赖外,Entry 结构还蕴含了【前一次执行工夫点】和【下一次执行工夫点】,这个目前可能忽略,只是为了辅助代码用。
按照工夫排序
// byTime is a handy wrapper to chronologically sort entries.
type byTime []*Entry
func (b byTime) Len() int { return len(b) }
func (b byTime) Swap(i, j int) {b[i], b[j] = b[j], b[i] }
// Less reports earliest
time i should sort before j.
// zero time is not earliest
time.
func (b byTime) Less(i, j int) bool {
if b[i].Next.IsZero() {return false}
if b[j].Next.IsZero() {return true}
return b[i].Next.Before(b[j].Next)
}
复制代码
这里是对 Entry 列表的简略封装,因为咱们可能同时有多个 Entry 需要调度,处理的次序很重要。这里实现了 sort 的接口, 有了 Len(), Swap(), Less() 咱们就可能用 sort.Sort() 来排序了。
此处的排序策略是按照工夫大小。