Go + Gin 实现动态定时任务系统:从静态注册到动态调度与日志记录

前言

在项目初期,或者公司业务规模较小时,定时任务往往直接集成在项目框架的代码中,或者简单地部署在 Linux 服务器上执行。虽然这种方式实现起来简单直观,但随着业务需求的不断增长,每当需要新增、修改或删除定时任务时,都必须修改代码并重新发布版本,导致维护成本不断升高。同时,考虑到项目的规模和复杂度,现阶段也不太适合引入复杂的第三方定时任务框架。

因此,在本系列文章中,我们来实现一个自定义封装的「后台动态管理定时任务模块」,支持任务的动态增删改,帮助我们项目在无需重启服务、无需修改代码的情况下,灵活高效地管理项目中的定时任务。

接下来,我们将基于 Gin 框架,集成 robfig/cron/v3 库,实现一个定时调度器,为项目提供灵活可控的定时任务能力。

第一步:安装 robfig/cron/v3 依赖

在开始编码之前,我们需要先安装 robfig/cron/v3 依赖库。这个库是 Go 语言中使用最广泛的定时任务调度框架,支持多种调度表达式,使用灵活,功能强大。

使用 Go 命令安装:

go 复制代码
go get github.com/robfig/cron/v3

第二步:创建定时任务模块目录结构

为了保持代码结构清晰,便于后续的扩展和维护,我们将定时任务模块拆分成多个文件,分别负责初始化、实例化调度器、注册任务等功能。本篇项目的目录结构如下:

go 复制代码
├── initialize/
│   └── timer.go         初始化定时器,加载任务
├── core/
│   └── cron.go          创建 cron 实例,全局挂载,便于统一管理
├── global/
│   └── global.go        定义全局变量 GVA_CRON,方便全局调用
├── timer/
│   └── simple_task.go   编写一个简单的任务示例,演示任务注册和执行

简要说明一下各文件职责:

  • initialize/timer.go:负责初始化定时器,调用任务注册方法,将所有任务注册到调度器中。
  • core/cron.go :创建并初始化 cron 实例,挂载到全局变量,保证全局只有一个调度器实例。
  • global/global.go :定义全局变量 GVA_CRON,便于项目各处调用和管理定时任务。
  • timer/simple_task.go:作为示例任务文件,编写一个简单的任务,帮助快速上手任务注册流程。

第三步:封装全局 cron 实例

为了实现定时任务的全局管理,我们先封装一个全局的 cron 调度器实例,便于后续在项目各处统一调用与管理。

core/cron.go

core 目录下创建 cron.go 文件,负责初始化并返回一个新的 cron 实例。

go 复制代码
package core

import (
	"github.com/robfig/cron/v3"
)

// InitCron 初始化 cron 实例,支持秒级调度
func InitCron() *cron.Cron {
	c := cron.New(cron.WithSeconds()) // 使用秒级时间配置
	return c
}

说明:

这里我们使用 cron.WithSeconds(),开启秒级调度支持,方便后续进行更精细的时间控制。

global/global.go

global 包中,定义一个全局变量 GVA_CRON,用于存储我们的 cron 实例。

go 复制代码
package global

import "github.com/robfig/cron/v3"

// GVA_CRON 全局 cron 实例
var GVA_CRON *cron.Cron

这样做的好处是,我们的调度器实例可以被全项目调用,实现任务的统一管理。

initialize/timer.go(初始化任务注册器)

go 复制代码
package initialize

import (
	"go-gin-demo/global"
	"go-gin-demo/timer"
)

// Timer 初始化任务注册器,注册所有定时任务
func Timer() {
	timer.RegisterTasks(global.GVA_CRON) // 注册任务
	global.GVA_CRON.Start()              // 启动 cron 调度器
}

说明:

  • timer.RegisterTasks():调用任务模块,注册所有需要运行的定时任务。
  • global.GVA_CRON.Start():启动定时器,所有任务开始调度。

main.go 初始化

