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...

相关推荐
jessecyj17 分钟前
SpringBoot详解
java·spring boot·后端
爱吃的小肥羊25 分钟前
刚刚!Claude最强大模型泄露,Anthropic紧急封锁
后端
qqty121726 分钟前
Spring Boot管理用户数据
java·spring boot·后端
bearpping1 小时前
SpringBoot最佳实践之 - 使用AOP记录操作日志
java·spring boot·后端
一叶飘零_sweeeet1 小时前
线上故障零扩散:全链路监控、智能告警与应急响应 SOP 完整落地指南
java·后端·spring
开心就好20252 小时前
不同阶段的 iOS 应用混淆工具怎么组合使用,源码混淆、IPA混淆
后端·ios
架构师沉默2 小时前
程序员如何避免猝死?
java·后端·架构
椰奶燕麦3 小时前
Windows PackageManager (winget) 核心故障排错与通用修复指南
后端
zjjsctcdl3 小时前
springBoot发布https服务及调用
spring boot·后端·https
zdl6864 小时前
Spring Boot文件上传
java·spring boot·后端