开箱即用的GO后台管理系统 Kratos Admin - 定时任务

开箱即用的GO后台管理系统 Kratos Admin - 定时任务

在后台管理系统中,定时任务是一个很实用的功能,可以帮助我们自动执行一些周期性的任务,比如定期清理数据、发送邮件提醒等。

在go里面,如果想要简单的实现一个周期性任务,我们可以用cron或者gron等仿linux的crontab的库。

但是,我们使用的是微服务框架,而且,还要考虑到能否实现分布式执行。那么,我们就需要利用**任务队列(Task Queue)**来实现。

任务队列(Task Queue) 一般用于跨线程或跨计算机分配工作的一种机制。其本质是生产者消费者模型,生产者发送任务到消息队列,消费者负责处理任务。

任务队列的输入是称为任务(Task)的工作单元。专用的工作进程不断监视任务队列以查找要执行的新工作。

任务队列可以使用于以下的场景:

  1. 分布式任务:可以将任务分发到多个工作者进程或机器上执行,以提高任务处理速度。
  2. 定时任务:可以在指定时间执行任务。例如:每天定时备份数据、日志归档、心跳测试、运维巡检。支持 crontab 定时模式
  3. 后台任务:可以在后台执行耗时任务,例如图像处理、数据分析等,不影响用户界面的响应。
  4. 解耦任务:可以将任务与主程序解耦,以提高代码的可读性和可维护性,解耦应用程序最直接的好处就是可扩展性和并发性能的提高。支持并发执行任务,同时支持自动动态扩展。
  5. 实时处理:可以支持实时处理任务,例如即时通讯、消息队列等。

在Python世界里面,我们可以使用Celery

在Golang语言里面,我们有像AsynqMachinery等,类似于Celery的分布式任务队列。

在我们Kratos Admin里面,我们将使用Asynq来实现定时任务,Machinery比较重,需要依赖第三方MQ,而Asynq只需要依赖Redis,足够轻量,admin的使用场景下,也足够使用了。

Asynq概述

Asynq是一个使用Go语言实现的分布式任务队列和异步处理库,它由Redis提供支持,它提供了轻量级的、易于使用的API,并且具有高可扩展性和高可定制化性。其作者Ken Hibino,任职于Google。

Asynq主要由以下几个组件组成:

  • 任务(Task):需要被异步执行的操作;
  • 处理器(Processor):负责执行任务的工作进程;
  • 队列(Queue):存放待执行任务的队列;
  • 调度器(Scheduler):根据规则将任务分配给不同的处理器进行执行。

通过使用Asynq,我们可以非常轻松的实现异步任务处理,同时还可以提供高效率、高可扩展性和高自定义性的处理方案。

Asynq的特点

  • 保证至少执行一次任务
  • 任务写入Redis后可以持久化
  • 任务失败之后,会自动重试
  • worker崩溃自动恢复
  • 可是实现任务的优先级
  • 任务可以进行编排
  • 任务可以设定执行时间或者最长可执行的时间
  • 支持中间件
  • 可以使用 unique-option 来避免任务重复执行,实现唯一性
  • 支持 Redis Cluster 和 Redis Sentinels 以达成高可用性
  • 作者提供了Web UI & CLI Tool让大家查看任务的执行情况

Asynq任务名的命名规则

asynq 里,任务名(也就是 type name)并没有严格的语法规则,但为了提升代码的可读性、可维护性,便于管理,建议遵循以下准则:

唯一性

每个任务类型名在整个应用程序中必须唯一。因为 asynq 依靠任务类型名来区分不同的任务处理逻辑,若有重复,就会在任务调度与处理时产生混乱。例如:

go 复制代码
const (
    TaskTypeSendEmail = "email:send"
    TaskTypeProcessOrder = "order:process"
)

这里的 TaskTypeSendEmailTaskTypeProcessOrder 都是独一无二的。

asynq还有一个特性:

我们注册一个任务名:email:send,我们创建一个任务名为:email:send:1却可以被email:send的回调方法所处理。

命名风格

  • 使用冒号分隔 :一般采用冒号来分隔不同的命名空间或模块,这样能让任务名结构更清晰。例如,email:send 表明该任务和邮件发送相关;order:process 意味着此任务和订单处理有关。
  • 采用小写字母 :任务名通常用小写字母和数字,单词间可用 连字符- 或者 下划线_ 分隔。像 user:registerpayment:refund 这样的命名,简洁且易于理解。

语义明确

任务名要准确反映任务的功能或用途,这样开发者在查看代码或者调试时,能迅速了解任务的作用。比如,不要用模糊的名称 task1job2,而要用 image:resizereport:generate 这类明确的名称。

避免特殊字符

尽量避免使用特殊字符(除了冒号、连字符和下划线),因为特殊字符可能会在某些场景下引发问题,并且会降低任务名的可读性。

版本管理

若任务逻辑有重大变更,可考虑在任务名里添加版本号。例如,email:send:v2 表示这是邮件发送任务的第二个版本。

Asynq可视化监控

Asynq提供了两种监控手段:CLI和Web UI。

命令行工具CLI

bash 复制代码
go install github.com/hibiken/asynq/tools/asynq@latest

Web UI

Asynqmon是一个基于Web的工具,用于监视管理Asynq的任务和队列,有关详细的信息可以参阅工具的README。

Web UI我们可以通过Docker的方式来进行安装:

bash 复制代码
docker pull hibiken/asynqmon:latest

docker run -d \
    --name asynqmon \
    -p 8080:8080 \
    hibiken/asynqmon:latest \
    --redis-url=redis://:*[email protected]:6379/1

安装好Web UI之后,我们就可以打开浏览器访问管理后台了:http://localhost:8080

  • 仪表盘
  • 任务视图
  • 性能

