asynq
是基于redis
实现的,且需要高版本的redis
,本文使用的是Redis-x64-5.0.14.1
。
redis下载地址:
Asynq 工作原理的高级概述:
Asynq的特性:
在github
的asynq
代码仓库里,可以看到Wiki
,这里就是asynq
的官方文档,里面有更详细的使用demo
。
本次示例代码
源代码README.md
中的代码示例
/asynq/official/task/task.go
package tasks
import (
"context"
"encoding/json"
"fmt"
"github.com/hibiken/asynq"
"log"
"time"
)
// A list of task types.
const (
//TypeEmailDelivery :任务类型为邮件
TypeEmailDelivery = "email:deliver"
//TypeImageResize :任务类型为邮件
TypeImageResize = "image:resize"
)
// EmailDeliveryPayload 队列里的任务消息体
type EmailDeliveryPayload struct {
UserID int
TemplateID string
}
// ImageResizePayload 队列里的任务消息体
type ImageResizePayload struct {
SourceURL string
}
//----------------------------------------------
// Write a function NewXXXTask to create a task.
// A task consists of a type and a payload.
//----------------------------------------------
func NewEmailDeliveryTask(userID int, tmplID string) (*asynq.Task, error) {
payload, err := json.Marshal(EmailDeliveryPayload{UserID: userID, TemplateID: tmplID})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeEmailDelivery, payload), nil
}
func NewImageResizeTask(src string) (*asynq.Task, error) {
payload, err := json.Marshal(ImageResizePayload{SourceURL: src})
if err != nil {
return nil, err
}
// task options can be passed to NewTask, which can be overridden at enqueue time.
return asynq.NewTask(TypeImageResize, payload, asynq.MaxRetry(5), asynq.Timeout(20*time.Minute)), nil
}
//---------------------------------------------------------------
// Write a function HandleXXXTask to handle the input task.(编写一个函数 HandleXXXTask 来处理输入的任务)
// Note that it satisfies the asynq.HandlerFunc interface.(请注意它满足 asynq.HandlerFunc 接口。)
//
// Handler doesn't need to be a function. You can define a type
// that satisfies asynq.Handler interface. See examples below.(处理程序不一定需要是一个函数。你可以定义一个满足 asynq.Handler 接口的类型。请参考下面的示例。)
//---------------------------------------------------------------
func HandleEmailDeliveryTask(ctx context.Context, t *asynq.Task) error {
var p EmailDeliveryPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
log.Printf("Sending Email to User: user_id=%d, template_id=%s", p.UserID, p.TemplateID)
// Email delivery code ...
return nil
}
// ImageProcessor implements asynq.Handler interface.
type ImageProcessor struct {
// ... fields for struct
}
func (processor *ImageProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var p ImageResizePayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
}
log.Printf("Resizing image: src=%s", p.SourceURL)
// Image resizing code ...
return nil
}
func NewImageProcessor() *ImageProcessor {
return &ImageProcessor{}
}
/asynq/official/client/client.go
package main
import (
tasks "go-zero-micro/asynq/official/task"
"log"
"time"
"github.com/hibiken/asynq"
)
const redisAddr = "127.0.0.1:6379"
func main() {
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
defer client.Close()
// ------------------------------------------------------
// Example 1: Enqueue task to be processed immediately.
// Use (*Client).Enqueue method.
// ------------------------------------------------------
task, err := tasks.NewEmailDeliveryTask(42, "some:template:id")
if err != nil {
log.Fatalf("could not create task: %v", err)
}
info, err := client.Enqueue(task)
if err != nil {
log.Fatalf("could not enqueue task: %v", err)
}
log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)
// ------------------------------------------------------------
// Example 2: Schedule task to be processed in the future.
// Use ProcessIn or ProcessAt option.
// ------------------------------------------------------------
info, err = client.Enqueue(task, asynq.ProcessIn(24*time.Hour))
if err != nil {
log.Fatalf("could not schedule task: %v", err)
}
log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)
// ----------------------------------------------------------------------------
// Example 3: Set other options to tune task processing behavior.
// Options include MaxRetry, Queue, Timeout, Deadline, Unique etc.
// ----------------------------------------------------------------------------
task, err = tasks.NewImageResizeTask("https://example.com/myassets/image.jpg")
if err != nil {
log.Fatalf("could not create task: %v", err)
}
info, err = client.Enqueue(task, asynq.MaxRetry(10), asynq.Timeout(3*time.Minute))
if err != nil {
log.Fatalf("could not enqueue task: %v", err)
}
log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)
}
/asynq/official/server/server.go
package main
import (
tasks "go-zero-micro/asynq/official/task"
"log"
"github.com/hibiken/asynq"
)
const redisAddr = "127.0.0.1:6379"
func main() {
srv := asynq.NewServer(
asynq.RedisClientOpt{Addr: redisAddr},
asynq.Config{
// Specify how many concurrent workers to use
Concurrency: 10,
// Optionally specify multiple queues with different priority.
Queues: map[string]int{
"critical": 6,
"default": 3,
"low": 1,
},
// See the godoc for other configuration options
},
)
// mux maps a type to a handler
mux := asynq.NewServeMux()
mux.HandleFunc(tasks.TypeEmailDelivery, tasks.HandleEmailDeliveryTask)
mux.Handle(tasks.TypeImageResize, tasks.NewImageProcessor())
// ...register other handlers...
if err := srv.Run(mux); err != nil {
log.Fatalf("could not run server: %v", err)
}
}
asynq 是一个基于 Go 的开源异步任务处理框架,它提供了一种简单的方式来处理异步任务,如任务队列、定时任务等。asynq 可以轻松地与现有的应用程序集成,并且非常易于使用。
NewServer(opts ...Option) *Server
:创建一个新的异步任务处理器。Server.Enqueue(task *Task) error
:将任务添加到任务队列中。Server.EnqueueAt(task *Task, t time.Time) error
:将任务添加到定时任务队列中,指定任务的执行时间。Server.EnqueueIn(task *Task, d time.Duration) error
:将任务添加到延迟任务队列中,指定任务的延迟时间。Server.ProcessTask(fn func(ctx context.Context, task *Task) error) error
:自定义任务处理器,可以编写自己的任务处理逻辑。Server.Start() error
:启动异步任务处理器。Server.Shutdown(ctx context.Context) error
:停止异步任务处理器。NewTask(typ string, payload Payload) *Task
:创建一个新的任务。Task.Meta()
:获取任务的元数据。Task.Payload() Payload
:获取任务的负载数据。Task.Type() string
:获取任务的类型。Task.ID() string
:获取任务的唯一标识符。此外,asynq
还提供了许多其他的 API
,如任务重试、任务优先级、任务过期等。具体的 API
可以参考 asynq
的文档。
通过官方Wiki文档可以选择合适的解决方案。(官方文档
和源代码
为主要参考)
入门:有异步任务处理的代码示例。
server端自定义结构体类型实现处理任务:有代码示例。
server端使用中间件:有代码示例。
任务的生命周期):纯讲解。
信号:纯讲解,注意目前Windows
不支持TSTP信号。
队列优先级:有代码示例。
任务重试:有代码示例。
任务超时和取消:有代码示例。
周期性任务:有代码示例。
周期性任务(动态):有代码示例。
速率限制:有代码示例。
任务唯一性:有代码示例。
任务保留和结果:有代码示例。
任务聚合:有代码示例。
Redis集群:有代码示例。
自动故障转移:有代码示例。
监控和警报:纯讲解。
通过官方文档可以选择合适的解决方案。(官方文档
和源代码
为主要参考)
这里仅展示 异步任务处理
、定时任务处理
、任务重试处理
、任务优先级管理
、延迟任务处理
这五个类型以及asynq
的Web UI
工具asynqmon
的使用。
本次示例代码
以异步发送邮件为例,这个示例其实与官方示例是大同小异,这里是拆分成单个示例了。
/asynq/others_demo/async_task_demo/async_task_task/task.go
package async_task_task
import (
"encoding/json"
"fmt"
"github.com/hibiken/asynq"
"golang.org/x/net/context"
)
// 创建一个新任务类型
const AsyncEmailTask = "async_email_task"
// AsyncEmailPayload 定义发送邮件的负载数据结构
type AsyncEmailPayload struct {
To string `json:"to"`
Subject string `json:"subject"`
Body string `json:"body"`
}
// NewAsyncEmailTask 创建异步电子邮件任务的函数
func NewAsyncEmailTask(asyncEmail AsyncEmailPayload) (*asynq.Task, error) {
payload, err := json.Marshal(asyncEmail)
if err != nil {
return nil, err
}
task := asynq.NewTask(AsyncEmailTask, payload)
return task, err
}
// HandleAsyncEmailTask 处理异步电子邮件任务的函数
func HandleAsyncEmailTask(ctx context.Context, task *asynq.Task) error {
payload := AsyncEmailPayload{}
if err := json.Unmarshal(task.Payload(), &payload); err != nil {
return err
}
// TODO: 模拟发送邮件
fmt.Printf("\nAsync Server:Start handle AsyncTask!")
fmt.Printf("Sending email to %s, subject: %s, body: %s\n", payload.To, payload.Subject, payload.Body)
fmt.Println("Async Server:End handle AsyncTask!")
return nil
}
/asynq/others_demo/async_task_demo/async_task_client/client.go
package main
import (
"fmt"
"github.com/hibiken/asynq"
"go-zero-micro/asynq/others_demo/async_task_demo/async_task_task"
"go-zero-micro/common/utils"
"log"
"time"
)
const redisAddr = "127.0.0.1:6379"
func main() {
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
defer client.Close()
//emailBody := fmt.Sprintf("异步任务邮件已发送,发送时间:%s", time.Now().Format(utils.DateTimeFormat))
//asyncEmail := async_task_task.AsyncEmailPayload{
// To: "[email protected]",
// Subject: "异步任务邮件",
// Body: emailBody,
//}
//task, err := async_task_task.NewAsyncEmailTask(asyncEmail)
//if err != nil {
// log.Fatalf("could not create task: %v", err)
//}
//info, err := client.Enqueue(task)
//if err != nil {
// log.Fatalf("could not enqueue task: %v", err)
//}
//log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)
for i := 0; i < 5; i++ {
emailBody := fmt.Sprintf("异步任务邮件已发送,发送时间:%s", time.Now().Format(utils.DateTimeFormat))
asyncEmail := async_task_task.AsyncEmailPayload{
To: "[email protected]",
Subject: "异步任务邮件",
Body: emailBody,
}
task, err := async_task_task.NewAsyncEmailTask(asyncEmail)
time.Sleep(time.Second)
if err != nil {
log.Fatalf("could not create task: %v", err)
}
//设定服务端立即处理任务
info, err := client.Enqueue(task)
if err != nil {
log.Fatalf("could not enqueue task: %v", err)
}
log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)
}
}
/asynq/others_demo/async_task_demo/async_task_task_server/server.go
package main
import (
"go-zero-micro/asynq/others_demo/async_task_demo/async_task_task"
"log"
"github.com/hibiken/asynq"
)
const redisAddr = "127.0.0.1:6379"
func main() {
srv := asynq.NewServer(
asynq.RedisClientOpt{Addr: redisAddr},
asynq.Config{
// Specify how many concurrent workers to use
Concurrency: 10,
// Optionally specify multiple queues with different priority.
Queues: map[string]int{
"critical": 6,
"default": 3,
"low": 1,
},
// See the godoc for other configuration options
},
)
// mux maps a type to a handler
mux := asynq.NewServeMux()
mux.HandleFunc(async_task_task.AsyncEmailTask, async_task_task.HandleAsyncEmailTask)
// ...register other handlers...
if err := srv.Run(mux); err != nil {
log.Fatalf("could not run server: %v", err)
}
}
启动演示:
先启动服务端,再启动客户端。
asynq的服务端除了直接调用处理任务的方法外,也可以通过声明结构体来实现asynq.Handler
里的ProcessTask(context.Context, *Task) error
接口来处理任务,这样会更加灵活,但是要注意直接调用和通过实现接口只能二选一。
参考:server端自定义结构体类型实现处理任务:有代码示例。
具体代码(核心代码):
/asynq/others_demo/async_task_demo/async_task_task/task.go
// AsyncEmailProcessor implements asynq.Handler interface.
type AsyncEmailProcessor struct {
// ... fields for struct
}
func NewAsyncEmailProcessor() *AsyncEmailProcessor {
return &AsyncEmailProcessor{}
}
func (processor *AsyncEmailProcessor) ProcessTask(ctx context.Context, t *asynq.Task) error {
var payload AsyncEmailPayload
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
//return fmt.Errorf("json.Unmarshal failed: %v: %w", err, asynq.SkipRetry)
return err
}
// TODO: 模拟发送邮件
fmt.Printf("\nAsync Server:ProcessTask:Start handle AsyncTask!\n")
fmt.Printf("Sending email to %s, subject: %s, body: %s\n", payload.To, payload.Subject, payload.Body)
fmt.Println("Async Server:ProcessTask:End handle AsyncTask!")
return nil
}
/asynq/others_demo/async_task_demo/async_task_task_server/server.go
//mux.HandleFunc(async_task_task.AsyncEmailTask, async_task_task.HandleAsyncEmailTask)
mux.Handle(async_task_task.AsyncEmailTask, async_task_task.NewAsyncEmailProcessor())
本次示例代码
定时任务示例的代码与异步任务比较类似,所以复用了异步任务的task.go
和server.go
的代码,这里只展示定时任务client.go
的代码。
package main
import (
"fmt"
"github.com/hibiken/asynq"
"go-zero-micro/asynq/others_demo/async_task_demo/async_task_task"
"go-zero-micro/common/utils"
"log"
"time"
)
const redisAddr = "127.0.0.1:6379"
func main() {
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
defer client.Close()
StartUpScheduledTask(client, 10*time.Second) // 每隔 10 秒执行一次发送数据任务)
}
func StartUpScheduledTask(client *asynq.Client, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
emailBody := fmt.Sprintf("定时任务邮件已发送,发送时间:%s", time.Now().Format(utils.DateTimeFormat))
scheduledEmail := async_task_task.AsyncEmailPayload{
To: "[email protected]",
Subject: "定时任务邮件",
Body: emailBody,
}
task, err := async_task_task.NewAsyncEmailTask(scheduledEmail)
//info, err := client.Enqueue(task)
//※ asynq.ProcessAt(time.Now().Add(interval))是让服务端延迟指定的时间执行
info, err := client.Enqueue(task, asynq.ProcessAt(time.Now().Add(interval)))
if err != nil {
log.Fatalf("could not enqueue task: %v", err)
}
log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)
}
}
启动步骤也是先服务端,后客户端。
注意:
asynq.ProcessAt(time.Now().Add(interval))
是让服务端延迟指定的时间执行。服务端处理时间
和客户端添加时间
发现服务端在处理时确实延迟了指定时间间隔。本次示例代码
使用Golang
的周期性定时器(Ticker
)实现定时器不够灵活,比如设定具体的执行时间等,这时可以使用cron
来实现。
参考:周期性任务:有代码示例。
client.go
代码:
package main
import (
"fmt"
"github.com/hibiken/asynq"
"go-zero-micro/asynq/others_demo/async_task_demo/async_task_task"
"log"
"time"
)
const redisAddr = "127.0.0.1:6379"
func main() {
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
panic(err)
}
scheduler := asynq.NewScheduler(
&asynq.RedisClientOpt{
Addr: redisAddr,
},
&asynq.SchedulerOpts{
Location: loc,
},
)
//emailBody := fmt.Sprintf("定时任务邮件已发送,发送时间:%s", time.Now().Format(utils.DateTimeFormat))
emailBody := fmt.Sprintf("定时任务邮件已发送!")
scheduledEmail := async_task_task.AsyncEmailPayload{
To: "[email protected]",
Subject: "定时任务邮件",
Body: emailBody,
}
task, err := async_task_task.NewAsyncEmailTask(scheduledEmail)
if err != nil {
log.Fatal(err)
}
//entryID1, err := scheduler.Register("* * * * *", task) //每分钟执行一次任务
//entryID1, err := scheduler.Register("*/1 * * * *", task) //每分钟执行一次任务
entryID1, err := scheduler.Register("*/2 * * * *", task) //每2分钟执行一次任务
//entryID1, err := scheduler.Register("@every 10s", task) //每隔10秒执行1次
//entryID1, err := scheduler.Register("@every 1m", task) //每隔1分钟执行1次
//entryID1, err := scheduler.Register("@every 1h", task) //每隔1小时执行1次
if err != nil {
log.Fatal(err)
}
log.Printf("registered an entry: %q\n", entryID1)
// 运行
if err := scheduler.Run(); err != nil {
log.Fatal(err)
}
}
本次示例代码
任务重试处理有两个地方可以配置,分别是在创建任务时,任务入队时。
参考:任务重试:有代码示例。
创建任务时设置重试次数、超时时间。
package main
import (
"encoding/json"
"fmt"
"github.com/hibiken/asynq"
"go-zero-micro/asynq/others_demo/async_task_demo/async_task_task"
"go-zero-micro/common/utils"
"log"
"time"
)
const redisAddr = "127.0.0.1:6379"
func main() {
emailBody := fmt.Sprintf("定时任务邮件已发送,发送时间:%s", time.Now().Format(utils.DateTimeFormat))
retryEmail := async_task_task.AsyncEmailPayload{
To: "[email protected]",
Subject: "定时任务邮件",
Body: emailBody,
}
task, err := NewRetryEmailTask(retryEmail)
if err != nil {
log.Fatal(err)
}
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
info, err := client.Enqueue(task)
if err != nil {
log.Fatal(err)
}
log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)
}
// NewRetryEmailTask 创建重试电子邮件任务的函数
func NewRetryEmailTask(asyncEmail async_task_task.AsyncEmailPayload) (*asynq.Task, error) {
payload, err := json.Marshal(asyncEmail)
if err != nil {
return nil, err
}
//任务级别:创建任务时设置重试次数、超时时间
task := asynq.NewTask(async_task_task.AsyncEmailTask, payload, asynq.MaxRetry(5), asynq.Timeout(1*time.Minute))
return task, err
}
任务入队时设置重试次数、超时时间。
package main
import (
"encoding/json"
"fmt"
"github.com/hibiken/asynq"
"go-zero-micro/asynq/others_demo/async_task_demo/async_task_task"
"go-zero-micro/common/utils"
"log"
"time"
)
const redisAddr = "127.0.0.1:6379"
func main() {
emailBody := fmt.Sprintf("定时任务邮件已发送,发送时间:%s", time.Now().Format(utils.DateTimeFormat))
retryEmail := async_task_task.AsyncEmailPayload{
To: "[email protected]",
Subject: "定时任务邮件",
Body: emailBody,
}
task, err := NewRetryEmailTask(retryEmail)
if err != nil {
log.Fatal(err)
}
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
info, err := client.Enqueue(task, asynq.MaxRetry(5), asynq.Timeout(1*time.Minute))
if err != nil {
log.Fatal(err)
}
log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)
}
// NewRetryEmailTask 创建重试电子邮件任务的函数
func NewRetryEmailTask(asyncEmail async_task_task.AsyncEmailPayload) (*asynq.Task, error) {
payload, err := json.Marshal(asyncEmail)
if err != nil {
return nil, err
}
//任务级别:创建任务时设置重试次数、超时时间
task := asynq.NewTask(async_task_task.AsyncEmailTask, payload, asynq.MaxRetry(5), asynq.Timeout(1*time.Minute))
return task, err
}
参考:队列优先级:有代码示例。
参考:入门。
主要是客户端在将任务入队时使用ProcessIn
或ProcessAt
选项来安排将来要处理的任务。
ProcessIn
:
info, err = client.Enqueue(task, asynq.ProcessIn(24*time.Hour))
ProcessAt
:
package main
import (
"fmt"
"github.com/hibiken/asynq"
"go-zero-micro/asynq/others_demo/async_task_demo/async_task_task"
"go-zero-micro/common/utils"
"log"
"time"
)
const redisAddr = "127.0.0.1:6379"
func main() {
client := asynq.NewClient(asynq.RedisClientOpt{Addr: redisAddr})
defer client.Close()
StartUpScheduledTask(client, 10*time.Second) // 每隔 10 秒执行一次发送数据任务)
}
func StartUpScheduledTask(client *asynq.Client, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
emailBody := fmt.Sprintf("定时任务邮件已发送,发送时间:%s", time.Now().Format(utils.DateTimeFormat))
scheduledEmail := async_task_task.AsyncEmailPayload{
To: "[email protected]",
Subject: "定时任务邮件",
Body: emailBody,
}
task, err := async_task_task.NewAsyncEmailTask(scheduledEmail)
//info, err := client.Enqueue(task)
//※ asynq.ProcessAt(time.Now().Add(interval))是让服务端延迟指定的时间执行
info, err := client.Enqueue(task, asynq.ProcessAt(time.Now().Add(interval)))
if err != nil {
log.Fatalf("could not enqueue task: %v", err)
}
log.Printf("enqueued task: id=%s queue=%s", info.ID, info.Queue)
}
}
asynqmon
源代码:https://github.com/hibiken/asynqmon
本次代码示例
参考文档:
在go-zero
使用asynq
前,如果对asynq
服务端、客户端的代码结构有更清楚的了解,在go-zero
集成asynq
时会更加轻松,很快能够明白各处的作用。
本次示例是将asynq服务端
和asynq客户端
放到同一个服务里,按照先服务端后客户端的顺序启动,也可以拆分成两个独立的服务,根据自己的实际需求改造即可。