148. Go实现简易Cron能力

一、Linux Cron介绍

1.引言

定时任务在日常的开发中使用的频率越来越高,比如电商系统每天进行的数据统计任务,再比如信用卡还款日提醒等等,都可能会使用定时任务来实现。所以如何实现定时任务是我们经常碰到的问题,采用哪种方案,可能影响到系统的架构设计。在系统的不断迭代的过程中,定时任务的实现方式也会一直会跟随着演进。本篇我们介绍定时任务的实现方案之一cron

2.cron是什么

cronlinux系统中,以后台进程模式周期性地执行命令或指定程序任务的服务软件。默认安装完linux系统之后,cron服务软件便会启动,服务对应的进程名字为crondcron服务会定期(默认每分钟检查一次)检查系统中是否有需要执行的任务工作计划。cron工具对应的crontab命令依赖于crond服务。

3.cron的适用场景

它非常适合定期运行脚本、备份数据、清理临时文件等一系列重复性任务。

4.cron原理

crond服务工作时会以每分钟为单位查看在/var/spool/cron路径下以系统用户名命名的定时任务文件,确定是否有需要执行的任务计划。如果有就把任务调到内存中执行,使用crontab命令编辑的文件最终都会以当前用户命名存在于/var/spool/cron路径下。

5.cron的使用方法

cron的使用分为查看定时任务和编辑定时任务两个操作。

5.1查看定时任务

linux命令行输入crontab -l,如果没有配置定时任务则不会显示任何东西。

在这里插入图片描述

5.2编辑定时任务

linux命令行输入crontab -e,进入编辑模式,每一行表示一个定时任务,格式是cron表达式紧跟着一个命令,表示按照cron表达式的方式执行命令。

cron表达式共五列,从左到右依次表示分钟、小时、天、月、星期。每一列都可以采用如下的方式指定。

  • *,表示不限制,可以翻译为,比如表达式 “0 0 * * * ”表示每天的0点0分(星期不显示,月份不限制,天不限制,限制0点和0分)
  • 数字、逗号分隔的数字、连接符表示的范围,比如表达式“0 1,3,5 2-7 3 *” 表示3月份2号到7号的每一天的凌晨1点0分 、3点0分、 5点0分分别执行
  • /n,表示每隔n执行,比如表达式“/10 * * * * ”,每隔10分钟执行

每一列的含义和取值范围如下

在这里插入图片描述

示例:

  • * * * * *:每分钟运行一次任务。
  • 0 1-5 * * *:每天凌晨的1点到5点的的第0分钟分别运行一次任务。
  • 0 0 1 * *:每月1号的00:00(午夜)运行一次任务。
  • 0 0 * * 1:每周一的00:00运行一次任务。

6.注意事项

  • 定时任务后面执行的命令不能有百分号,如果有必须要转义
  • 定时任务尽量用脚本,即将命令放入脚本
  • 执行定时任务尽量用全路径,避免脚本找不到造成执行失败
  • 结尾加上 &>/dev/null或者>/dec/null 2>&1,避免产生输出到控制台

7.问题排查

如果任务没有按照预期执行,可以从以下几个方面排查一下

  • 自己手工执行一下命令,看是否是命令存在问题
  • 通过日志排查,通过tail -f /var/log/cron查看日志
  • 权限问题,检查如下两个文件。第一个/etc/cron.deny 文件中所列用户不允许使用crontab命令 ;第二个/etc/cron.allow 该文件中所列用户允许使用crontab命令,优先于/etc/cron.deny

8.优缺点

优点:

  • 自动化:cron允许用户自动化任务,无需人工干预。
  • 资源利用:在不需要运行时,cron任务不会使用系统资源。
  • 可扩展性:cron允许用户创建复杂的执行计划,可以包含多个任务。

缺点:

  • 错误配置:如果配置不当,cron任务可能不会运行或运行不正确。
  • 安全性问题:如果cron任务包含敏感信息或对系统产生负面影响的命令,那么错误的cron设置可能会导致安全问题。
  • 调试困难:cron任务的故障排除可能比较困难,因为它们通常在后台运行。
  • 时间精准度,cron表达式只能精确到分钟,如果需求需要精确到秒级则不适合。

二、Go实现简易Cron

Go 语言中实现自制的Cron能力可以通过以下步骤:

1.理解 cron 的功能需求

cron 是一个定时任务调度器,它可以在指定的时间执行特定的任务。主要功能包括:

  • 支持设置任务的执行时间规则,如每天特定时间、每周特定日期等。
  • 能够管理多个任务,并按照各自的时间规则执行。
  • 具有一定的容错性和稳定性,即使在系统出现故障或重启后,也能继续执行任务。

2.选择合适的数据结构

任务结构体

type Task struct {
    schedule string
    command  func()
}

其中 schedule 表示任务的执行时间规则,command 是要执行的任务函数。

存储任务的容器

可以使用切片来存储多个任务。

var tasks []Task

3.解析时间规则

可以使用第三方库或者自己实现一个简单的时间规则解析器。

对于简单的时间规则,如每天特定时间,可以直接解析字符串。

func parseSchedule(schedule string) (hour int, minute int) {
    _, err := fmt.Sscanf(schedule, "%d:%d", &hour, &minute)
    if err!= nil {
        panic(err)
    }
    return
}

4.启动定时任务调度

使用time包中的Tick函数来创建一个定时器,每隔一分钟检查一次是否有任务需要执行。

ticker := time.Tick(time.Minute)
for range ticker {
    now := time.Now()
    for _, task := range tasks {
        hour, minute := parseSchedule(task.schedule)
        if now.Hour() == hour && now.Minute() == minute {
            task.command()
        }
    }
}

