后台任务的那些"痛点",你中了几枪?
在 Go 应用里,总有些任务不适合当场做完,比如:
- 发送邮件/短信:总不能让用户点一下按钮,就一直眼巴巴地盯着屏幕等着"发送成功"吧?
- 处理耗时计算:比如生成报表、分析数据,总不能让 CPU 一直被占着,其他请求都卡住吧?
- 定时"叫魂" :每天凌晨的统计任务、每小时的数据同步,难道要我们自己写个无限循环的
time.Sleep
吗?
这些任务,通常会把它们扔进一个"异步任务队列"里,让后台的"工人(Worker)"慢慢去处理。
听起来很美好,但自己动手搞一套,或者用一些比较"原始"的库,马上就会遇到新的麻烦:
- Goroutine 泛滥 :任务一多,是不是就疯狂
go func()
?然后发现成千上万的 Goroutine 难以管理,调度压力山大。 - 失败了怎么办:网络抖动、服务暂时不可用,任务失败了就失败了?当然不行!得有重试机制。
- 任务也分三六九等:关键的支付通知任务,和普通的日志记录任务,能享受同等待遇吗?必须得有优先级!
- 代码越写越乱:定义任务、序列化、反序列化、注册处理器...... 一套流程下来,业务代码和队列逻辑混在一起,简直是一团乱麻。
如果你对以上任何一点感到"是我了",那么,恭喜你,sasynq
就是为你量身定做的"解药"!
sasynq
是什么?
简单来说,sasynq
是一个基于大名鼎鼎的 asynq 库的封装。 asynq
本身已经很优秀了,它是一个基于 Redis 的分布式任务队列,稳定又高效。
觉得asynq
还能更简单、更"傻瓜"一点!sasynq
的目标就是提供一个更简单、更用户友好的 SDK ,同时又完全兼容原生 asynq
的所有高级玩法。
它有什么绝活呢?
- 开箱即用:支持 Redis Cluster 和 Sentinel,高可用、高扩展性,再也不用担心你的任务队列"单点故障"了。
- 功能全面:优先级队列、延迟队列、任务去重、定时任务,这些复杂的调度功能,它都帮你做好了。
- 安全可靠:任务重试、超时、截止时间(Deadline),各种"保险丝"都给你装上了,保证任务"要么成功,要么可控"。
- API 极简 :这是最关键的!它把
asynq
的使用流程大大简化,让你能用更清爽、更优雅的方式编写代码。
sasynq 到底有多好用?
口说无凭,代码为证,直接上代码,看看 sasynq
是如何把复杂的事情变简单的。
第一步:定义你的任务------三种姿势,任君选择
在 sasynq
里,定义一个任务和它的处理器,就像呼吸一样自然。它甚至贴心地提供了三种方式:
go
// example/common/task.go
package common
import (
"context"
"encoding/json"
"fmt"
"github.com/hibiken/asynq"
"github.com/go-dev-frame/sponge/pkg/sasynq"
)
// ----------------------------- 姿势一 (推荐!) ----------------------------------
const TypeEmailSend = "email:send"
// 任务长啥样
type EmailPayload struct {
UserID int `json:"user_id"`
Message string `json:"message"`
}
// 任务怎么做
func HandleEmailTask(ctx context.Context, p *EmailPayload) error {
fmt.Printf("[Email] 嗨,%d号用户,你的邮件发送成功啦!\n", p.UserID)
return nil
}
// ----------------------------- 姿势二 ----------------------------------
const TypeSMSSend = "sms:send"
type SMSPayload struct {
UserID int `json:"user_id"`
Message string `json:"message"`
}
// 让 Payload 自己实现处理方法
func (p *SMSPayload) ProcessTask(ctx context.Context, t *asynq.Task) error {
fmt.Printf("[SMS] 喂,%d号用户,你的短信已送达!\n", p.UserID)
return nil
}
// ----------------------------- 姿势三 ----------------------------------
const TypeMsgNotification = "msg:notification"
type MsgNotificationPayload struct {
UserID int `json:"user_id"`
Message string `json:"message"`
}
// 最原始的方式,自己解析 Payload
func HandleMsgNotificationTask(ctx context.Context, t *asynq.Task) error {
var p MsgNotificationPayload
if err := json.Unmarshal(t.Payload(), &p); err != nil {
return fmt.Errorf("failed to unmarshal payload: %w", err)
}
fmt.Printf("[MSG] 报告!给 %d号用户的消息通知搞定!\n", p.UserID)
return nil
}
看到了吗?尤其是第一种方法,直接把 Payload 类型和处理函数分离开 ,代码结构是不是超级清晰?你再也不用在处理函数里写那段烦人的 json.Unmarshal
了!
第二步:生产任务------想怎么扔,就怎么扔
作为"生产者",你的任务就是把活儿派发出去。sasynq
提供了极其流畅的 API,让你随心所欲地派发任务。
go
// example/producer/main.go
package main
import (
"fmt"
"time"
"github.com/go-dev-frame/sponge/pkg/sasynq"
"example/common"
)
func runProducer(client *sasynq.Client) error {
// 立刻执行!还是十万火急的那种!
payload1 := &common.EmailPayload{UserID: 101, Message: "这个任务最重要!"}
_, _, err := client.EnqueueNow(common.TypeEmailSend, payload1,
sasynq.WithQueue("critical"), // 扔到"紧急"队列
sasynq.WithRetry(5), // 失败了给我重试5次!
)
if err != nil { return err }
// 5秒后执行,优先级一般
payload2 := &common.SMSPayload{UserID: 202, Message: "这个任务不着急..."}
_, _, err = client.EnqueueIn(5*time.Second, common.TypeSMSSend, payload2,
sasynq.WithQueue("default"),
sasynq.WithRetry(3),
)
if err != nil { return err }
// 10秒后执行,优先级最低
payload3 := &common.MsgNotificationPayload{UserID: 303, Message: "有空再做吧"}
_, _, err = client.EnqueueAt(time.Now().Add(10*time.Second), common.TypeMsgNotification, payload3,
sasynq.WithQueue("low"),
sasynq.WithRetry(1),
)
if err != nil { return err }
// 独一无二的任务,别重复执行!
payload4 := &common.EmailPayload{UserID: 404, Message: "这个任务15秒内必须做完!"}
task, _ := sasynq.NewTask(common.TypeEmailSend, payload4)
_, err = client.Enqueue(task,
sasynq.WithQueue("low"),
sasynq.WithDeadline(time.Now().Add(15*time.Second)), // 设置截止时间
sasynq.WithUniqueID("unique-id-xxxx-xxxx"), // 设置唯一ID
)
if err != nil { return err }
return nil
}
EnqueueNow
, EnqueueIn
, EnqueueAt
,这命名,是不是看一眼就知道是干嘛的?通过链式调用的 sasynq.WithXXX
选项,设置队列、重试次数、截止时间都变得异常直观。
第三步:消费任务------注册处理器,so easy!
有了任务,就得有"工人"来干活。sasynq
的消费端(Server)注册处理器也同样简单到令人发指。
go
// example/consumer/main.go
package main
import (
"github.com/go-dev-frame/sponge/pkg/sasynq"
"example/common"
)
func runConsumer(redisCfg sasynq.RedisConfig) (*sasynq.Server, error) {
// 默认就给你分好了 critical, default, low 三个队列
serverCfg := sasynq.DefaultServerConfig()
srv := sasynq.NewServer(redisCfg, serverCfg)
// 注册处理器,注册的三种方式分别对应定义payload的三种定义
// 方式一 (强烈推荐!)
sasynq.RegisterTaskHandler(srv.Mux(), common.TypeEmailSend, sasynq.HandleFunc(common.HandleEmailTask))
// 方式二
srv.Register(common.TypeSMSSend, &common.SMSPayload{})
// 方式三
srv.RegisterFunc(common.TypeMsgNotification, common.HandleMsgNotificationTask)
srv.Run()
return srv, nil
}
看看 RegisterTaskHandler
,它优雅地将任务类型和我们之前定义的 HandleEmailTask
绑定在一起。整个过程行云流水,没有任何多余的胶水代码。
定时任务------你的专属小闹钟
需要每隔几秒、几分钟、几小时就执行一次任务?sasynq
的 Scheduler
让你轻松搞定!
go
package main
// ... (省略定义和处理器)
func registerSchedulerTasks(scheduler *sasynq.Scheduler) error {
// 每2秒去"骚扰"一次谷歌
payload1 := &ScheduledGetPayload{URL: "https://google.com"}
scheduler.RegisterTask("@every 2s", TypeScheduledGet, payload1)
// 每3秒去"问候"一下必应
payload2 := &ScheduledGetPayload{URL: "https://bing.com"}
scheduler.RegisterTask("@every 3s", TypeScheduledGet, payload2)
scheduler.Run()
return nil
}
只需要一行 scheduler.RegisterTask
,传入一个 Cron 表达式(@every 2s
这种简单格式也支持),剩下的事情就交给 sasynq
吧!
结论
sasynq
通过巧妙的封装和设计,它隐藏了 asynq
底层复杂的细节,使用更为简单易用。
它解决了我们日常开发中关于异步任务的种种痛点:代码臃肿、配置复杂、流程不清 。使用 sasynq
,你可以更专注于业务逻辑本身,而不是和任务队列的实现细节"斗智斗勇"。
如果你正在寻找一个能在 Go 项目中快速、安全地实现异步和分布式任务处理的方案,那么 sasynq
绝对是你的不错的选择。它能帮你写出更整洁、更可维护的后台任务代码,让你的后台任务也享受到"丝般顺滑"的体验!
sasynq
地址:github.com/go-dev-frame/sponge/pkg/sasynq
sasynq
是 Sponge 框架的一个子组件,Sponge 是一个强大且易用的 Go 开发框架,其核心理念是 定义即代码 (Definition is Code),帮助开发者以"低代码"方式轻松构建稳定可靠的高性能后端服务(包括 RESTful API、gRPC、HTTP+gRPC、gRPC Gateway 等)。
👉 Sponge 项目地址 :github.com/go-dev-fram...