定时任务应该这样写

一、Cron表达式

sql 复制代码
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 * * * * *"。这六位数分别表示秒,分,小时,每月第几天,月,每个星期第几天;

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

  • *:代表任一志;*在分钟字段,表示每分钟;
  • /:用来指定时间间隔,*/15在分钟字段,表示每隔15分钟;
  • ,:列出多个离散值,1,15在天字段,表示每月1号和15号;
  • -:定义某个范围,9-17在小时字段,表示上午9点到下午5点,两边都是闭区间;
  • ?:表示无特定值。在Cron中,如果天数与星期的指定会互斥。看下面两个例子:
    • 0 0 12 ? * WED - 表示每周三中午12点。关心星期,忽略天数;
    • 0 0 12 15 * ? - 表示每个月的第15天中午12点。关心天数,忽略星期;

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

less 复制代码
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包?

go 复制代码
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()
}

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

  • c.AddFunc()
  • c.Schedule()

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

三、如何设计一个Cron?

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

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

go 复制代码
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

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

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

go 复制代码
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传输快照

go 复制代码
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 *",系统是如何知道每年的第一个月执行相应的任务呢?下面代码,以月份为例。

程序的大致流程:

  1. 将月份规则转化为二进制数值;
  2. 通过当前时间不断+1,直到匹配规则月份;

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

go 复制代码
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. 实现接口的函数

go 复制代码
// 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接口中;

参考

相关推荐
往事随风去9 分钟前
Redis的内存淘汰策略(Eviction Policies)有哪些?
redis·后端·算法
秦禹辰17 分钟前
宝塔面板安装MySQL数据库并通过内网穿透工具实现公网远程访问
开发语言·后端·golang
lypzcgf27 分钟前
Coze源码分析-资源库-删除插件-后端源码-应用和领域服务层
后端·go·coze·coze插件·coze源码分析·智能体平台·ai应用平台
lssjzmn31 分钟前
Spring Web 异步响应实战:从 CompletableFuture 到 ResponseBodyEmitter 的全链路优化
java·前端·后端·springboot·异步·接口优化
shark_chili36 分钟前
程序员必知的底层原理:CPU缓存一致性与MESI协议详解
后端
愿时间能学会宽恕1 小时前
SpringBoot后端开发常用工具详细介绍——SpringSecurity认证用户保证安全
spring boot·后端·安全
CodeSheep1 小时前
稚晖君又开始摇人了,有点猛啊!
前端·后端·程序员
小宁爱Python1 小时前
Django 从环境搭建到第一个项目
后端·python·django
uzong1 小时前
深入浅出:画好技术图
后端·架构
IT_陈寒1 小时前
Java性能优化:从这8个关键指标开始,让你的应用提速50%
前端·人工智能·后端