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.TaskInfoerror

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
}