Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go定时任务cron

Go语言定时任务cron的设计与使用

作者:小范真是一把好手

这篇文章主要为大家详细介绍了Go语言中定时任务cron的设计与使用,文中的示例代码讲解详细,对我们深入掌握Go语言有一定的帮助,需要的可以参考下

一、Cron表达式

Field name   | Mandatory? | Allowed values  | Allowed special characters
----------   | ---------- | --------------  | --------------------------
Seconds      | Yes        | 0-59            | * / , -
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  | * / , - ?

Cron表达式的格式是通过六个字符表示:"1 * * * * *"。这六位数分别表示秒,分,小时,每月第几天,月,每个星期第几天;

在这里重点解释一下特殊字符:

0 0 12 ? * WED - 表示每周三中午12点。关心星期,忽略天数;

0 0 12 15 * ? - 表示每个月的第15天中午12点。关心天数,忽略星期;

同时在"github.com/robfig/cron/v3"包中预定义的Schedule,如下所示:

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

二、如何使用Cron包

func TestCron(t *testing.T) {
	c := cron.New(cron.WithSeconds())

	// 每分钟第一秒执行该任务
	c.AddFunc("1 * * * * *", func() {
		fmt.Println("Hello world!")
	})

    // 每10s执行一次任务
	sh := cron.Every(10 * time.Second)
	c.Schedule(sh, cron.FuncJob(func() {
		fmt.Println("you are ok")
	}))

	go func() {
		ticker := time.NewTicker(time.Second * 4)
		for {
			select {
			case <-ticker.C:
				fmt.Println("length: ", len(c.Entries()))
			}
		}
	}()

	// c.Start()
	c.Start()

	// Wait for the Cron job to run
	time.Sleep(5 * time.Minute)

	// Stop the Cron job scheduler
	c.Stop()
}

上述示例代码中,使用两种创建定时任务的方式,分别是:

cron包的使用非常简单,你只需要提供Job以及其执行的规则即可。

三、如何设计一个Cron

关于Cron,调用者所有的操作与系统执行对应的任务之间是异步的。因此,对于调用者来说,系统用例如下:

更进一步,可以查看下Cron提供的API:

type Cron struct {
	// Has unexported fields.
}
    Cron keeps track of any number of entries, invoking the associated func as
    specified by the schedule. It may be started, stopped, and the entries may
    be inspected while running.

func New(opts ...Option) *Cron
func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error)
func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error)
func (c *Cron) Entries() []Entry
func (c *Cron) Entry(id EntryID) Entry
func (c *Cron) Location() *time.Location
func (c *Cron) Remove(id EntryID)
func (c *Cron) Run()
func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID
func (c *Cron) Start()
func (c *Cron) Stop() context.Context

调用者添加完所有任务之后,系统的处理流程如下(从后台任务的角度看):

上述就是后台任务的流程,简化后的代码如下:

func (c *Cron) run() {
	// Figure out the next activation times for each entry.
	now := c.now()

	for {
		// Determine the next entry to run.
		// 将所有任务,按照下一次运行时间排序
    sort.Sort(byTime(c.entries))
    
		for {
			select {
			case now = <-timer.C:
				now = now.In(c.location)
				c.logger.Info("wake", "now", now)

				// Run every entry whose next time was less than now
				for _, e := range c.entries {
					if e.Next.After(now) || e.Next.IsZero() {
						break
					}
					c.startJob(e.WrappedJob)
					e.Prev = e.Next
					e.Next = e.Schedule.Next(now)
					c.logger.Info("run", "now", now, "entry", e.ID, "next", e.Next)
				}

    	// 新增一个任务
			case newEntry := <-c.add:
      	....
        // 添加任务到数组容器
        
    	 // 获取当前时刻,Cron里面所有的定时任务
			case replyChan := <-c.snapshot:
				replyChan <- c.entrySnapshot()
				continue

    	// 停止Cron
			case <-c.stop:
      	...
				return

    	// 移除某个定时任务
			case id := <-c.remove:
        ....
				c.removeEntry(id)

			}

			break
		}
	}
}

四、学习点

1. 通过channel传输快照

func (c *Cron) Entries() []Entry {
    c.runningMu.Lock()
    defer c.runningMu.Unlock()

    // 如果Cron,正在运行,那么返回一个通道
    if c.running {
        replyChan := make(chan []Entry, 1)
        c.snapshot <- replyChan
        return <-replyChan
    }

    // 如果Cron,已经结束了,直接返回所有Entry
    return c.entrySnapshot()
}

这种写法特别有意思。当调用者想查看当前系统所有的任务时,系统返回的是一个通道,接着在通道中返回所有的数据。具体时序图如下所示:

下面这个架构图画的不是很好,画都画了就放这吧。

2. 匹配规则

读到cron这个项目,你是否有这样的疑问?cron后台任务根据调用给定的规则,如何执行任务的呢?比如"* * * * 1 *",系统是如何知道每年的第一个月执行相应的任务呢?下面代码,以月份为例。

程序的大致流程:

这里主要借助下面这个函数:

func getBits(min, max, step uint) uint64 {
    var bits uint64

    // If step is 1, use shifts.
    if step == 1 {
        return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
    }

    // Else, use a simple loop.
    for i := min; i <= max; i += step {
        bits |= 1 << i
    }
    return bits
}

func TestGetBits(t *testing.T) {
    res := getBits(1, 3, 1)

    fmt.Printf("%d 的二进制表示是 %b\n", res, res)
}

3. 实现接口的函数

// Job is an interface for submitted cron jobs.
type Job interface {
    Run()
}

type FuncJob func()

func (f FuncJob) Run() { f() }

上述代码定义Job接口、FuncJob类型,并且函数类型实现了Job接口。这种写法很常见,比如http.HandleFunc。这样写的好处,能够将一个函数强转之后直接丢到接口参数中,具体转化流程如下:

func() 类型函数 -- 强转:FuncJob(func()) -- FuncJob -- 可以丢进Job接口中;

到此这篇关于Go语言定时任务cron的设计与使用的文章就介绍到这了,更多相关Go定时任务cron内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文