Skip to content
Go
任务创建与入队

任务创建与入队

1 Task 创建

1.1 NewTask

func NewTask(typename string, payload []byte, opts ...Option) *Task
  • typename:任务类型标识,用于 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
}