一、概述
Asynq 是一个 Go 库,用于对任务进行排队并与工作线程异步处理它们。它由Redis支持,旨在可扩展且易于入门。并且有对应webUi界面可以进行图形化管理。相比kafka、rabbitMq等传统mq消息中间件,更加轻量化
任务队列用作跨多台机器分配工作的机制。一个系统可以由多个工作服务器和代理组成,从而实现高可用性和水平扩展。支持单点redis和redis集群。
GitHub地址:github.com/hibiken/asy...
二、Asynq主要功能和基本任务状态
1、主要功能
- 普通队列
- 优先级队列
- 延时队列
- 定时任务
- 周期性任务
- 自动重试
- 超时、取消
2、基本任务状态
在asynq中,根据任务的不同类型,有不同的任务状态,以下是大致的任务状态说明: Scheduled:任务正在等待将来处理(仅适用于带有ProcessAt或ProcessIn选项的任务)。
待处理:任务已准备好处理,并将由空闲的工作人员接手。
Active:任务正在由工作人员处理(即随任务调用处理程序)。
重试:worker 处理任务失败,等待将来重试。
已存档:任务达到最大重试次数并存储在存档中以供手动检查。
已完成:任务已成功处理并保留,直到保留 TTL 过期(仅适用于带有选项的任务Retention)。
三、使用方法
go get -u github.com/hibiken/asynq
asynq的使用方法同队列相似。
在队列用法中分为client
(相当于生产者)和server
(相当于消费者);
在周期性任务中通过Scheduler
实例进行注册和执行任务。
1、普通队列用法
1.1 client 创建普通队列
- 初始化client
golang
//初始化client,指定redis配置参数,后续server配置应和此处一致
redisOpt := asynq.RedisClientOpt{
Addr: RedisAddr,
Password: RedisAuth,
DB: 1,
}
client := asynq.NewClient(redisOpt)
- 新建任务并插入队列
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创建延迟队列非常简单,在创建任务的时候指定配置项即可,且支持的延迟队列有两种:指定时间执行和延迟多少时间执行。对应的配置项为ProcessAt
和ProcessIn
。
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方法执行业务逻辑
- 初始化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)
这将创建一个具有三个队列的实例:critical 、default 和low。与队列名称关联的数字是队列的优先级。
通过上述配置:
- 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进入页面即可看到队列信息。
六、结尾
本文只简单介绍了基本用法,在开发中可能会用得更加深入,具体请参照文档。