在项目入口文件 main.go 中初始化全局调度器,并调用定时任务初始化函数,完成任务的注册。

go 复制代码
package main

import (
	"your_project_path/core"
	"your_project_path/global"
	"your_project_path/initialize"
)

func main() {
	// 初始化全局 cron 实例
	global.GVA_CRON = core.InitCron()

	// 初始化定时任务注册
	initialize.Timer()

	// ... 其他启动逻辑
}

说明:

  • global.GVA_CRON = core.InitCron():初始化全局定时器。
  • initialize.Timer():注册所有的定时任务。

第四步:创建一个简单定时任务

接下来,我们来创建一个简单的定时任务,验证我们的调度器是否正常工作。


timer/simple_task.go

go 复制代码
package timer

import (
	"github.com/robfig/cron/v3"
	"go-gin-demo/global"
	"log"
)

// RegisterTasks 注册定时任务
func RegisterTasks(cr *cron.Cron) {
	// 每隔 5 秒执行一次的定时任务
	_, err := cr.AddFunc("*/5 * * * * *", func() {
		global.GVA_LOG.Info("⏰ 定时任务触发:每 5 秒执行一次")
		log.Println("定时任务执行:我是一个测试任务")
	})
if err != nil {
    global.GVA_LOG.Error("注册定时任务失败", zap.Any("err", err))
}

说明:

  • cr.AddFunc():向调度器添加一个新的定时任务。
  • "*/5 * * * * *":cron 表达式,表示「每隔 5 秒执行一次」。
  • global.GVA_LOG.Info():记录日志,方便我们查看任务是否被正常触发。
  • log.Println():控制台打印,作为简单输出。

效果验证:

如果一切配置正确,运行项目后我们将在日志中看到类似输出:

复制编辑 复制代码
⏰ 定时任务触发:每 5 秒执行一次
定时任务执行:我是一个测试任务

动态任务调度中心设计与实现

到目前为止,我们已经完成了「静态任务注册」,并成功验证了调度器能够正常工作,定时任务能够按照预期周期执行。

不过,目前的任务依然是写死在代码中的,无法根据实际业务需求动态调整。为了实现更加灵活的任务管理,下一步我们将开始实现一个 动态管理定时任务的调度中心,实现任务从数据库中动态获取加载和执行。

在进入具体实现之前,我们先明确一下本次「任务调度中心」要实现的主要目标功能:

功能模块 说明
sys_job 存储每一个定时任务的定义(任务名称、Cron 表达式、状态、入口函数名称等)
支持持久化任务加载 项目启动时自动从数据库加载已配置的任务,实现任务持久化管理
支持动态启用/停用任务 任务状态可以在后台管理界面动态控制,无需重启服务
任务执行日志记录 每次任务执行时记录日志,包括执行时间、执行结果(成功/失败)、错误信息等,方便追溯和排查

通过实现以上功能,我们可以做到任务的统一管理、灵活调度以及问题可追溯,为后续扩展更多高级能力(如失败重试、任务通知、可视化监控)打下基础。

第一步:定义 SysJob 任务模型(任务表结构)

为了实现任务的持久化管理,我们需要先设计一张任务表,存储所有定时任务的配置。

这张表不仅要保存任务的基本信息,还要为后续的动态管理、扩展功能打下基础。

表设计说明

字段 说明
任务名称、任务分组 标识任务归属,便于分类管理
cron 表达式 定义任务的调度周期
是否启用 用于控制任务是否启用
执行函数名 用作调度时的映射关系
可选参数 任务运行时的参数(字符串形式,便于存储和解析)
状态字段 记录任务状态,如:运行中、暂停等
执行策略 支持并发执行或单线程模式
下次运行时间 可选字段,便于扩展任务调度预览功能
备注 任务描述或备注信息,便于维护人员理解任务目的

通过以上设计,我们可以实现任务的动态增删改查,并且为未来扩展如:任务运行记录、失败重试策略等功能做好准备。


表结构定义

model/sys_job.go

go 复制代码
package model

import "gorm.io/gorm"

type SysJob struct {
    gorm.Model
    Name         string `json:"name" gorm:"type:varchar(100);comment:任务名称"`
    Group        string `json:"group" gorm:"type:varchar(50);default:'default';comment:任务分组"`
    Cron         string `json:"cron" gorm:"type:varchar(64);comment:cron表达式"`
    InvokeTarget string `json:"invokeTarget" gorm:"type:varchar(100);comment:调用目标函数名"`
    Args         string `json:"args" gorm:"type:varchar(255);comment:执行参数"`
    Status       int    `json:"status" gorm:"type:tinyint(1);default:1;comment:状态(1启用 0停用)"`
    Concurrent   bool   `json:"concurrent" gorm:"default:false;comment:是否并发执行"`
    Remark       string `json:"remark" gorm:"type:varchar(255);comment:备注"`
}

func (SysJob) TableName() string {
    return "sys_job"
}

建表并注册到项目中

在完成 SysJob 模型定义后,我们需要将模型注册到项目初始化流程中,确保项目启动时可以自动创建对应的数据表。


initialize/register_tables.go

go 复制代码
package initialize

import (
	"go-gin-demo/global"
	"go-gin-demo/model"
	"go.uber.org/zap"
)

// RegisterTables 初始化数据库表
func RegisterTables() {
	err := global.GVA_DB.AutoMigrate(
		// ... 其他逻辑表
		&model.SysJob{}, // 新增任务表
	)
	if err != nil {
		global.GVA_LOG.Error("register table failed", zap.Error(err))
	}
}

说明:

  • 通过 AutoMigrate() 方法,Gorm 会根据模型自动创建或更新数据库表结构。
  • 我们新增的 SysJob 模型也一并注册到了表初始化流程中。

测试数据插入示例

为了方便验证定时任务调度功能,我们可以手动插入如下测试数据到 sys_job 表中:

字段名 示例值 说明
name 订单清理任务 任务名称
group system 任务分组
cron */10 * * * * * Cron 表达式,表示「每 10 秒执行一次」
invoke_target ClearOrders 执行函数名称(后续映射用)
args {"status":"expired"} 执行参数(JSON 字符串)
status 1 任务状态(1 启用,0 停用)
concurrent false 是否并发执行

说明一下:

  • args 字段采用 JSON 格式,可以灵活传递多个参数。
  • invoke_target 需要与后续定义的任务函数名称保持一致,否则调度时无法正确匹配任务。

第二步:实现任务加载器(从数据库加载任务到 cron 调度器)

在完成任务表结构和测试数据准备后,我们开始实现任务加载器。

任务加载器的主要职责是:项目启动时,动态加载数据库中的任务配置,并注册到调度器中执行。

实现目标功能

功能 说明
加载任务配置 sys_job 表中读取所有启用状态(status=1)的任务
注册调度任务 将任务注册到 cron 调度器,实现动态加载
函数路由映射 根据 invoke_target 字段,映射到对应的任务执行函数
参数传递支持 任务执行时,支持参数传递,参数以字符串或 JSON 形式传入函数

设计思路简要说明:

  • loader.go:负责从数据库加载任务,并注册到 cron 调度器。
  • registry.go:作为任务执行函数的注册表,实现 invoke_target 到具体函数的映射。
  • tasks/:具体任务实现目录,每个任务文件实现对应的任务函数。

文件结构

go 复制代码
timer/
├── loader.go            任务加载器:从数据库加载任务
├── registry.go          任务注册表:invokeTarget -> 执行函数映射
├── tasks/
│   └── clear_orders.go  示例任务实现:订单清理任务

说明:

  • 这样结构清晰,便于后续扩展更多任务类型。
  • 如果我们后续要支持动态新增任务、任务热更新,这个结构也非常好扩展。

1.实现任务注册器:registry.go

为了实现通过配置调用不同任务函数的能力,我们先创建一个任务注册器,用于管理所有可执行的任务函数。

timer/registry.go

go 复制代码
package timer

import (
	"errors"
	"fmt"
)

// 函数注册表:invokeTarget => 实际执行函数
var taskRegistry = make(map[string]func(string) error)

// RegisterTask 注册任务函数
func RegisterTask(name string, handler func(string) error) {
	taskRegistry[name] = handler
}

// GetTask 获取已注册的任务函数
func GetTask(name string) (func(string) error, error) {
	if handler, ok := taskRegistry[name]; ok {
		return handler, nil
	}
	return nil, errors.New("任务未注册: " + name)
}

说明:

  • taskRegistry:维护任务名称和实际处理函数的映射关系。
  • RegisterTask():在初始化阶段注册任务函数到注册表。
  • GetTask():根据任务名称获取对应的处理函数。

2.实现示例任务:clear_orders.go

接下来,我们实现一个简单的示例任务:订单清理任务。

timer/tasks/clear_orders.go

go 复制代码
package tasks

import (
	"fmt"
	"go-gin-demo/global"
	"time"
)

// ClearOrders 示例任务:订单清理
func ClearOrders(args string) error {
	global.GVA_LOG.Info("执行订单清理任务", args)
	fmt.Println("执行订单清理逻辑,参数为:", args)

	// 模拟任务耗时
	time.Sleep(2 * time.Second)

	return nil
}

说明:

  • 任务名称:ClearOrders,与我们 sys_job 表中配置的 invoke_target 字段保持一致。
  • 参数:通过 args string 传递,支持自定义参数(JSON 字符串或简单文本)。
  • 日志:记录任务执行日志,便于排查问题。

3.编写任务加载器:loader.go

接下来,我们来完善任务加载器,从数据库加载任务并注册到调度器中。

说明: 当前的实现是一个简单的 demo,主要用于验证「任务从数据库加载并执行」的流程是否正常。

目前的代码还没有处理并发安全、线程竞争等问题,像多任务同时执行、任务函数内部共享变量、任务注册的读写锁控制等情况,暂未做进一步处理。后面会详细讲一下处理哈。

timer/loader.go

go 复制代码
package timer

import (
	"github.com/robfig/cron/v3"
	"go-gin-demo/global"
	"go-gin-demo/model"
	"go-gin-demo/timer/tasks"
	"go.uber.org/zap"
)

// RegisterAllTasks 注册所有任务到注册表中
func RegisterAllTasks() {
	RegisterTask("ClearOrders", tasks.ClearOrders)
}

// LoadJobsFromDB 从数据库加载任务并注册到调度器
func LoadJobsFromDB(cr *cron.Cron) {

	// 查询数据库中启用的任务
	var jobs []model.SysJob
	err := global.GVA_DB.Where("status = ?", 1).Find(&jobs).Error
	if err != nil {
		global.GVA_LOG.Error("加载定时任务失败", zap.Error(err))
		return
	}

	// 遍历任务列表,逐一注册到调度器
	for _, job := range jobs {
		taskFunc, err := GetTask(job.InvokeTarget)
		if err != nil {
			global.GVA_LOG.Warn("找不到任务函数: " + job.InvokeTarget)
			continue
		}

		_, err = cr.AddFunc(job.Cron, func() {
			global.GVA_LOG.Info("⏱ 开始执行任务: " + job.Name)
			if err := taskFunc(job.Args); err != nil {
				global.GVA_LOG.Error("任务执行失败: "+job.Name, zap.Error(err))
			}
		})
		if err != nil {
			global.GVA_LOG.Error("注册任务失败: "+job.Name, zap.Error(err))
		}
	}
}

4.初始化定时器:timer.go

在完成任务注册表和任务加载器的开发之后,我们需要在项目启动时,调用这些初始化方法,完成任务调度器的初始化。

initialize/timer.go

go 复制代码
package initialize

import (
	"go-gin-demo/global"
	"go-gin-demo/timer"
)

// Timer 初始化定时任务调度器
func Timer() {
	// 注册任务函数映射表
	timer.RegisterAllTasks()

	// 从数据库加载任务并注册到调度器
	timer.LoadJobsFromDB(global.GVA_CRON)

	// 启动调度器
	global.GVA_CRON.Start()
}

说明

  • RegisterAllTasks()
    注册所有任务函数到任务注册表,确保后续加载任务时可以正确找到对应函数。
  • LoadJobsFromDB()
    从数据库加载所有启用的任务配置,并注册到调度器。
  • global.GVA_CRON.Start()
    启动调度器,正式开始任务调度。

验证效果

到这里,我们的动态定时任务调度中心已经完成了基础功能开发。接下来,让我们运行项目,验证一下效果。

启动项目后,控制台日志输出类似如下内容:

css 复制代码
2025-04-11 10:00:05	INFO	⏱ 开始执行任务: 订单清理任务
执行订单清理逻辑,参数为: {"status":"expired"}
2025-04-11 10:00:15	INFO	⏱ 开始执行任务: 订单清理任务
执行订单清理逻辑,参数为: {"status":"expired"}

说明:

  • 系统成功从数据库加载了启用状态的任务。
  • 调度器根据 cron 表达式定时触发任务(如:每 10 秒执行一次)。
  • 任务执行时,日志中能够正常输出任务名称和执行参数。
  • 测试任务 "订单清理任务" 成功执行,模拟了订单清理的业务逻辑。

多任务扩展:目录结构展示

目前我们已经实现了单个任务的动态调度。

但是我们的项目中会有多个定时任务,例如「清理日志」、「同步数据」等,不需要做额外的框架调整,只需要在 tasks 目录中增加对应的任务文件,并在注册表中进行注册即可。

大概目录结构如下:

go 复制代码
timer/
├── loader.go            // 从数据库加载任务
├── registry.go          // 注册表:任务名 -> 函数
├── tasks/               // 所有任务都集中管理
│   ├── clean_logs.go        // 清理日志任务
│   ├── clear_orders.go      // 清理订单任务
│   └── sync_data.go         // 同步外部数据任务

说明:

  • tasks/ 目录:存放所有的业务任务函数,每个任务一个独立文件,方便维护和扩展。
  • registry.go:统一注册所有任务函数,便于动态调度时快速查找映射。
  • loader.go:从数据库加载任务配置,并注册到调度器中执行。

增强可观测性:实现任务执行日志记录功能

虽然我们已经实现了定时任务的动态调度,任务也能够根据配置正常执行,但在实际项目中,仅仅依靠任务本身执行是不够的。
为了能够及时了解任务的执行情况,方便后续排查问题、复盘任务状态,我们需要为定时任务系统增加「执行日志记录」功能。

通过日志记录,我们可以实现对每一次任务执行情况的全面追溯,帮助开发和运维人员快速定位问题,确保系统的稳定运行。

功能目标

功能 描述
每次任务执行记录一条日志 包含:任务 ID、任务名称、执行参数、状态(成功/失败)、开始时间、执行耗时、错误信息等
日志存储在数据库表 新增日志表:sys_job_log
支持后期查询与分页 方便后台管理页面查看任务执行历史记录,支持分页查询,便于运维使用

这样做的好处:

  • 任务异常时,可以快速查看执行记录和错误信息。
  • 后台页面可以实时展示任务执行情况,提升系统可观测性。
  • 后续扩展如:失败自动重试、任务告警通知、任务统计分析时,都离不开执行日志作为基础数据支撑。

第一步:定义任务日志表模型

我们首先需要设计一张任务执行日志表,用于记录每一次任务的执行情况。

model/sys_job_log.go

go 复制代码
package model

import (
    "gorm.io/gorm"
    "time"
)

type SysJobLog struct {
    gorm.Model
    JobID     uint      `json:"jobId" gorm:"comment:任务ID"`
    JobName   string    `json:"jobName" gorm:"type:varchar(100);comment:任务名称"`
    Args      string    `json:"args" gorm:"type:varchar(255);comment:执行参数"`
    Status    string    `json:"status" gorm:"type:varchar(10);comment:状态 success/fail"`
    ErrorMsg  string    `json:"errorMsg" gorm:"type:text;comment:错误信息"`
    StartTime time.Time `json:"startTime" gorm:"comment:开始时间"`
    Duration  int64     `json:"duration" gorm:"comment:耗时(毫秒)"`
}

func (SysJobLog) TableName() string {
    return "sys_job_log"
}

说明:

  • JobID:关联任务主表,方便后续查询对应任务的日志。
  • JobName:记录任务名称,便于日志列表快速查看。
  • Args:执行时传入的参数,帮助还原执行场景。
  • Status:任务执行状态,标记成功或失败。
  • ErrorMsg:任务执行失败时的错误信息。
  • StartTime:任务开始执行时间。
  • Duration:任务执行耗时,单位毫秒,便于性能分析。

第二步:注册数据表

接下来,将任务日志模型注册到项目初始化流程中,确保数据库表可以正常创建。

nitialize/register_tables.go

go 复制代码
package initialize

import (
	"go-gin-demo/global"
	"go-gin-demo/model"
	"go.uber.org/zap"
)

// RegisterTables 初始化数据库表
func RegisterTables() {
	err := global.GVA_DB.AutoMigrate(
		// ... 其他逻辑表
		&model.SysJob{},
		&model.SysJobLog{}, // 新增任务日志表
	)
	if err != nil {
		global.GVA_LOG.Error("register table failed", zap.Error(err))
	}
}

执行项目初始化后,数据库中将自动生成 sys_job_log 表。

第三步:任务执行时写入日志

有了任务日志表之后,我们需要在每次任务执行时,记录下任务的执行情况,包括开始时间、耗时、执行结果(成功/失败)以及错误信息等。这样便于后续排查问题和复盘任务执行情况。

修改 timer/loader.go → 执行任务时记录日志

go 复制代码
// LoadJobsFromDB 从数据库加载任务并注册到调度器
func LoadJobsFromDB(cr *cron.Cron) {

    // 查询数据库中启用的任务
    var jobs []model.SysJob
    err := global.GVA_DB.Where("status = ?", 1).Find(&jobs).Error
    if err != nil {
       global.GVA_LOG.Error("加载定时任务失败", zap.Error(err))
       return
    }

    // 遍历任务列表,逐一注册到调度器
    for _, job := range jobs {
       // 新建一个 jobCopy,确保每次循环隔离 job 内容
       jobCopy := job
       taskFunc, err := GetTask(jobCopy.InvokeTarget)
       if err != nil {
          global.GVA_LOG.Warn("找不到任务函数: " + jobCopy.InvokeTarget)
          continue
       }

       _, err = cr.AddFunc(jobCopy.Cron, func() {
          start := time.Now()

          global.GVA_LOG.Info("⏱ 开始任务: " + jobCopy.Name)

          execErr := taskFunc(jobCopy.Args)

          duration := time.Since(start).Milliseconds()

          log := model.SysJobLog{
             JobID:     jobCopy.ID,
             JobName:   jobCopy.Name,
             Args:      jobCopy.Args,
             StartTime: start,
             Duration:  duration,
             Status:    "success",
          }

          if execErr != nil {
             log.Status = "fail"
             log.ErrorMsg = execErr.Error()
             global.GVA_LOG.Error("任务失败: "+jobCopy.Name, zap.Any("err", execErr))
          } else {
             global.GVA_LOG.Info("任务完成: " + jobCopy.Name)
          }

          _ = global.GVA_DB.Create(&log).Error
       })
       if err != nil {
          global.GVA_LOG.Error("注册任务失败: "+jobCopy.Name, zap.Error(err))
       }
    }
}

说明

  • 任务开始时间start := time.Now()
  • 执行耗时duration := time.Since(start).Milliseconds()
  • 执行状态
    • 成功:status = success
    • 失败:记录 errorMsg
  • 日志写入数据库global.GVA_DB.Create(&log).Error

第四步:效果示例(数据库)

任务成功执行后,sys_job_logs 表中新增一条记录,效果如下:

job_id job_name status duration error_msg
1 清理订单任务 success 2053 null

总结:从静态任务到动态调度任务系统

到目前为止,我们已经完成了从「静态任务注册」到「动态加载任务」的系统搭建:

  • 静态任务注册:最初我们通过写死在代码中的方式注册定时任务,实现了简单的调度。
  • 动态任务调度中心:通过数据库维护任务配置,实现任务的动态加载与调度,不需要修改代码或重启服务。
  • 任务执行日志:每次任务执行后自动记录日志,帮助我们追踪任务运行情况,便于问题排查和历史回溯。

通过这些功能,我们基本实现了一个可动态管理的定时任务系统。

问题引出:并发问题 & panic 崩溃风险

虽然目前系统功能已经比较完善,但在实际应用场景中,仍然存在两个潜在的隐患:

并发执行导致重复处理

我们的数据库表虽然设计了 concurrent 字段(是否允许并发),但当前实现中还没有生效,所有任务默认都是并发执行的。

这样就会带来一个典型问题:

如果任务本身执行耗时较长,而任务的调度周期又很短,就会导致任务重叠执行。

例如:

  • 某任务耗时 30 秒
  • 但 cron 表达式是:*/10 * * * * *(每 10 秒执行一次)

那么就会发生:

  • 上一次任务还在执行,下一次调度已经触发;

  • 任务出现重叠执行,可能导致:

    • 数据库重复写入
    • 外部接口多次调用,造成资源浪费
    • 数据竞争,产生冲突或脏数据

任务 panic 崩溃风险

目前我们的任务函数没有做异常保护。

如果任务执行过程中出现 panic,例如:

  • 空指针引用
  • 数组越界
  • 调用第三方库异常退出

Go 的 cron 库默认不会帮我们 recover(),任务中的 panic 会导致整个调度器崩溃,影响其他所有定时任务的执行,甚至导致服务不可用。

解决方案:互斥控制 + panic 异常保护

为了保证系统稳定可靠,我们准备引入以下两个优化方案:

优化目标 方案描述
任务互斥控制 引入互斥锁,任务执行前判断是否允许并发。如果任务配置为「不允许并发」,当前任务正在执行时则跳过本次调度,避免重复执行。
异常捕获保护 在任务执行函数外层增加 recover(),当任务函数内部发生 panic 异常时,优雅捕获并记录日志,避免影响调度器稳定性。

这样,不仅可以保证任务执行的安全性,还能有效避免由于业务或逻辑问题导致的服务故障。

最后

因为这篇文章主要是从零实现整个定时任务系统,从静态到动态,篇幅上我尽量写得详细,废话比较多,以便于更好理清思路。不过目前的任务加载器其实还很简单,暂时没有处理并发安全、异常保护,也还不支持任务热更新。

所以,下一篇我会重点优化任务加载器,补齐这些特性,让我们的任务调度系统变得更强大、更稳定一些。

相关推荐
程序员契奇17 分钟前
Tools工具使用
人工智能·后端
老鹰86230 分钟前
实战 dig:Go 编译时依赖注入的完整教程与迁移指南
go
IT_陈寒34 分钟前
SpringBoot自动配置没生效?你可能漏了这个注解
前端·人工智能·后端
长明39 分钟前
C#项目组织与概念梳理
后端·c#
xn713344 分钟前
个人网站站外分发怎么做归因?我给 XBSTACK 补了一套 UTM 追踪规则
后端·低代码
用户2330713074791 小时前
JUC 并发容器与工具
后端
冰暮流星1 小时前
flask之模版渲染
后端·python·flask
威武的花瓣1 小时前
细说ASP.NET的各种异步操作
后端·asp.net·php
漂亮的摩托1 小时前
如何编写一个SpringBoot项目告警推送的Starter
java·spring boot·后端
任性的芝麻1 小时前
ASP.NET MVC 中的异步方式
后端·asp.net·mvc