golang基于redis的mq组件 Asynq使用小结(2024.1.4)

一、概述

Asynq 是一个 Go 库,用于对任务进行排队并与工作线程异步处理它们。它由Redis支持,旨在可扩展且易于入门。并且有对应webUi界面可以进行图形化管理。相比kafka、rabbitMq等传统mq消息中间件,更加轻量化

任务队列用作跨多台机器分配工作的机制。一个系统可以由多个工作服务器和代理组成,从而实现高可用性和水平扩展。支持单点redis和redis集群。

GitHub地址:github.com/hibiken/asy...

二、Asynq主要功能和基本任务状态

1、主要功能

  1. 普通队列
  2. 优先级队列
  3. 延时队列
  4. 定时任务
  5. 周期性任务
  6. 自动重试
  7. 超时、取消

2、基本任务状态

在asynq中,根据任务的不同类型,有不同的任务状态,以下是大致的任务状态说明: Scheduled:任务正在等待将来处理(仅适用于带有ProcessAt或ProcessIn选项的任务)。

待处理:任务已准备好处理,并将由空闲的工作人员接手。

Active:任务正在由工作人员处理(即随任务调用处理程序)。

重试:worker 处理任务失败,等待将来重试。

已存档:任务达到最大重试次数并存储在存档中以供手动检查。

已完成:任务已成功处理并保留,直到保留 TTL 过期(仅适用于带有选项的任务Retention)。

三、使用方法

go get -u github.com/hibiken/asynq

asynq的使用方法同队列相似。

在队列用法中分为client(相当于生产者)和server(相当于消费者);

在周期性任务中通过Scheduler实例进行注册和执行任务。

1、普通队列用法

1.1 client 创建普通队列

  1. 初始化client
golang 复制代码
//初始化client,指定redis配置参数,后续server配置应和此处一致
redisOpt := asynq.RedisClientOpt{
    Addr:     RedisAddr,
    Password: RedisAuth,
    DB:       1,
}

client := asynq.NewClient(redisOpt)
  1. 新建任务并插入队列
golang 复制代码
//定义一个任意结构体作为负载,后续序列化为json传入任务中
type EmailTaskPayload struct {
    UserId int
}

//定义对应的typename,和队列的topic类似,server通过指定相同的typename消费指定任务
var typename = "test:typename"

//利用NewTask函数新建一个任务

payload, err := json.Marshal(EmailTaskPayload{
    UserId: 56,
})
if err != nil {
    log.Fatal(err)
}

//NewTask函数接收2个固定参数
//typename 指定任务名称
//payload 负载,为消费任务时需要的数据
//返回一个*Task,后续将此结构体指针传入队列
task := asynq.NewTask(typename, payload)

//通过Client的Enqueue方法将任务入队
info, err := client.Enqueue(task)
if err != nil {
    log.Fatal(err)
}

以上就是基本队列入队的使用发放。

1.2 client 创建延迟队列

asynq创建延迟队列非常简单,在创建任务的时候指定配置项即可,且支持的延迟队列有两种:指定时间执行和延迟多少时间执行。对应的配置项为ProcessAtProcessIn

golang 复制代码
//指定时间执行,当前时间+10s执行
task := asynq.NewTask(typename, payload, asynq.ProcessAt(time.Now().Add(time.Second*10)))

//5s后执行
task := asynq.NewTask(typename, payload,asynq.ProcessIn(time.Second * 5))
//入队
info, err := client.Enqueue(task1)
if err != nil {
    log.Fatal(err)
}

1.3 server 消费队列任务

消费队列任务需要初始化server实例,且注册对应的handle方法执行业务逻辑

  1. 初始化server
css 复制代码
redisOpt := asynq.RedisClientOpt{
    Addr:     RedisAddr,
    Password: RedisAuth,
    DB:       1,
}

cfg := asynq.Config{
    Concurrency: 10, //并发处理量
}

srv := asynq.NewServer(redisOpt, cfg)

//run方法同时注册handler 
if err := srv.Run(asynq.HandlerFunc(handler)); err != nil {
    log.Fatal(err)
}
go 复制代码
//函数为固定格式
//type HandlerFunc func(context.Context, *Task) error
//t.Type()返回值为传入的typename
func handler(ctx context.Context, t *asynq.Task) error {
    switch t.Type() {
    case "test:typename":
       var p EmailTaskPayload
       if err := json.Unmarshal(t.Payload(), &p); err != nil {
          return err
       }
       log.Printf(" [*] Send Welcome Email to User %d", p.UserId)
    }
    return nil
}

2、优先级队列用法

asynq默认开启默认的default队列,但是我们在初始化server的时候可以开启不同的优先级队列,并在client入队时指定具体的队列名称,从而实现优先级队列。

2.1 server开启不同优先等级的队列

