任务创建与入队
1 Task 创建
1.1 NewTask
func NewTask(typename string, payload []byte, opts ...Option) *Tasktypename:任务类型标识,用于 ServeMux 路由匹配payload:任务负载,推荐 JSON 序列化opts:可选的任务级配置(如 Queue、MaxRetry、Timeout 等)
// 基础创建
task := asynq.NewTask("email:send", payload)
// 带选项创建(选项也可以在 Enqueue 时传递,效果相同)
task := asynq.NewTask("email:send", payload,
asynq.Queue("critical"),
asynq.MaxRetry(5),
)1.2 Task 方法
| 方法 | 返回值 | 说明 |
|---|---|---|
t.Type() |
string |
返回任务类型标识 |
t.Payload() |
[]byte |
返回负载数据 |
t.ResultWriter() |
*ResultWriter |
返回结果写入器(仅在 Handler 中有效,新创建的任务返回 nil) |
1.3 Payload 设计建议
💡 最佳实践:
| 建议 | 说明 |
|---|---|
| 使用 JSON | 跨语言兼容,人类可读,避免 gob 的版本兼容问题 |
| 保持小巧 | Payload 应仅携带标识和必要参数,大数据存对象存储传 URL |
| 避免 time.Time | 传 Unix 时间戳或 RFC3339 字符串,避免时区/序列化问题 |
| 避免动态字段 | 如果使用 Unique 去重,动态字段(时间戳/随机数)会导致去重失效 |
// ✅ 推荐:简洁稳定的结构
type ExportPayload struct {
ReportID string `json:"report_id"`
Format string `json:"format"` // "pdf" | "csv"
NotifyURL string `json:"notify_url"` // 完成后回调
}
// ❌ 避免:Payload 中嵌入大段数据
type BadPayload struct {
ImageData []byte `json:"image_data"` // 应该存对象存储传 URL
}2 Client 创建与生命周期
client := asynq.NewClient(asynq.RedisClientOpt{Addr: "localhost:6379"})
defer client.Close() // 确保释放 Redis 连接Client 是并发安全的,可以在多个 goroutine 中共用。生产环境中建议作为全局单例使用。
3 入队选项详解
Client.Enqueue(task, opts ...Option) 支持以下选项:
3.1 选项速查表
| 选项 | 签名 | 说明 | 默认值 |
|---|---|---|---|
Queue(name) |
func Queue(name string) Option |
指定目标队列 | "default" |
ProcessIn(d) |
func ProcessIn(d time.Duration) Option |
延迟 d 时间后执行 | 无(立即执行) |
ProcessAt(t) |
func ProcessAt(t time.Time) Option |
在指定时间点执行 | 无 |
MaxRetry(n) |
func MaxRetry(n int) Option |
最大重试次数 | 25 |
Timeout(d) |
func Timeout(d time.Duration) Option |
单次执行超时 | 无限制 |
Deadline(t) |
func Deadline(t time.Time) Option |
绝对截止时间 | 无 |
Unique(ttl) |
func Unique(ttl time.Duration) Option |
TTL 内去重 | 不去重 |
TaskID(id) |
func TaskID(id string) Option |
自定义任务 ID | 自动生成 UUID |
Group(name) |
func Group(name string) Option |
聚合组名 | 无 |
Retention(d) |
func Retention(d time.Duration) Option |
完成后保留时长 | 立即清理 |
3.2 延迟与定时入队
// 30 分钟后执行
client.Enqueue(task, asynq.ProcessIn(30*time.Minute))
// 明天上午 9 点执行
tomorrow9am := time.Now().Add(24*time.Hour).Truncate(24*time.Hour).Add(9*time.Hour)
client.Enqueue(task, asynq.ProcessAt(tomorrow9am))
// ProcessIn 和 ProcessAt 互斥,同时使用以最后一个为准🔬 深入原理:延迟/定时任务通过 Redis ZSET 实现,score 为执行时间戳。Forwarder 每隔 DelayedTaskCheckInterval 扫描一次,将到期的任务从 ZSET 移动到 pending list。
3.3 超时与截止时间
// 单次执行超过 5 分钟则强制终止(context 取消)
client.Enqueue(task, asynq.Timeout(5*time.Minute))
// 超过绝对 deadline 后任务不再被调度
client.Enqueue(task, asynq.Deadline(time.Now().Add(1*time.Hour)))| 参数 | 作用阶段 | 超时后行为 |
|---|---|---|
Timeout |
单次执行 | ctx.Done() 触发,任务进入 retry |
Deadline |
整个任务生命周期 | 不再调度,直接进入 archived |
🚨 陷阱:Timeout 只在单次执行中生效。如果任务第一次执行超时,重试时会重新计时。Deadline 则是绝对的——超过该时间后,即使还有剩余重试次数,任务也不再执行。
3.4 组合使用
info, err := client.Enqueue(task,
asynq.Queue("critical"),
asynq.ProcessIn(10*time.Minute),
asynq.MaxRetry(3),
asynq.Timeout(30*time.Second),
asynq.Deadline(time.Now().Add(2*time.Hour)),
asynq.Unique(1*time.Hour),
asynq.Retention(24*time.Hour),
)4 唯一任务(去重)
client.Enqueue(task, asynq.Unique(10*time.Minute))🔬 深入原理:
- 唯一性 key 由
{taskType}:{payload hash}组成 - 使用 Redis
SETNX获取锁,TTL 时间内相同 key 的任务会被拒绝 - 如果 Payload 中包含时间戳、随机数等动态字段,唯一性会失效
唯一性最佳实践
// ✅ 推荐:Payload 中只包含稳定的业务标识
type DedupPayload struct {
OrderID int `json:"order_id"` // 业务唯一标识
Action string `json:"action"` // "cancel" | "refund"
}
// ❌ 错误:包含时间戳导致去重失效
type BadPayload struct {
OrderID int `json:"order_id"`
CreatedAt int64 `json:"created_at"` // 每次不同,去重无效
}🚨 陷阱:Unique 只能防止完全相同的任务在 TTL 内重复入队。如果同一个业务逻辑需要更复杂的去重(如"同一订单的取消操作 5 分钟内只能提交一次"),应通过自定义 TaskID 或在业务层使用分布式锁实现。
5 自定义任务 ID
client.Enqueue(task, asynq.TaskID("order-12345-cancel"))- 如果不指定,Asynq 自动生成 UUID
- 如果指定的 ID 与已存在的任务冲突,
Enqueue返回ErrTaskIdConflict - 💡 适用于业务去重场景:用业务键(如
order:{id}:cancel)作为 TaskID
6 批量入队
⚠️ v0.26.0 尚未正式发布此功能(已合入 master,预计 v0.27.0 发布)
tasks := []*asynq.Task{task1, task2, task3}
infos, err := client.EnqueueBatch(tasks)- 使用 Redis Pipeline 一次性提交多个任务
- 减少网络往返次数
- ⚡ 大量任务创建时性能提升显著
7 任务 Headers
v0.26.0+ 支持
task := asynq.NewTask("my:type", payload,
asynq.Headers(map[string]string{
"X-Trace-ID": traceID,
"X-Tenant-ID": tenantID,
"X-Request-ID": requestID,
}),
)
// Handler 中读取
func Handler(ctx context.Context, t *asynq.Task) error {
traceID := t.Headers["X-Trace-ID"]
// ...
}💡 Headers 适用场景:
- 分布式追踪(传递 Trace ID)
- 多租户标识(传递 Tenant ID)
- 请求溯源(传递 Request ID)
- 自定义元数据(版本号、环境标识等)
8 指定目标队列
// 方式 1:NewTask 时指定
task := asynq.NewTask("email:send", payload, asynq.Queue("critical"))
// 方式 2:Enqueue 时指定(覆盖 NewTask 时的设置)
client.Enqueue(task, asynq.Queue("critical"))如果两处都设置了 Queue 选项,Enqueue 时指定的值会覆盖 NewTask 时的设置。
9 消息持久化
// 任务完成后在 Redis 中保留 24 小时,便于查询和审计
client.Enqueue(task, asynq.Retention(24*time.Hour))- 默认情况下,任务完成后立即清理
- 设置
Retention后,完成任务会在 Redis 中保留指定时长 - 保留期间可通过 Inspector 查询任务结果
- 与
ResultWriter配合使用,可在任务完成后获取执行结果
10 入队返回值
Enqueue 返回 *asynq.TaskInfo 和 error:
info, err := client.Enqueue(task)
if err != nil {
if errors.Is(err, asynq.ErrTaskIdConflict) {
// 自定义 TaskID 冲突
}
log.Printf("enqueue failed: %v", err)
return
}
log.Printf("task enqueued: id=%s queue=%s state=%s", info.ID, info.Queue, info.State)常见错误:
asynq.ErrTaskIdConflict:自定义 TaskID 与已有任务冲突- Redis 连接错误:网络或配置问题
11 完整的 Payload 设计案例
package tasks
import (
"encoding/json"
"fmt"
"github.com/hibiken/asynq"
)
// ─── 类型常量 ───────────────────────
const (
TypeEmailSend = "email:send"
TypeOrderCancel = "order:cancel"
TypeReportExport = "report:export"
TypeImageProcess = "image:process"
)
// ─── Payload 定义 ───────────────────
type EmailSendPayload struct {
To string `json:"to"`
Subject string `json:"subject"`
Body string `json:"body"`
}
type OrderCancelPayload struct {
OrderID int64 `json:"order_id"`
Reason string `json:"reason"`
}
type ReportExportPayload struct {
ReportID string `json:"report_id"`
Format string `json:"format"` // "pdf" | "csv" | "xlsx"
CallbackURL string `json:"callback_url"` // 完成后通知
}
type ImageProcessPayload struct {
ImageURL string `json:"image_url"`
Operations []string `json:"operations"` // "resize", "watermark", "compress"
}
// ─── 任务构造函数 ──────────────────
func NewEmailSendTask(p EmailSendPayload) (*asynq.Task, error) {
data, err := json.Marshal(p)
if err != nil {
return nil, fmt.Errorf("marshal: %w", err)
}
return asynq.NewTask(TypeEmailSend, data,
asynq.Queue("default"),
asynq.MaxRetry(3),
asynq.Retention(24*time.Hour),
), nil
}
func NewOrderCancelTask(orderID int64, reason string) (*asynq.Task, error) {
data, err := json.Marshal(OrderCancelPayload{orderID, reason})
if err != nil {
return nil, err
}
return asynq.NewTask(TypeOrderCancel, data,
asynq.Queue("critical"),
asynq.MaxRetry(5),
asynq.TaskID(fmt.Sprintf("order:%d:cancel", orderID)), // 业务去重
), nil
}