Skip to content
Go
最佳实践与陷阱

最佳实践与陷阱

1 任务设计

1.1 Payload 设计原则

原则 说明 示例
💡 保持小巧 Payload 仅携带标识和参数 {"user_id": 42}{"image_data": "<base64>"}
💡 使用 JSON 跨语言兼容,人类可读 json.Marshal(p)gob.NewEncoder()
💡 避免 time.Time 使用 Unix 时间戳或 RFC3339 "2026-07-17T09:00:00Z"time.Now()
💡 避免动态字段 如果使用 Unique 去重 {"order_id": 123}{"created_at": <now>}
💡 大数据存对象存储 Payload 中只放 URL {"file_url": "https://s3/..."}

1.2 任务类型命名规范

// ✅ 推荐:domain:action 格式
const (
    TypeEmailSend        = "email:send"
    TypeEmailSendWelcome = "email:send:welcome"  // 子类型
    TypeOrderCancel      = "order:cancel"
    TypeOrderRefund      = "order:refund"
    TypeReportExport     = "report:export"
    TypeImageProcess     = "image:process"
)

// ✅ 版本化(当需要不兼容变更时)
const (
    TypeEmailSendV1 = "email:send:v1"
    TypeEmailSendV2 = "email:send:v2"
)

// ❌ 避免:无命名空间,容易冲突
const TypeSend = "send"

1.3 幂等性设计

由于 Asynq 保证至少一次执行,Handler 必须支持幂等:

// 策略 1:数据库唯一约束
func HandleOrderCancel(ctx context.Context, t *asynq.Task) error {
    var p OrderCancelPayload
    json.Unmarshal(t.Payload(), &p)

    // INSERT ... ON DUPLICATE KEY 或 UPSERT
    err := db.CancelOrder(ctx, p.OrderID)
    if errors.Is(err, ErrAlreadyCancelled) {
        return nil // 已取消,幂等
    }
    return err
}

// 策略 2:分布式锁
func HandleUniqueAction(ctx context.Context, t *asynq.Task) error {
    lockKey := fmt.Sprintf("lock:task:%s", t.Type())
    locked, err := redisClient.SetNX(ctx, lockKey, "1", 30*time.Second).Result()
    if err != nil || !locked {
        return nil // 正在处理,跳过
    }
    defer redisClient.Del(ctx, lockKey)

    // 执行实际逻辑
    return doWork(ctx, t)
}

// 策略 3:状态机
func HandleStateTransition(ctx context.Context, t *asynq.Task) error {
    var p Payload
    json.Unmarshal(t.Payload(), &p)

    // 只有状态为 "pending" 时才执行
    rowsAffected, err := db.TransitionState(ctx, p.ID, "pending", "processing")
    if rowsAffected == 0 {
        return nil // 已被其他 Worker 处理,幂等
    }
    return err
}

2 队列规划

2.1 推荐队列结构

队列 权重 用途 并发分配(Concurrency=20)
critical 6 支付回调、订单处理等核心业务 ~12 Workers
default 3 常规任务(邮件、通知等) ~6 Workers
low 1 报表、日志、非紧急任务 ~2 Workers

2.2 队列规划原则

✅ 按业务重要性划分队列
✅ 慢任务和快任务分到不同队列
✅ 有外部依赖的任务单独分队列(便于暂停隔离)
❌ 不要所有任务都用同一个队列
❌ 不要创建过多队列(管理复杂,建议 3~5 个)

2.3 队列隔离示例

asynq.Config{
    Queues: map[string]int{
        "critical":      6,  // 核心业务
        "default":       3,  // 常规任务
        "external_api": 2,  // 调用外部 API(可单独暂停此队列)
        "low":           1,  // 低优先级
    },
}

当外部 API 故障时,可以暂停 external_api 队列而不影响其他任务:

asynq pause external_api

3 错误处理策略

3.1 决策矩阵