golang 复制代码
cfg := asynq.Config{
    Concurrency: 10, //并发处理量
    Queues: map[string]int{
        "critical": 6,
        "default":  3,
        "low":      1,

    },
}

srv := asynq.NewServer(redisOpt, cfg)

这将创建一个具有三个队列的实例:criticaldefaultlow。与队列名称关联的数字是队列的优先级。

通过上述配置:

  • critical 队列中的任务将在 60% 的时间内得到处理
  • default 队列中的任务将有30% 的时间被处理
  • low 队列中的任务将被处理10% 的时间

2.2 在client将任务放入队列时指定队列名称

golang 复制代码
//指定放入critical队列
err := client.Enqueue(task, asynq.Queue("critical"))

3、周期性任务使用方法

向队列中添加任务,支持spec表达式和@every的写法

//添加周期性任务

golang 复制代码
redisOpt := asynq.RedisClientOpt{
    Addr:     RedisAddr,
    Password: RedisAuth,
    DB:       1,
}

scheduler := asynq.NewScheduler(redisOpt, nil)
typename := "test:scheduler"
tk1 := asynq.NewTask("test:scheduler", nil)

//使用@every方式
entryID, err := scheduler.Register("@every 20s", tk1)
if err != nil {
    log.Fatal(err)
}
//使用spec 表达式
entryID, err = scheduler.Register("* * * * *", tk1)
if err != nil {
    log.Fatal(err)
}


log.Printf("registered an entry: %q\n", entryID)

scheduler.Run()

消费周期性任务则在server中 用法一致

四、错误重试机制

在server中执行任务时,若返回error则标记为任务失败,进入重试。如果任务用尽其所有重试次数(默认值:25),该任务将移至存档以进行调试和检查,并且不会自动重试(仍然可以使用 CLI 或 WebUI 手动运行任务)

1、每个任务的最大重试次数

NewTask()函数传入可选参数,asynq.MaxRetry(n int),配置重试次数 不传入该参数,则使用默认重试次数25。

2、失败的任务可以再次重试之前等待的持续时间(即延迟)

默认为指数退避。 自定义:

使用RetryDelayFunc函数指定如何计算重试延迟

golang 复制代码
RetryDelayFunc func(n int, e error, t *asynq.Task) time.Duration

在NewServer的时候,通过RetryDelayFunc配置项,根据不同的typename设定时间。

go 复制代码
srv := asynq.NewServer(redis, asynq.Config{
//typename为foo则为2s,其他的则为默认
    RetryDelayFunc: func(n int, e error, t *asynq.Task) time.Duration {
        if t.Type() == "foo" {
            return 2 * time.Second 
        }
        return asynq.DefaultRetryDelayFunc(n, e, t) 
    },
})

3、自定义需要处理的错误

有时您可能希望返回错误并Handler稍后重试该任务,但不想消耗重试计数。例如,您可能希望稍后重试,因为工作线程没有足够的资源来处理该任务。

您可以选择在初始化服务器时提供IsFailure(error) bool函数。Config该谓词函数确定从 Handler 返回的错误是否算作失败。如果函数返回 false(即非失败错误),服务器将不会消耗任务的重试计数,而只是安排任务稍后重试。

go 复制代码
srv := asynq.NewServer(redisConnOpt, asynq.Config{
    // ... other config options
    IsFailure: func(err error) bool {
        return err != ErrResourceNotAvailable // If resource is not available, it's a non-failure error
    },
})

4、跳过重试

如果Handler.ProcessTask返回SkipRetry错误,则无论剩余重试次数是多少,任务都将被存档。返回的错误可以是SkipRetry错误,也可以是包装SkipRetry错误的错误。

golang 复制代码
func ExampleHandler(ctx context.Context, task *asynq.Task) error {
    // Task handling logic here...
    // If the handler knows that the task does not need a retry, then return SkipRetry
    return fmt.Errorf("<reason for skipping retry>: %w", asynq.SkipRetry)
}

五、WebUI页面

github地址:github.com/hibiken/asy...

webui支持docker直接部署,拉取镜像后直接指定redis配置信息运行就行。

shell 复制代码
docker run --rm \
    --name asynqmon \
    --network dev-network \
    -p 8080:8080 \
    hibiken/asynqmon --redis-addr=dev-redis:6379 --redis-password=123456 --redis-db=1

--redis-addr: redis地址 --redis-db: redis数据库编号 默认 0 --redis-password: redis pass

运行成功后,直接进入输入对应的ip:8080进入页面即可看到队列信息。

六、结尾

本文只简单介绍了基本用法,在开发中可能会用得更加深入,具体请参照文档。

文档很详细:github.com/hibiken/asy...

相关推荐
小码编匠1 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平3 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码4 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞4 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
童先生4 小时前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go
LunarCod5 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。5 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man6 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang