介绍
在开发go语言项目时,对一些容易变化的资源进行一系列的定时监控。设置一系列的前置条件,当条件满足则触发告警请求。
使用场景:
- log日志存储文件中的日志信息过多,当到达500M时,进行报警操作。
- redis缓存键值对过多,进行报警操作。
对定时任务的使用,是根据其业务逻辑来制定的。
逻辑图:
第三方依赖包
go get github.com/robfig/cron/v3@v3.0.0
robfig/cron/v3是一个基于go语言的第三方库,用于在应用程序中实现定时任务的调度和管理。允许按照预定的时间间隔或时间点自动执行指定的任务。可以自定义定时任务并设置其执行规则。比如,每天的特定时间、每周的某一天。该库还可以支持定时任务的并发执行。
/*创建了一个cron的调度器*/
var Client = cron.New()
func Test01() {
//每隔1秒,执行func(){}
_, err := Client.AddFunc("@every 1s", func() {
fmt.Println("execing", time.Now().Format("2006-01-02 15:04:05"))
})
if err != nil {
fmt.Println(err)
}
//调度器开始
Client.Start()
//睡眠5分钟
time.Sleep(5 * time.Minute)
//调度器结束
Client.Stop()
}
execing 2023-07-08 09:58:12
execing 2023-07-08 09:58:13
execing 2023-07-08 09:58:14
execing 2023-07-08 09:58:15
execing 2023-07-08 09:58:16
execing 2023-07-08 09:58:17
execing 2023-07-08 09:58:18
在上述代码中,创建了一个调度器client,在其方法AddFunc("间隔时间",执行函数)实现了定时任务的执行。这只是一个简单的demo。接下来分析其源码:
var Client = cron.New()
func New(opts ...Option) *Cron {
c := &Cron{
entries: nil,
chain: NewChain(),
add: make(chan *Entry),
stop: make(chan struct{}),
snapshot: make(chan chan []Entry),
remove: make(chan EntryID),
running: false,
runningMu: sync.Mutex{},
logger: DefaultLogger,
location: time.Local,
parser: standardParser,
}
for _, opt := range opts {
opt(c)
}
return c
}
解释:在创建调度器时,可以传入参数,自定义调度器。可以对调度器做以下配置:
1.设置时间解析器
2.设置日志记录器
3.设置时区
4.设置并发执行数
一般来说,没有人想去自定义定时任务的调度器。因为默认的调度器就很完美了,几乎可以按满足需求。项目中使用调度器也就做个定时任务而已。如果有自定义配置调度器的需要,请查阅文档。
Client.AddFunc("@every 1s", func() {
fmt.Println("execing", time.Now().Format("2006-01-02 15:04:05"))
})
func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {
return c.AddJob(spec, FuncJob(cmd))
}
可以看出client.AddFunc()接收两个参数,一个时间表达式、一个执行任务的函数。关于cron包的时间表达式我放在下一主题讲解。在AddFunc()源码中,其底层也是实现了AddJob()的方法。对AddFunc()只需要了解如下信息:
- AddFunc()可以被同一个client多次调用,执行多个定时任务
- 在client.start()时,才开始进行任务
- AddFunc()内的任务是并发的,是不会相互影响的。即使在上一个任务还未执行完毕,下一个任务在时间到了后也会按照预期的时间策略触发
c.Start()
// Start the cron scheduler in its own goroutine, or no-op if already started.
func (c *Cron) Start() {
c.runningMu.Lock()
defer c.runningMu.Unlock()
if c.running {
return
}
c.running = true
go c.run()
}
- 当你想要执行已添加到Cron对象的若干个定时任务时,即可调用start()
- 在调用start()方法后,cron对象会按照预设的时间策略来自动触发定时任务
- Cron对象会启动一个后台的调度器协程own goroutine,其负责计算和判断任务触发的时间,并执行任务
- 在调用
c.Start()方法之前,必须先向 Cron 对象中添加任务,否则调度器无法执行任何任务。 - 一旦调用了
Start()方法,任务调度器将不会停止,直到显式地调用了c.Stop()方法或程序结束。 - 如果在任务调度期间有新的任务添加进来,调度器会即时生效,无需重新启动。
- 如果任务执行时间过长而导致错过了下一次预定的触发时间,调度器会等待任务执行完毕后立即开始下一次触发。
Client.Stop()
解释:停止调度器运行
cron包的时间表达式
在cron包的时间策略两个方面:一是在指定时间点触发一次任务,二是每隔多少时间触发一次任务。
这是特定时间点的常用写法:一个 cron 表达式是一个由 5 个空格分隔的字符串,每一部分从左到右分别表示 分, 时, 天,月, 星期,每个部分由数字和一些特殊字符表示一个约定的时间项,在 robfig/cron 中,每一部分允许的特殊字符为*。
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 | * / , - ?
"30 * * * *" 表示在*:*:*:*:30分钟执行一次任务,即只要分钟数达到30时,即执行。说白了就是每隔30
"31 5 8 6 *" 表示在每一年的6月8号5点31分0秒时,执行一次任务
相信大家已经在心里默默的说了句MMP,是的,这就是个反人类的设计。看着很不舒服。接下来说的是时间间隔的方式,也是最常用的时间策略。
@every 10m 表示每隔10分钟执行一次任务
@every 1s 表示每隔1秒执行一次任务
这个时间间隔,是以当前时间为基准的。
对比上述两种时间策略。总的来说各有千秋吧。如果定时任务执行的时间期限长,比如,一年只执行一次。那可以选择第一种时间策略。如果是每天都要测一次,那就用第二种。
实战
在官方文档里,还有很多的函数讲解。但是一般开发也用不到很多。我这里就以实战来讲解了。
package timer
import (
"sync"
"github.com/robfig/cron/v3"
)
//定义了一个接口,下列是实现这些接口的方法
type Timer interface {
AddTaskByFunc(taskName string, spec string, task func(), option ...cron.Option) (cron.EntryID, error)
AddTaskByJob(taskName string, spec string, job interface{ Run() }, option ...cron.Option) (cron.EntryID, error)
FindCron(taskName string) (*cron.Cron, bool)
StartTask(taskName string)
StopTask(taskName string)
Remove(taskName string, id int)
Clear(taskName string)
Close()
}
// timer 定时任务管理【taskLis内部存入调度器和任务名称】
type timer struct {
taskList map[string]*cron.Cron
sync.Mutex
}
// AddTaskByFunc 通过函数的方法添加任务,自定义封装AddFunc() 加锁 有start() 激活状态
func (t *timer) AddTaskByFunc(taskName string, spec string, task func(), option ...cron.Option) (cron.EntryID, error) {
t.Lock()
defer t.Unlock()
if _, ok := t.taskList[taskName]; !ok {
t.taskList[taskName] = cron.New(option...)
}
id, err := t.taskList[taskName].AddFunc(spec, task)
t.taskList[taskName].Start()
return id, err
}
// AddTaskByJob 通过接口的方法添加任务,与上述的AddTaskByFunc()是一样的,都实现了AddJob()
func (t *timer) AddTaskByJob(taskName string, spec string, job interface{ Run() }, option ...cron.Option) (cron.EntryID, error) {
t.Lock()
defer t.Unlock()
if _, ok := t.taskList[taskName]; !ok {
t.taskList[taskName] = cron.New(option...)
}
id, err := t.taskList[taskName].AddJob(spec, job)
t.taskList[taskName].Start()
return id, err
}
// FindCron 获取对应taskName的cron 可能会为空
func (t *timer) FindCron(taskName string) (*cron.Cron, bool) {
t.Lock()
defer t.Unlock()
v, ok := t.taskList[taskName]
return v, ok
}
// StartTask 开始任务
func (t *timer) StartTask(taskName string) {
t.Lock()
defer t.Unlock()
if v, ok := t.taskList[taskName]; ok {
v.Start()
}
}
// StopTask 停止任务
func (t *timer) StopTask(taskName string) {
t.Lock()
defer t.Unlock()
if v, ok := t.taskList[taskName]; ok {
v.Stop()
}
}
// Remove 从taskName 删除指定任务
func (t *timer) Remove(taskName string, id int) {
t.Lock()
defer t.Unlock()
if v, ok := t.taskList[taskName]; ok {
v.Remove(cron.EntryID(id))
}
}
// Clear 清除任务
func (t *timer) Clear(taskName string) {
t.Lock()
defer t.Unlock()
if v, ok := t.taskList[taskName]; ok {
v.Stop()
delete(t.taskList, taskName)
}
}
// Close 释放资源
func (t *timer) Close() {
t.Lock()
defer t.Unlock()
for _, v := range t.taskList {
v.Stop()
}
}
func NewTimerTask() Timer {
return &timer{taskList: make(map[string]*cron.Cron)}
}
上述代码是封装了cron的原始函数,也就是加了一个锁。
func hander() {
// 获取Timer接口
t := timer.NewTimerTask()
// 开始设置定时任务,默认为激活状态
_, err := t.AddTaskByFunc("taskName", "@every 10m", func() {
//此处可以设计业务逻辑并报警
}
}
报警
常见的报警有很多种:发送电子邮件、微信通知、QQ通知、打电话、企业微信机器人等
我最喜欢用的即使企业微信机器人,因为简单。
type Message struct {
MsgType string `json:"msgtype"`
Text struct {
Content string `json:"content"`
} `json:"text"`
}
func Test09() {
webhookURL := "你自己的企业微信机器人的webhookURL"
message := Message{
MsgType: "text",
Text: struct {
Content string `json:"content"`
}{
Content: "text",
},
}
data, err := json.Marshal(message)
if err != nil {
fmt.Println("JSON marshal error:", err)
return
}
req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(data))
if err != nil {
fmt.Println("HTTP request error:", err)
return
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("HTTP request error:", err)
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
fmt.Println("HTTP request success")
} else {
fmt.Println("HTTP request failed")
}
}
总结
没得总结,嘿嘿···