错误类型 返回 原因
Payload 格式错误 SkipRetry 重试多次结果相同
业务参数无效(用户不存在、订单已取消) SkipRetry 业务逻辑错误,重试无意义
网络超时 普通 error 可能是临时的,值得重试
数据库连接失败 普通 error 可能是临时的,值得重试
下游服务限流(429) 普通 error 稍后可能恢复
磁盘空间不足 普通 error 运维可能正在清理
任务已被业务撤销 RevokeTask 直接丢弃,不归档

3.2 错误信息设计

// ✅ 推荐:提供上下文
return fmt.Errorf("send email to %s: smtp error: %w", p.To, err)

// ✅ 推荐:标明是否可重试
return fmt.Errorf("transient db error: %w", err)         // 可重试
return fmt.Errorf("invalid user: %w", asynq.SkipRetry)   // 不重试

// ❌ 避免:无上下文的错误
return err

4 性能优化

4.1 Concurrency 调优

1. 从 CPU 核心数开始
2. 观察 CPU 使用率和任务延迟
3. CPU 使用率 < 50% → 提高 Concurrency
4. 任务延迟增加但仍有余量 → 检查下游瓶颈
5. Redis CPU 接近 100% → 考虑 Redis 扩容或降低 Concurrency

4.2 Redis 优化

优化项 配置 说明
连接池 PoolSize >= Concurrency 避免连接等待
独立 DB DB: 0(与其他应用隔离) 避免 key 冲突
内存管理 maxmemory-policy noeviction 防止任务数据被意外淘汰
持久化 RDB + AOF 防止重启后任务丢失

4.3 避免常见性能反模式

// ❌ 在 Handler 中做大量同步 IO
func BadHandler(ctx context.Context, t *asynq.Task) error {
    // 每个任务都创建新的 HTTP Client
    resp, _ := http.Get("https://api.example.com")
    return nil
}

// ✅ 复用 HTTP Client
var httpClient = &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:    100,
        IdleConnTimeout: 90 * time.Second,
    },
}

func GoodHandler(ctx context.Context, t *asynq.Task) error {
    resp, _ := httpClient.Get("https://api.example.com")
    return nil
}

// ❌ 在 Handler 中连接数据库(频繁创建连接)
func BadHandler(ctx context.Context, t *asynq.Task) error {
    db, _ := sql.Open("postgres", dsn)
    defer db.Close()
    // ...
}

// ✅ 注入连接池
type Handler struct {
    db *sql.DB // 全局连接池
}

5 部署最佳实践

5.1 进程分离

┌────────────────┐     ┌────────────────┐     ┌────────────────┐
│ API Server     │────▶│ Redis          │◀────│ Worker Server  │
│ (Client)       │     │                │     │ (Server)       │
│ 处理 HTTP 请求  │     │ 任务队列        │     │ 处理异步任务    │
└────────────────┘     └────────────────┘     └────────────────┘
                               ▲
                        ┌────────────────┐
                        │ Scheduler      │
                        │ 定时任务触发     │
                        └────────────────┘

💡 推荐架构

  • API Server:处理 HTTP 请求,作为 Client 入队任务
  • Worker Server:独立进程,仅运行 Asynq Server 处理任务
  • Scheduler:独立进程,仅运行定时任务调度
  • Asynqmon:独立部署,用于监控

5.2 容器化部署

# Worker Dockerfile
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o /worker ./cmd/worker

FROM alpine:3.20
COPY --from=builder /worker /usr/local/bin/worker
ENTRYPOINT ["/usr/local/bin/worker"]

5.3 优雅关闭配置

srv := asynq.NewServer(redisOpt, asynq.Config{
    Concurrency:     20,
    ShutdownTimeout: 30 * time.Second, // 关键:给活跃任务足够时间
})

// K8s: 确保 terminationGracePeriodSeconds > ShutdownTimeout
# Kubernetes Pod 配置
spec:
  terminationGracePeriodSeconds: 60  # 应大于 ShutdownTimeout
  containers:
  - name: worker
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5"]  # 等待 Endpoint 更新