5.添加任务

提供一个函数来添加任务。

func AddTask(schedule string, command func()) {
    tasks = append(tasks, Task{schedule, command})
}

6.以下是一个完整的示例代码:

package main

import (
    "fmt"
    "time"
)

type Task struct {
    schedule string
    command  func()
}

var tasks []Task

func parseSchedule(schedule string) (hour int, minute int) {
    _, err := fmt.Sscanf(schedule, "%d:%d", &hour, &minute)
    if err!= nil {
       panic(err)
    }
    return
}

func startCron() {
    ticker := time.Tick(time.Minute)
    for range ticker {
       now := time.Now()
       for _, task := range tasks {
          hour, minute := parseSchedule(task.schedule)
          if now.Hour() == hour && now.Minute() == minute {
             task.command()
          }
       }
    }
}

func AddTask(schedule string, command func()) {
    tasks = append(tasks, Task{schedule, command})
}

func main() {
    AddTask("10:30", func() {
       fmt.Println("Task 1 executed at", time.Now())
    })
    startCron()
}

在这个示例中,我们实现了一个简单的 cron 功能,它可以在指定的时间执行任务。你可以根据实际需求扩展这个功能,比如支持更复杂的时间规则、添加任务管理功能等。

三、如何保证Go语言实现的定时任务的稳定性?

1.错误处理机制

在定时任务执行的函数中,需要有完善的错误处理。例如,如果任务是向数据库写入数据,可能会遇到数据库连接错误、数据格式错误等情况。

注:如下myTask是func()类型,符合【二】中介绍的Task定义,可以作为任务传入

示例:

func myTask() {
    // 假设这是一个向数据库写入数据的任务
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/mydb")
    if err!= nil {
        // 记录错误日志
        log.Println("数据库连接错误:", err)
        return
    }
    defer db.Close()
    // 执行数据库操作
    _, err = db.Exec("INSERT INTO mytable (column1) VALUES (?)", "value")
    if err!= nil {
        log.Println("数据库写入错误:", err)
    }
}

当出现错误时,通过记录错误日志,可以方便后续排查问题。同时,根据错误的严重程度,可以选择是重试任务还是直接跳过。

2.任务重试机制

对于一些可能由于网络波动、资源暂时不可用等原因导致失败的任务,可以设置重试策略。

定义重试次数和重试间隔:

func myTaskWithRetry() {
    maxRetries := 3
    retryInterval := time.Second * 5
    for i := 0; i < maxRetries; i++ {
        err := doActualTask()
        if err == nil {
            return
        }
        // 记录重试信息
        log.Printf("任务执行失败,正在进行第 %d 次重试,错误信息: %v", i + 1, err)
        time.Sleep(retryInterval)
    }
    log.Println("任务经过多次重试后仍然失败")
}

这样,在一定程度上可以提高任务成功执行的概率。

3.资源管理和释放

确保在任务执行过程中正确地管理和释放资源。如果任务涉及文件操作,要及时关闭文件;如果涉及网络连接,要正确地关闭连接。

以文件操作为例:

func fileWriteTask() {
    file, err := os.Create("output.txt")
    if err!= nil {
        log.Println("文件创建错误:", err)
        return
    }
    defer file.Close()
    _, err = file.WriteString("这是写入文件的内容")
    if err!= nil {
        log.Println("文件写入错误:", err)
    }
}

正确地使用defer关键字可以确保资源在任务结束后得到释放,避免资源泄漏。

4. 持久化任务状态和进度

如果任务执行时间较长或者可能被中断,可以将任务的状态和进度进行持久化。
例如,对于一个下载任务,可以将已下载的字节数、文件总字节数等信息存储到数据库或者本地文件中。

假设使用数据库来存储下载任务状态:

type DownloadTask struct {
    ID       int
    FileURL  string
    Progress int
    TotalSize int
}
// DownloadTask有个func()类型的方法可以作为定时任务
// 而此处的updateDownloadProgress函数则是使用DB记录对应任务的执行进度
// progress表示已经下载的字节数
func updateDownloadProgress(task *DownloadTask, progress int) {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/mydb")
    if err!= nil {
        log.Println("数据库连接错误:", err)
        return
    }
    defer db.Close()
    // 更新数据库中的下载进度
    _, err = db.Exec("UPDATE download_tasks SET progress =? WHERE id =?", progress, task.ID)
    if err!= nil {
        log.Println("下载进度更新错误:", err)
    }
}

这样,即使程序崩溃或者重启,也可以根据之前存储的进度继续执行任务。

5.监控和报警机制

建立监控系统来跟踪定时任务的执行情况。可以通过记录任务的开始时间、结束时间、执行结果等信息来实现。

例如,使用 PrometheusGrafana 来监控定时任务。首先,在任务执行函数中添加指标记录:

import (
    "github.com/prometheus/client_golang/prometheus"
)
var taskExecutionDuration = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "task_execution_duration_seconds",
        Help:    "Duration of task execution in seconds",
    },
    []string{"task_name"},
)
func init() {
    prometheus.MustRegister(taskExecutionDuration)
}
func myTask() {
    start := time.Now()
    // 执行任务内容
    end := time.Now()
    duration := end.Sub(start).Seconds()
    taskExecutionDuration.WithLabelValues("my_task").Observe(duration)
}

同时,设置报警规则,当任务执行时间过长、频繁失败等异常情况发生时,及时通知相关人员。可以通过发送邮件、短信或者使用即时通讯工具等方式进行报警。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值