如何在Kratos Admin中使用定时任务

在上面提到的Asynq和Machinery,我已经将之以transport.Server的形式进行了封装:

我们需要在项目中安装Asynq的依赖库:

bash 复制代码
go get -u github.com/tx7do/kratos-transport/transport/asynq

接着,在internal/server当中创建Asynq的服务器:

go 复制代码
package server

import (
    ...
	"github.com/tx7do/kratos-transport/transport/asynq"
)

// NewAsynqServer creates a new asynq server.
func NewAsynqServer(cfg *conf.Bootstrap, _ log.Logger, svc *service.TaskService) *asynq.Server {
	if cfg == nil || cfg.Server == nil || cfg.Server.Asynq == nil {
		return nil
	}

	srv := asynq.NewServer(
		asynq.WithAddress(cfg.Server.Asynq.GetEndpoint()),
		asynq.WithRedisPassword(cfg.Server.Asynq.GetPassword()),
		asynq.WithRedisDatabase(int(cfg.Server.Asynq.GetDb())),
		asynq.WithLocation(cfg.Server.Asynq.GetLocation()),
		asynq.WithEnableKeepAlive(false),
		asynq.WithGracefullyShutdown(true),
		asynq.WithShutdownTimeout(3*time.Second),
	)

	svc.Server = srv

	var err error

	// 注册任务
	if err = asynq.RegisterSubscriber(srv, task.BackupTaskType, svc.AsyncBackup); err != nil {
		log.Error(err)
	}

	// 启动所有的任务
	_, _ = svc.StartAllTask(context.Background())

	return srv
}

然后,我们需要在main.go里面把asynq.Server注册进kratos.App

go 复制代码
func newApp(
	...
	as *asynq.Server,
) *kratos.App {
	return bootstrap.NewApp(as, ...)
}

现在就可以开始写业务逻辑到service里面了:

go 复制代码
package service

// AsyncBackup 异步备份
func (s *TaskService) AsyncBackup(taskType string, taskData *task.BackupTaskData) error {
	s.log.Infof("AsyncBackup [%s] [%+v] [%s]", taskType, taskData, taskData.Name)
	return nil
}

// startTask 启动一个任务
func (s *TaskService) startTask(t *systemV1.Task) error {
	if t == nil {
		return errors.New("task is nil")
	}

	if t.GetEnable() == false {
		return errors.New("task is not enable")
	}

	var opts []asynq.Option
	var payload broker.Any
	var err error

	switch t.GetType() {
	case systemV1.TaskType_TaskType_Periodic:
		opts, payload = s.convertTaskOption(t)
		if _, err = s.Server.NewPeriodicTask(t.GetCronSpec(), t.GetTypeName(), payload, opts...); err != nil {
			s.log.Errorf("[%s] 创建定时任务失败[%s]", t.GetTypeName(), err.Error())
			return err
		}

	case systemV1.TaskType_TaskType_Delay:
		opts, payload = s.convertTaskOption(t)
		if err = s.Server.NewTask(t.GetTypeName(), payload, opts...); err != nil {
			s.log.Errorf("[%s] 创建延迟任务失败[%s]", t.GetTypeName(), err.Error())
			return err
		}

	case systemV1.TaskType_TaskType_WaitResult:
		opts, payload = s.convertTaskOption(t)
		if err = s.Server.NewWaitResultTask(t.GetTypeName(), payload, opts...); err != nil {
			s.log.Errorf("[%s] 创建等待结果任务失败[%s]", t.GetTypeName(), err.Error())
			return err
		}
	}

	return nil
}

// StartAllTask 启动所有的任务
func (s *TaskService) StartAllTask(ctx context.Context) (int32, error) {
    // 读取任务列表
	resp, err := s.ListTask(ctx, &pagination.PagingRequest{
		NoPaging: trans.Ptr(true),
		Query:    trans.Ptr(""),
	})
	if err != nil {
		s.log.Errorf("获取任务列表失败[%s]", err.Error())
		return 0, err
	}

	s.log.Infof("开始开启定时任务,总计[%d]个", resp.GetTotal())

	// 重新启动任务
	var count int32
	for _, t := range resp.GetItems() {
		if s.startTask(t) != nil {
			continue
		} else {
			count++
		}
	}

	s.log.Infof("总共成功开启定时任务[%d]个", count)

	return count, nil
}

项目代码

参考资料

相关推荐
向阳2564 分钟前
SpringBoot+vue前后端分离整合sa-token(无cookie登录态 & 详细的登录流程)
java·vue.js·spring boot·后端·sa-token·springboot·登录流程
你的人类朋友18 分钟前
JS严格模式,启动!
javascript·后端·node.js
Aurora_NeAr19 分钟前
深入理解Java虚拟机-Java内存区域与内存溢出异常
后端
风象南22 分钟前
SpringBoot实现数据库读写分离的3种方案
java·spring boot·后端
lzj201422 分钟前
DataPermissionInterceptor源码解读
后端
ChinaRainbowSea37 分钟前
3. RabbitMQ 的(Hello World) 和 RabbitMQ 的(Work Queues)工作队列
java·分布式·后端·rabbitmq·ruby·java-rabbitmq
dleei1 小时前
MySql安装及SQL语句
数据库·后端·mysql
CryptoPP1 小时前
springboot 对接马来西亚数据源API等多个国家的数据源
spring boot·后端·python·金融·区块链
Source.Liu1 小时前
【学Rust写CAD】27 双线性插值函数(bilinear_interpolation.rs)
后端·rust·cad
yinhezhanshen1 小时前
理解rust里面的copy和clone
开发语言·后端·rust