Skip to content
Go
并发控制与限流

并发控制与限流

1 Server 级别并发控制

1.1 Concurrency 配置

asynq.Config{
    Concurrency: 20, // 最多同时处理 20 个任务
}

🔬 深入原理:Server 内部使用一个容量为 Concurrency 的 buffered channel 作为信号量:

// Asynq 内部近似实现
sema := make(chan struct{}, Concurrency)

// Worker 获取执行权
sema <- struct{}{}        // 阻塞直到有空位
defer func() { <-sema }() // 释放

// 执行任务
handler.ProcessTask(ctx, task)

1.2 并发数选取原则

任务类型 公式 示例(8 核)
CPU 密集型(加密、压缩、图像处理) NumCPU 8
IO 密集型(HTTP、DB、Redis、RPC) NumCPU × 2~4 16~32
混合型 NumCPU × 1.5~2 12~16
import "runtime"

srv := asynq.NewServer(redisOpt, asynq.Config{
    Concurrency: runtime.NumCPU() * 2,
})

性能提示:过高的 Concurrency 不会带来线性性能提升。受限于 Redis 吞吐、下游服务能力、网络带宽等因素,需要通过压测确定最优值。

1.3 多实例并发

实例 A: Concurrency=10
实例 B: Concurrency=10
实例 C: Concurrency=10
──────────────────────
全局并发 = 30(所有实例共享 Redis 队列)

2 队列级别并发控制

通过队列权重间接控制各队列的并发分配:

Queues: map[string]int{
    "critical": 6,  // ~60% Workers → ~12 并发(当 Concurrency=20)
    "default":  3,  // ~30% Workers → ~6 并发
    "low":      1,  // ~10% Workers → ~2 并发
}

3 分布式信号量(x/rate)

实验性功能,位于 github.com/hibiken/asynq/x/rate

当需要在多个 Server 实例之间对特定任务类型进行跨进程并发控制时,使用 x/rate.Semaphore

3.1 创建信号量

import "github.com/hibiken/asynq/x/rate"

// 创建一个针对 "image:process" 任务类型的信号量
// 全局最多同时处理 10 个该类型任务
sema := rate.NewSemaphore(redisOpt, "image:process", 10)

3.2 信号量参数

参数 类型 说明
rco RedisConnOpt Redis 连接配置
scope string 信号量作用域(可用 queue name 或 task type)
maxTokens int 最大并发数

3.3 使用模式

var globalSemaphore = rate.NewSemaphore(redisOpt, "image:process", 5)

func HandleImageProcess(ctx context.Context, task *asynq.Task) error {
    // 确保 context 有 deadline(信号量自动清理过期 token)
    ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
    defer cancel()

    // 尝试获取 token
    acquired, err := globalSemaphore.Acquire(ctx)
    if err != nil || !acquired {
        // 获取失败 → 返回可重试错误,稍后再试
        return &RateLimitError{RetryIn: 30 * time.Second}
    }
    defer globalSemaphore.Release(ctx)

    // 持有 token,执行任务
    return processImage(ctx, task)
}

3.4 完整的限流 Handler 示例

package tasks

import (
    "context"
    "errors"
    "fmt"
    "time"

    "github.com/hibiken/asynq"
    "github.com/hibiken/asynq/x/rate"
)

// 按任务类型的分布式信号量
var taskSemaphores = map[string]*rate.Semaphore{
    "image:process":  rate.NewSemaphore(redisOpt, "image:process", 10),
    "video:transcode": rate.NewSemaphore(redisOpt, "video:transcode", 3),
    "llm:inference":  rate.NewSemaphore(redisOpt, "llm:inference", 2),
}

var ErrRateLimited = errors.New("rate limited")

func RateLimitHandler(next asynq.Handler) asynq.Handler {
    return asynq.HandlerFunc(func(ctx context.Context, t *asynq.Task) error {
        sema, ok := taskSemaphores[t.Type()]
        if !ok {
            // 无限制,直接执行
            return next.ProcessTask(ctx, t)
        }

        // 设置 deadline 确保 token 自动清理
        ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
        defer cancel()

        acquired, err := sema.Acquire(ctx)
        if err != nil || !acquired {
            return fmt.Errorf("%w: %v", ErrRateLimited, err)
        }
        defer sema.Release(ctx)

        return next.ProcessTask(ctx, t)
    })
}

3.5 Server 配合配置

srv := asynq.NewServer(redisOpt, asynq.Config{
    Concurrency: 50,

    // 限流错误不消耗重试次数
    IsFailure: func(err error) bool {
        if errors.Is(err, ErrRateLimited) {
            return false // 不消耗重试配额
        }
        return true
    },

    // 被限流时延迟更久
    RetryDelayFunc: func(n int, e error, t *asynq.Task) time.Duration {
        if errors.Is(e, ErrRateLimited) {
            return time.Duration(n+1) * 15 * time.Second
        }
        return asynq.DefaultRetryDelayFunc(n, e, t)
    },
})

3.6 工作原理

🔬 深入原理x/rate.Semaphore 使用 Redis Lua 脚本实现分布式计数信号量:

Semaphore key:  asynq:sema:<scope>
Redis 结构:     ZSET(member=taskID, score=deadline)

Acquire:
  1. 清理过期成员 (ZREMRANGEBYSCORE)
  2. 检查当前计数 < maxTokens
  3. 是 → ZADD taskID deadline
  4. 否 → 返回 false

Release:
  1. ZREM taskID
  • context 的 deadline 作为 token 的自动过期时间
  • 即使进程崩溃没有调用 Release,token 也会在 deadline 后自动释放
  • 跨所有 Server 实例共享同一限制

4 并发控制策略对比

策略 作用范围 复杂度 适用场景
Concurrency 单实例全局 所有场景的基础配置
队列权重 单实例按队列 不同优先级任务的资源分配
x/rate.Semaphore 跨实例按任务类型 保护下游有限资源(DB连接、API配额)
自定义信号量 自定义 复杂的多维度限流需求

5 保护下游资源示例

// 场景:下游 API 每秒仅支持 10 次调用
// 多个 Worker 实例共享此限制

var apiSemaphore = rate.NewSemaphore(redisOpt, "external_api", 10)

func HandleAPITask(ctx context.Context, t *asynq.Task) error {
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    // 获取调用许可
    acquired, err := apiSemaphore.Acquire(ctx)
    if err != nil || !acquired {
        return fmt.Errorf("api rate limited: %w", err)
    }
    defer apiSemaphore.Release(ctx)

    // 调用外部 API
    resp, err := externalAPI.Call(ctx, t.Payload())
    if err != nil {
        return fmt.Errorf("api call failed: %w", err)
    }

    // 处理响应...
    return nil
}

6 多维度限流架构

对于复杂场景,可组合多种策略:

┌─────────────────────────────────────────┐
│           任务队列 (Redis)               │
│  critical ─► 高权重 (60%)               │
│  default  ─► 中权重 (30%)               │
│  api      ─► 低权重 (10%)               │
└────────────────┬────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────┐
│         Server 1 (Concurrency=10)        │
│  ├── Worker Pool                        │
│  ├── x/rate("api_call", 5) ──► 限流    │
│  └── x/rate("db_write", 20) ──► 限流    │
└─────────────────────────────────────────┘