6 常见陷阱汇总

6.1 Redis 相关

🚨 陷阱 说明 解决方案
Redis 版本过低 < 4.0 不支持部分 Lua 命令 升级 Redis ≥ 5.0
内存爆满 Retention 过长 + 高频任务 缩短 Retention,监控内存
Cluster 兼容性 部分 Lua 脚本跨 slot 失败 优先使用 Sentinel
多应用共享同 DB 不同应用的任务 key 冲突 使用独立 DB 编号
maxmemory 淘汰 allkeys-lru 可能淘汰任务数据 使用 noeviction

6.2 任务设计相关

🚨 陷阱 说明 解决方案
Payload 过大 传输慢,Redis 内存压力 大数据存对象存储,Payload 放 URL
唯一性失效 Payload 包含时间戳/随机数 提取稳定字段,或使用自定义 TaskID
非幂等 Handler 至少一次执行导致重复 数据库唯一约束、状态机、分布式锁
time.Time 序列化 不同时区、反序列化失败 用 Unix 时间戳或 RFC3339 字符串
用 gob 序列化 Go 版本升级不兼容 统一用 JSON
长时间运行的 Handler 阻塞 Worker,无法响应关闭信号 设置 Timeout,定期检查 ctx.Done()

6.3 并发与重试相关

🚨 陷阱 说明 解决方案
无限重试 IsFailure 返回 false 但错误永不恢复 确保可恢复,配合超时或手动终止
ShutdownTimeout 太短 频繁重启,大量孤儿任务 设为最长任务执行时间的 2 倍
重试雪崩 大量任务同时重试打垮下游 RetryDelayFunc 添加随机抖动
Concurrency 太高 Redis 连接不够,上下文切换开销 通过压测确定最优值
忘记 defer client.Close() 连接泄漏 创建后立即 defer Close()
忘记 defer sema.Release() x/rate 信号量 token 泄漏 紧跟 Acquire 写 defer Release

6.4 运维相关

🚨 陷阱 说明 解决方案
死信堆积 归档任务无人清理 定期清理脚本 + 告警
缺少监控 任务堆积不知情 Prometheus + Grafana + 告警规则
队列暂停忘记恢复 维护后队列一直暂停 运维 SOP 记录恢复步骤
Scheduler 多实例未加锁 v0.24 以前版本可能重复调度 使用 PeriodicTaskManager(自带锁)
日志级别太低 Debug 日志撑爆磁盘 生产环境用 Warn 或 Info

6.5 序列化相关

🚨 陷阱 说明 解决方案
用 map[string]interface{} 类型断言易出错 定义明确的 Payload 结构体
Payload 向后不兼容 添加必填字段,旧任务反序列化失败 新字段用指针或设默认值
二进制数据无编码 JSON 不支持二进制 Base64 编码或存对象存储

7 上线前自检清单

任务设计

  • 所有 Handler 支持幂等执行
  • Payload 使用 JSON,结构小巧稳定
  • 任务类型命名使用 domain:action 格式
  • 永久性错误正确返回 SkipRetry
  • 临时性错误保留重试语义

配置

  • Concurrency 根据任务特征合理设置
  • 队列按优先级划分(建议 3~5 个)
  • ShutdownTimeout 大于最长任务执行时间
  • Retention 合理设置,避免内存爆满
  • RetryDelayFunc 根据任务类型定制

部署

  • Redis 配置高可用(Sentinel 或主从)
  • Scheduler 独立部署
  • Worker 可水平扩展
  • 优雅关闭已配置并验证工作正常

监控

  • Prometheus 指标已采集
  • 队列积压告警已配置
  • 死信数量告警已配置
  • Web UI(asynqmon)已部署可访问
  • 关键日志已接入日志平台

运维

  • 死信定期清理脚本已就绪
  • 运维手册包含:暂停/恢复队列、手动重试、清理任务
  • 故障恢复流程已文档化