跳转至正文

Queue ​


简介 ​

github.com/prismgo/framework/queue 是 Laravel Queue 风格的 Go 队列组件,用于把耗时、可重试、可延迟的任务从主请求流程中拆出来异步执行。框架内置 sync 与 redis transport;RabbitMQ 由独立模块 github.com/prismgo/rabbitmq 提供。

队列 worker、失败任务和重启命令的完整参数见 命令行:队列命令。

适合用来处理:

  • 发送短信、站内信、企微消息、邮件
  • 导出报表、生成文件、处理图片和视频
  • 调用第三方接口并按退避策略重试
  • 批量任务拆分、链式任务编排
  • 事件监听器异步化

连接 vs. 队列 ​

在开始使用队列之前,理解"连接"和"队列"的区别很重要:

概念说明示例
连接(Connection)队列后端驱动,对应一个具体的消息中间件sync、redis、rabbitmq
队列(Queue)同一连接内的逻辑分组,类似命名管道default、high、mail、exports

每个连接可以包含多个队列。你可以把高优先级任务投递到 high 队列,普通任务投递到 default 队列,Worker 按优先级顺序消费。

驱动注意事项与前提条件 ​

驱动前提条件适用场景
sync无额外依赖测试、本地开发、无需后台 Worker 的简单场景
redis需要 github.com/prismgo/framework/redis 包已注册连接池生产环境后台消费
rabbitmq安装 github.com/prismgo/rabbitmq 扩展,并提供可用的 RabbitMQ 服务使用 AMQP 后端的生产环境

安装 RabbitMQ 扩展 ​

bash
go get github.com/prismgo/rabbitmq

在 framework 默认 Provider 与业务 Provider 之间注册扩展:

go
import "github.com/prismgo/rabbitmq"

app := foundation.Configure().
    WithExtensionProviders(rabbitmq.ServiceProvider{}).
    WithProviders(applicationProviders...).
    Create()

Boot 只向当前 Application 的 Queue Manager 注册 connector,不会立即建立 AMQP 连接;首次解析 RabbitMQ connection 时才连接 broker。升级后若仍配置 driver: "rabbitmq" 却未安装扩展,Queue Manager 会返回 unknown driver 错误。

配置 ​

配置文件 ​

队列的配置集中注册在 config/queue.go:

go
config.Add("queue", func() map[string]interface{} {
    return map[string]interface{}{
        "default":  Env("QUEUE_CONNECTION", "sync"),
        "encoding": Env("QUEUE_ENCODING", ""),
        "failed": map[string]interface{}{
            "driver": Env("QUEUE_FAILED_DRIVER", "memory"),
            "store":  Env("QUEUE_FAILED_STORE", "default"),
            "prefix": Env("QUEUE_FAILED_PREFIX", "workorder_queue"),
            "ttl":    Env("QUEUE_FAILED_TTL", 0),
        },
        "batching": map[string]interface{}{
            "driver": Env("QUEUE_BATCHING_DRIVER", "memory"),
            "store":  Env("QUEUE_BATCHING_STORE", "default"),
            "prefix": Env("QUEUE_BATCHING_PREFIX", "workorder_queue"),
            "ttl":    Env("QUEUE_BATCHING_TTL", 0),
        },
        "restart": map[string]interface{}{
            "cache": Env("QUEUE_RESTART_CACHE", ""),
            "key":   Env("QUEUE_RESTART_KEY", "prismgo:queue:restart"),
        },
        "connections": map[string]interface{}{
            "sync":    map[string]interface{}{ /* ... */ },
            "redis":   map[string]interface{}{ /* ... */ },
            "rabbitmq": map[string]interface{}{ /* ... */ },
        },
    }
})

配置参数 ​

顶层配置 ​

配置路径环境变量默认值说明
queue.defaultQUEUE_CONNECTIONsync默认连接名称
queue.encodingQUEUE_ENCODING""(继承 encoding.default,最终默认 msgpack)Payload 编码方式,支持 msgpack 或 json

Sync 连接 ​

配置路径环境变量默认值说明
queue.connections.sync.driver—sync驱动类型
queue.connections.sync.queueSYNC_QUEUEdefault默认队列名

Redis 连接 ​

配置路径环境变量默认值说明
queue.connections.redis.driver—redis驱动类型
queue.connections.redis.queueREDIS_QUEUEdefault默认队列名
queue.connections.redis.prefixREDIS_QUEUE_PREFIXworkorder_queueRedis queue transport key 前缀
queue.connections.redis.connectionREDIS_QUEUE_CONNECTIONdefault使用的 database.redis.{connection} 连接名
queue.connections.redis.retry_afterREDIS_QUEUE_RETRY_AFTER90reserved 任务超时后重新可见的秒数
queue.connections.redis.block_forREDIS_QUEUE_BLOCK_FOR0Worker 阻塞等待任务的秒数

失败任务存储 ​

配置路径环境变量默认值说明
queue.failed.driverQUEUE_FAILED_DRIVERmemory存储驱动,支持 memory、redis
queue.failed.storeQUEUE_FAILED_STOREdefaultRedis failed store 使用的连接名
queue.failed.prefixQUEUE_FAILED_PREFIXworkorder_queueRedis failed store key 前缀
queue.failed.ttlQUEUE_FAILED_TTL0失败任务 TTL,0 表示不过期

批次状态存储 ​

配置路径环境变量默认值说明
queue.batching.driverQUEUE_BATCHING_DRIVERmemory存储驱动,支持 memory、redis
queue.batching.storeQUEUE_BATCHING_STOREdefaultRedis batch store 使用的连接名
queue.batching.prefixQUEUE_BATCHING_PREFIXworkorder_queueRedis batch store key 前缀
queue.batching.ttlQUEUE_BATCHING_TTL0批次状态 TTL,0 表示不过期

重启信号存储 ​

配置路径环境变量默认值说明
queue.restart.cacheQUEUE_RESTART_CACHE空使用的 cache store;空值表示应用默认 cache store
queue.restart.keyQUEUE_RESTART_KEYprismgo:queue:restart重启信号 key

RabbitMQ 配置 ​

RabbitMQ 连接的私有字段保存在 connection options 中,由 RabbitMQ connector 解析。应用侧通常只需设置 QUEUE_CONNECTION=rabbitmq 和 RabbitMQ 连接参数,其余字段使用默认值即可。

连接参数 ​

配置路径环境变量默认值说明
queue.connections.rabbitmq.urlRABBITMQ_URL空完整 AMQP URL;非空时优先于分字段配置
queue.connections.rabbitmq.schemeRABBITMQ_SCHEMEamqpAMQP 协议头
queue.connections.rabbitmq.hostRABBITMQ_HOST127.0.0.1Broker 主机
queue.connections.rabbitmq.portRABBITMQ_PORT5672Broker 端口
queue.connections.rabbitmq.usernameRABBITMQ_USERNAME空用户名
queue.connections.rabbitmq.passwordRABBITMQ_PASSWORD空密码
queue.connections.rabbitmq.vhostRABBITMQ_VHOST/Virtual host

Exchange 与 Queue ​

配置路径环境变量默认值说明
queue.connections.rabbitmq.exchangeRABBITMQ_EXCHANGEprismgo.queue业务消息使用的 exchange
queue.connections.rabbitmq.exchange_typeRABBITMQ_EXCHANGE_TYPEdirectExchange 类型;默认 direct,routing key 等于 queue name
queue.connections.rabbitmq.declareRABBITMQ_DECLAREtrue是否自动声明 exchange、queue、binding
queue.connections.rabbitmq.exchange_durableRABBITMQ_EXCHANGE_DURABLEtrueExchange 是否持久化
queue.connections.rabbitmq.queue_durableRABBITMQ_QUEUE_DURABLEtrue业务 queue 是否持久化
queue.connections.rabbitmq.queue_max_priorityRABBITMQ_QUEUE_MAX_PRIORITY0x-max-priority;0 表示不开启优先级队列
queue.connections.rabbitmq.message_persistentRABBITMQ_MESSAGE_PERSISTENTtrue消息是否使用 persistent delivery mode
queue.connections.rabbitmq.auto_deleteRABBITMQ_AUTO_DELETEfalseExchange/queue 无使用者时是否自动删除
queue.connections.rabbitmq.exclusiveRABBITMQ_EXCLUSIVEfalseQueue 是否仅允许当前连接独占
queue.connections.rabbitmq.no_waitRABBITMQ_NO_WAITfalse声明资源时是否不等待 broker 响应

发布与确认 ​

配置路径环境变量默认值说明
queue.connections.rabbitmq.confirmRABBITMQ_CONFIRMtrue发布时是否等待 publisher confirm
queue.connections.rabbitmq.publish_timeoutRABBITMQ_PUBLISH_TIMEOUT5发布、等待重连和 confirm 的超时秒数
queue.connections.rabbitmq.publish_channelsRABBITMQ_PUBLISH_CHANNELS1发布专用 AMQP channel 池大小

延迟模式 ​

配置路径环境变量默认值说明
queue.connections.rabbitmq.delay_modeRABBITMQ_DELAY_MODEplugin延迟模式:plugin、ttl_dlx 或 none
queue.connections.rabbitmq.delay_bucketsRABBITMQ_DELAY_BUCKETS5,10,30,60,300,900,3600ttl_dlx 模式的固定延迟 bucket(秒)

消费与重连 ​

配置路径环境变量默认值说明
queue.connections.rabbitmq.prefetchRABBITMQ_PREFETCH1Consumer 预取数量
queue.connections.rabbitmq.heartbeatRABBITMQ_HEARTBEAT10AMQP heartbeat 秒数
queue.connections.rabbitmq.reconnect_min_delayRABBITMQ_RECONNECT_MIN_DELAY100ms断线后最小重连退避
queue.connections.rabbitmq.reconnect_max_delayRABBITMQ_RECONNECT_MAX_DELAY5s断线后最大重连退避
queue.connections.rabbitmq.block_forRABBITMQ_BLOCK_FOR1Pop 阻塞等待秒数

重启与拓扑缓存 ​

配置路径环境变量默认值说明
queue.connections.rabbitmq.restart_queueRABBITMQ_RESTART_QUEUEprismgo.queue.restart跨进程时间戳队列
queue.connections.rabbitmq.restart_enabledRABBITMQ_RESTART_ENABLEDtrue是否启用 RabbitMQ RestartStore
queue.connections.rabbitmq.restart_poll_intervalRABBITMQ_RESTART_POLL_INTERVAL1s读取 restart queue 的本地降载窗口
queue.connections.rabbitmq.topology_cache_ttlRABBITMQ_TOPOLOGY_CACHE_TTL0拓扑缓存 sliding TTL;0 表示不按时间淘汰
queue.connections.rabbitmq.topology_cache_max_entriesRABBITMQ_TOPOLOGY_CACHE_MAX_ENTRIES0拓扑缓存容量上限;0 表示不按容量淘汰

注意:不要在 RabbitMQ 连接中显式配置非零 retry_after。RabbitMQ 不支持 Redis 风格的 visibility timeout,如果显式传入非零 retry_after,Manager 初始化会返回 queue.ErrUnsupportedRetryAfter。

创建任务 ​

任务结构 ​

Job 的最小契约只有一个方法:

go
type Job interface {
    Handle(context.Context) error
}

创建一个任务只需实现 Handle 方法,并在 init 中注册类型:

go
package jobs

import (
    "context"
    "fmt"
    "time"

    "github.com/prismgo/framework/queue"
)

type SendWelcomeMailJob struct {
    UserID uint   `json:"user_id"`
    Email  string `json:"email"`
}

func init() {
    queue.RegisterType[*SendWelcomeMailJob]()
}

func (j *SendWelcomeMailJob) Handle(ctx context.Context) error {
    fmt.Printf("send welcome mail to user=%d email=%s\n", j.UserID, j.Email)
    return nil
}

类型注册很重要:Redis Worker 是独立进程时,只能通过 RegisterType[*T]() 知道如何恢复 Job。推荐在 Job 所在包的 init 中注册。

Payload 规范:Job 会按 Payload Encoding 序列化保存到队列。只把业务 ID、必要快照和幂等判断字段放进 Job,不要把数据库连接、repository、service、logger 等运行时对象放进 payload。

任务策略接口 ​

除 Handle 外,其余接口都是可选能力。实现某个方法后,Dispatch 会在投递时读取它并写入任务 envelope;Worker 后续按 envelope 中的策略执行。

接口名方法用途可被投递选项覆盖
ConnectionProviderQueueConnection() string指定默认连接queue.OnConnection
QueueProviderQueueName() string指定默认队列queue.OnQueue
DelayProviderQueueDelay() time.Duration指定默认延迟queue.Delay
TriesProviderTries() int最大执行次数queue.Tries
TimeoutProviderTimeout() time.Duration单次执行超时queue.Timeout
BackoffProviderBackoff() []time.Duration失败后退避序列queue.Backoff
RetryUntilProviderRetryUntil() time.Time重试截止时间queue.RetryUntil
MaxExceptionsProviderMaxExceptions() int最大异常次数queue.MaxExceptions
FailOnTimeoutProviderFailOnTimeout() bool超时后直接失败queue.FailOnTimeout
EncryptedProviderShouldEncrypt() bool加密 payloadqueue.Encrypt
MiddlewareProviderMiddleware() []MiddlewareJob 级中间件否
UniqueIDProviderUniqueID() string唯一任务 keyqueue.Unique
UniqueForProviderUniqueFor() time.Duration唯一锁 TTLqueue.Unique
UniqueViaProviderUniqueVia() cachecontract.Repository唯一锁缓存 storequeue.UniqueVia
UniqueUntilProcessingProviderUniqueUntilProcessing() bool处理前释放唯一锁queue.UniqueUntilProcessing
DebounceIDProviderDebounceID() string防抖 keyqueue.Debounce
DebounceForProviderDebounceFor() time.Duration防抖窗口queue.Debounce
DebounceViaProviderDebounceVia() cachecontract.Repository防抖缓存 storequeue.DebounceVia
FailedProviderFailed(context.Context, error)最终失败回调否
TagsProviderTags() []stringHorizon 展示标签queue.Tags
SilencedProviderSilenced() boolHorizon 静默queue.Silenced

策略优先级:DispatchOption > Job 方法 > Manager/Worker 默认值。

下面示例展示一个较完整的 Job,实际项目中只实现需要的接口即可:

go
type SyncOrderJob struct {
    OrderID  uint   `json:"order_id"`
    TenantID uint   `json:"tenant_id"`
}

func (j *SyncOrderJob) Handle(ctx context.Context) error {
    if j.OrderID == 0 {
        return queue.Fail(errors.New("missing order id"))
    }
    // 在 Handle 内重新获取 service/repository,再按 ID 查询最新业务数据。
    return nil
}

func (j *SyncOrderJob) QueueConnection() string { return "redis" }
func (j *SyncOrderJob) QueueName() string       { return "orders" }
func (j *SyncOrderJob) Tries() int              { return 5 }
func (j *SyncOrderJob) Timeout() time.Duration  { return 30 * time.Second }
func (j *SyncOrderJob) FailOnTimeout() bool     { return true }
func (j *SyncOrderJob) MaxExceptions() int      { return 3 }
func (j *SyncOrderJob) Backoff() []time.Duration {
    return []time.Duration{time.Second, 10 * time.Second, time.Minute}
}
func (j *SyncOrderJob) RetryUntil() time.Time {
    return time.Now().Add(15 * time.Minute)
}
func (j *SyncOrderJob) ShouldEncrypt() bool { return true }
func (j *SyncOrderJob) UniqueID() string {
    return fmt.Sprintf("sync-order:%d", j.OrderID)
}
func (j *SyncOrderJob) UniqueFor() time.Duration { return 10 * time.Minute }
func (j *SyncOrderJob) Middleware() []queue.Middleware {
    return []queue.Middleware{
        queue.WithoutOverlapping(fmt.Sprintf("sync-order:%d", j.OrderID)).
            ReleaseAfter(30 * time.Second).
            ExpireAfter(10 * time.Minute),
    }
}
func (j *SyncOrderJob) Failed(ctx context.Context, err error) {
    // 记录最终失败日志或发送告警
}

唯一任务 ​

唯一任务用于防止同一业务 key 重复入队。锁存在时,Dispatch 返回 queue.ErrDuplicate。

go
func (j *SyncOrderJob) UniqueID() string {
    return fmt.Sprintf("sync-order:%d", j.OrderID)
}

func (j *SyncOrderJob) UniqueFor() time.Duration {
    return 10 * time.Minute
}

指定缓存 store:

go
func (j *SyncOrderJob) UniqueVia() cachecontract.Repository {
    return cache.Store("redis")
}

只在任务开始处理前保持唯一锁(允许处理期间新的任务入队):

go
func (j *SyncOrderJob) UniqueUntilProcessing() bool { return true }

也可以只对单次投递启用:

go
_, err := queue.Dispatch(
    ctx,
    &jobs.SyncOrderJob{OrderID: 10001},
    queue.Unique("sync-order:10001", 10*time.Minute),
    queue.UniqueVia(cache.Store("redis")),
)

处理重复投递:

go
_, err := queue.Dispatch(ctx, &jobs.SyncOrderJob{OrderID: 10001})
if errors.Is(err, queue.ErrDuplicate) {
    return nil // 已有同 key 任务在队列中,忽略
}
if err != nil {
    return err
}

唯一任务 vs. WithoutOverlapping:

能力语义适用场景
唯一任务已有同 key 任务时,后续投递直接失败防止重复同步、重复结算、重复通知
WithoutOverlapping同 key 任务不并发执行,但可以排队允许多个任务入队,但不允许同一业务 ID 并发执行

防抖任务 ​

防抖适合"短时间内多次触发,只执行最后一次"的场景,例如刷新索引、重算统计。

go
type RefreshCustomerStatsJob struct {
    CustomerID uint `json:"customer_id"`
}

func (j *RefreshCustomerStatsJob) Handle(ctx context.Context) error {
    // 重算客户统计
    return nil
}

func (j *RefreshCustomerStatsJob) DebounceID() string {
    return fmt.Sprintf("customer:stats:%d", j.CustomerID)
}

func (j *RefreshCustomerStatsJob) DebounceFor() time.Duration {
    return 30 * time.Second
}

func (j *RefreshCustomerStatsJob) DebounceVia() cachecontract.Repository {
    return cache.Store("redis")
}

单次投递启用防抖:

go
_, err := queue.Dispatch(
    ctx,
    &jobs.RefreshCustomerStatsJob{CustomerID: 1001},
    queue.Debounce("customer:stats:1001", 30*time.Second),
    queue.DebounceVia(cache.Store("redis")),
)

防抖投递时,如果当前延迟小于防抖窗口,组件会把任务可执行时间推迟到窗口之后。Worker 消费旧任务时会检查它是否已经不是该 key 的最后一次投递;旧任务会被删除,不再执行 Handle。

唯一任务 vs. 防抖任务:

能力语义适用场景
唯一任务已有同 key 任务时,后续投递直接失败并返回 ErrDuplicate防止重复同步、重复结算
防抖任务同 key 后续投递会覆盖前面的投递,只执行最后一次搜索索引刷新、统计重算、频繁编辑后的延迟保存

加密任务 ​

如果 Job payload 包含敏感字段,可以加密保存:

go
func (j *SendWelcomeMailJob) ShouldEncrypt() bool { return true }

或只加密单次投递:

go
_, err := queue.Dispatch(ctx, &jobs.SendWelcomeMailJob{UserID: 1001}, queue.Encrypt())

加密使用 AES-GCM,密钥来自 app.key 配置,支持 base64: 前缀(与 Laravel 兼容)。

任务中间件 ​

Job 可以实现 Middleware() []queue.Middleware 声明自己的执行中间件。Manager 也可以通过 UseMiddleware 注册全局中间件。执行顺序是:Manager 全局 middleware 在前,Job 自身 middleware 在后。

频率限制 ​

RateLimit 限制任务执行频率,适合短信、邮件、第三方 API。超过限制时,任务不会失败,而是通过 ReleaseAfter 释放回队列,等待窗口恢复后再执行。

go
func (j *SendWelcomeMailJob) Middleware() []queue.Middleware {
    return []queue.Middleware{
        queue.RateLimit("mail:provider", 20, time.Second),
    }
}

也可以注册成 Manager 级全局 middleware:

go
queue.UseMiddleware(queue.RateLimit("queue:global", 1000, time.Minute))

防止任务重叠 ​

WithoutOverlapping 保证同一 key 的任务不会并发执行。它和唯一任务不同:唯一任务控制"能不能入队",WithoutOverlapping 控制"能不能同时执行"。

拿不到锁时默认释放回队列稍后再试:

go
func (j *SyncOrderJob) Middleware() []queue.Middleware {
    return []queue.Middleware{
        queue.WithoutOverlapping(fmt.Sprintf("sync-order:%d", j.OrderID)).
            ReleaseAfter(30 * time.Second).
            ExpireAfter(10 * time.Minute).
            Via(cache.Store("redis")),
    }
}

拿不到锁时直接跳过:

go
queue.WithoutOverlapping("sync-order:10001").DontRelease()
方法说明
ReleaseAfter(d)拿不到锁时多久后释放回队列
DontRelease()拿不到锁时直接跳过当前任务
ExpireAfter(d)执行锁的最长持有时间
Via(store)指定互斥锁使用的缓存 store
Shared()保留 Laravel 风格 API;当前锁 key 默认已在连接范围内共享

异常节流 ​

ThrottlesExceptions 在第三方连续异常时暂停一段时间再继续尝试。max 表示异常阈值,decay 表示达到阈值后的冷却时间。

go
func (j *SyncProviderJob) Middleware() []queue.Middleware {
    return []queue.Middleware{
        queue.ThrottlesExceptions(10, 5*time.Minute).
            By("provider:sync").
            Via(cache.Store("redis")).
            Backoff(30 * time.Second).
            When(func(err error) bool {
                return !errors.Is(err, context.Canceled)
            }),
    }
}
方法说明
By(key)指定异常节流 key,默认使用任务类型名
Backoff(d)未达到节流阈值前的释放延迟
When(fn)只让满足条件的错误参与计数
Via(store)指定异常节流计数使用的缓存 store

跳过任务 ​

SkipIf 在 predicate 返回 true 时跳过任务,Worker 会按成功处理并删除任务:

go
func (j *SyncOrderJob) Middleware() []queue.Middleware {
    return []queue.Middleware{
        queue.SkipIf(func(job queue.Job) bool {
            j := job.(*SyncOrderJob)
            return j.OrderID == 0
        }),
    }
}

自定义中间件 ​

go
func TenantGuard(tenantID uint) queue.Middleware {
    return queue.MiddlewareFunc(func(ctx context.Context, job queue.Job, next queue.Next) error {
        if tenantID == 0 {
            return queue.ErrSkipped
        }
        return next(ctx)
    })
}

queue.ErrSkipped 表示任务被主动跳过,Worker 会按成功处理并删除任务。普通错误会进入重试或失败流程。

分发任务 ​

基础投递:

go
id, err := queue.Dispatch(ctx, &jobs.SyncOrderJob{
    OrderID:  10001,
    TenantID: 1,
})

投递时覆盖连接、队列、延迟、重试和超时:

go
id, err := queue.Dispatch(
    ctx,
    &jobs.SyncOrderJob{OrderID: 10001, TenantID: 1},
    queue.OnConnection("redis").
        OnQueue("orders").
        Delay(30*time.Second).
        Tries(5).
        Timeout(20*time.Second).
        Backoff(5*time.Second, 30*time.Second, time.Minute).
        RetryUntil(time.Now().Add(15*time.Minute)),
)
_ = id

DispatchOption 是带链式方法的命名函数类型,因此任意一个 option 都可以作为链起点:

go
id, err := queue.Dispatch(
    ctx,
    &jobs.SyncOrderJob{OrderID: 10001, TenantID: 1},
    queue.Delay(30*time.Second).
        OnConnection("redis").
        OnQueue("orders").
        Tries(5),
)
_ = id

延迟分发 ​

使用 Delay 选项:

go
id, err := queue.Dispatch(
    ctx,
    &jobs.SyncOrderJob{OrderID: 10001},
    queue.Delay(5 * time.Minute),
)

使用 Later 函数(秒级延迟):

go
id, err := queue.Later(ctx, 300, &jobs.SyncOrderJob{OrderID: 10001})

使用 DelaySeconds 选项:

go
id, err := queue.Dispatch(
    ctx,
    &jobs.SyncOrderJob{OrderID: 10001},
    queue.DelaySeconds(300),
)

同步分发 ​

sync 连接会在 queue.Dispatch 当前调用栈内同步执行任务。任务失败会直接返回给调用方,不会走 Worker retry 或 failed store。

go
// 配置 QUEUE_CONNECTION=sync 时,Dispatch 直接同步执行
id, err := queue.Dispatch(ctx, &jobs.SyncOrderJob{OrderID: 10001})

也可以临时指定同步连接:

go
id, err := queue.Dispatch(
    ctx,
    &jobs.SyncOrderJob{OrderID: 10001},
    queue.OnConnection("sync"),
)

任务链 ​

链式任务按顺序执行,前一个成功后才投递下一个:

go
id, err := queue.Chain(
    &jobs.ExtractMediaJob{ID: 1},
    &jobs.ConvertMediaJob{ID: 1},
    &jobs.NotifyMediaReadyJob{ID: 1},
).Options(
    queue.OnConnection("redis"),
    queue.OnQueue("media"),
    queue.Tries(3),
).Dispatch(ctx)
_ = id

链中任一任务失败,后续任务不会投递。

自定义队列与连接 ​

通过 DispatchOption 覆盖 Job 默认的连接和队列:

go
id, err := queue.Dispatch(
    ctx,
    &jobs.SendWelcomeMailJob{UserID: 1001},
    queue.OnConnection("redis"),
    queue.OnQueue("mail"),
)

指定最大尝试次数与超时 ​

go
id, err := queue.Dispatch(
    ctx,
    &jobs.SyncOrderJob{OrderID: 10001},
    queue.Tries(5),
    queue.Timeout(30*time.Second),
    queue.FailOnTimeout(),
    queue.Backoff(5*time.Second, 30*time.Second, time.Minute),
    queue.RetryUntil(time.Now().Add(15*time.Minute)),
    queue.MaxExceptions(3),
)

错误处理 ​

在 Handle 内部,你可以控制任务的行为:

普通错误:触发重试判断,Worker 会根据 tries、maxExceptions、retryUntil 和 backoff 决定重新入队或写入失败任务。

go
func (j *SyncOrderJob) Handle(ctx context.Context) error {
    if temporaryFailure() {
        return errors.New("provider timeout")
    }
    return nil
}

直接失败:不再重试,立即进入失败任务。

go
func (j *SyncOrderJob) Handle(ctx context.Context) error {
    if j.OrderID == 0 {
        return queue.Fail(errors.New("missing order id"))
    }
    return nil
}

释放回队列:指定下次执行时间。

go
func (j *SyncOrderJob) Handle(ctx context.Context) error {
    if providerBusy() {
        return queue.ReleaseAfter(2*time.Minute, errors.New("provider busy"))
    }
    return nil
}

跳过任务:Worker 按成功处理并删除任务。

go
func (j *SyncOrderJob) Handle(ctx context.Context) error {
    if alreadyProcessed() {
        return queue.ErrSkipped
    }
    return nil
}

最终失败回调:只在任务最终失败时调用一次,不会在每次普通重试时调用。

go
func (j *SyncOrderJob) Failed(ctx context.Context, err error) {
    logger.WithFields(map[string]any{
        "order_id":  j.OrderID,
        "tenant_id": j.TenantID,
    }).WithError(err).Error("sync order job failed finally")
}

任务批处理 ​

批量任务适合把一个大任务拆成多个子任务,并跟踪整体进度。

分发批次 ​

go
status, err := queue.Batch(
    &jobs.ExportChunkJob{Chunk: 1},
    &jobs.ExportChunkJob{Chunk: 2},
    &jobs.ExportChunkJob{Chunk: 3},
).Name("customer-export").
    Options(queue.OnConnection("redis"), queue.OnQueue("exports")).
    Dispatch(ctx)
if err != nil {
    return err
}
_ = status

检查批次 ​

go
latest, err := queue.GetBatchStatus(ctx, status.ID)
if err != nil {
    return err
}
fmt.Printf("pending=%d processed=%d failed=%d\n",
    latest.Pending, latest.Processed, latest.Failed)

Worker 执行批次内任务时会自动调用 queue.MarkBatchJob 更新进度。成功路径标记 success=true;最终失败归档成功后标记 success=false;可重试失败不更新批次进度。

取消批次 ​

go
err := queue.CancelBatch(ctx, status.ID)

批次取消后,未执行的子任务会被跳过(返回 queue.ErrBatchCancelled)。

运行队列 Worker ​

queue:work 命令 ​

sync 连接不需要启动 Worker。redis 和 rabbitmq 连接需要启动 Worker 消费:

bash
# 启动默认连接的 Worker
go run ./ queue

# 指定连接和队列
go run ./ queue redis --queue=high,default

# 只处理一个任务后退出
go run ./ queue --once

# 队列为空后退出
go run ./ queue --stop-when-empty

# 限制任务数和运行时间
go run ./ queue --max-jobs=1000 --max-time=3600

queue 命令同时保留 queue:work 别名。

参数说明
{connection?}队列连接名称,例如 redis
--queue逗号分隔的队列名,Worker 按顺序消费
--once只处理一个任务后退出
--stop-when-empty队列为空后退出
--sleep队列为空时休眠秒数
--timeout单个任务执行超时秒数
--tries默认最大尝试次数
--backoff失败后退避秒数,支持逗号分隔
--retry-afterreserved 任务重新可见秒数
--max-jobs处理指定数量任务后退出
--max-time运行指定秒数后退出

代码中启动 Worker:

go
manager, err := queue.Resolve()
if err != nil {
    return err
}

worker := queue.NewWorker(manager)
return worker.Work(ctx, queue.WorkerOptions{
    Connection: "redis",
    Queues:     []string{"high", "default"},
    Tries:      3,
    Backoff:    []time.Duration{5 * time.Second, 30 * time.Second},
    Timeout:    time.Minute,
})

队列优先级 ​

Worker 按配置顺序消费队列。--queue=high,default 表示优先消费 high 队列,high 为空时才消费 default。

Worker 多队列消费时先按配置顺序对完整队列组执行一次非阻塞 Pop,避免空 high queue 的 block_for 延迟 low queue 已 ready 的任务;只有所有队列都空时,才对同一组队列执行阻塞等待。

Worker 与部署 ​

部署新代码后,需要重启 Worker 使其加载最新代码:

bash
go run ./ queue:restart

Worker 会在当前任务执行完毕后自动退出,Supervisor 或进程管理器会自动拉起新 Worker。

queue:restart 信号通过 RestartStore 保存,Worker 会在任务之间检查该时间戳,并忽略早于 Worker 启动时间的旧信号。

任务过期与超时 ​

配置说明
retry_afterRedis 保留任务超时后重新可见的秒数(默认 90 秒)
Timeout()单次任务执行超时,Worker 会给 Handle 传入带 deadline 的 context
FailOnTimeout()超时后直接失败而不是重试

注意:Handle 内部应该尊重 ctx.Done()。如果任务忽略 context,即使 Worker 已经判定超时,内部耗时逻辑也可能继续运行到自然结束。

处理失败任务 ​

清理失败任务 ​

bash
# 查看失败任务列表
go run ./ queue:failed

# 删除单条失败记录
go run ./ queue:forget failed-id

# 清空所有失败记录
go run ./ queue:flush

重试失败任务 ​

bash
# 重试单条失败任务
go run ./ queue:retry failed-id

失败任务保存完整 envelope,只要 Job 类型仍已注册,就可以通过 queue:retry 重新入队。

失败任务事件 ​

任务最终失败时会触发 queue.job_failed 事件,并调用 Job 的 Failed(ctx, err) 方法(如果实现了 FailedProvider)。

go
func (j *SyncOrderJob) Failed(ctx context.Context, err error) {
    logger.WithFields(map[string]any{
        "order_id": j.OrderID,
    }).WithError(err).Error("sync order job failed finally")
}

生命周期事件 ​

事件名结构体说明
queue.job_queuedJobQueued任务已进入队列
queue.job_processingJobProcessingWorker 即将执行
queue.job_processedJobProcessed执行成功
queue.job_releasedJobReleased失败后释放回队列等待重试
queue.job_failedJobFailed最终失败并写入失败任务
queue.batch_createdBatchEvent批次创建
queue.batch_updatedBatchEvent批次进度更新
queue.batch_cancelledBatchEvent批次取消
queue.batch_finishedBatchEvent批次完成
queue.poison_envelopePoisonEnvelope消息体无法解码为 Envelope
queue.release_republish_failedInfrastructureEventRabbitMQ release 替换发布失败

基础设施事件:

事件名说明
queue.connection_connecting连接正在建立
queue.connection_connected连接已建立
queue.connection_disconnected连接已断开
queue.connection_reconnecting正在重连
queue.connection_reconnected重连成功
queue.connection_reconnect_failed重连失败
queue.topology_declared拓扑声明成功
queue.topology_declare_failed拓扑声明失败
queue.consumer_started消费者已启动
queue.consumer_stopped消费者已停止
queue.consumer_stop_failed消费者停止失败
queue.publish_failed发布失败

监听事件:

go
queue.UseEventSink(func(ctx context.Context, ev queue.Event) {
    switch e := ev.(type) {
    case queue.JobFailed:
        logger.WithFields(map[string]any{
            "job": e.JobName,
            "id":  e.ID,
        }).WithError(errors.New(e.Error)).Warn("queue job failed")
    case queue.JobQueued:
        logger.Info("job queued", "id", e.JobID, "queue", e.Queue)
    }
})

加密 Payload ​

加密使用 AES-GCM,密钥来自 app.key 配置。NewPayloadCipher(secret) 支持 base64: 前缀,与 Laravel 密钥格式兼容。

go
// Job 级声明
func (j *SendWelcomeMailJob) ShouldEncrypt() bool { return true }

// 投递级声明
_, err := queue.Dispatch(ctx, &jobs.SendWelcomeMailJob{UserID: 1001}, queue.Encrypt())

注意:如果 app.key 为空,加密声明的 Job 将在运行期失败。启动时会在控制台输出警告。

自定义驱动 ​

如果需要接入数据库队列或业务自研队列,实现 contracts/queue 包中的三个核心接口并注册为自定义 driver 即可。

需实现的接口 ​

Connector — 连接工厂 ​

Connector 负责根据连接名和配置创建 Queue 实例。Manager 在首次解析连接时调用 Connect,后续复用同一实例。

go
type Connector interface {
    Connect(ctx context.Context, name string, config map[string]any) (Queue, error)
}
参数说明
name连接配置名,如 "database"
config连接配置副本,原始字段保存在 config["_spec"].(ConnectionConfig).Options 中

Queue — 队列传输连接 ​

Queue 是队列 transport 的核心接口,定义了消息的入队、出队和生命周期操作。

go
type Queue interface {
    // Push 将已编码 payload 推入指定队列
    Push(ctx context.Context, queue string, body Payload) error

    // Later 将已编码 payload 延迟推入指定队列
    Later(ctx context.Context, queue string, body Payload, delay time.Duration) error

    // Bulk 批量推入已编码 payload,返回 transport 已接收数量
    Bulk(ctx context.Context, queue string, bodies []Payload) (BulkResult, error)

    // Pop 从一个或多个队列中拉取一个 reserved job;无任务时返回 ErrEmpty
    Pop(ctx context.Context, queues []string, wait ...PopWaitMode) (ReservedJob, error)

    // Size 返回指定队列待处理任务数
    Size(ctx context.Context, queue string) (int64, error)

    // Clear 清空指定队列
    Clear(ctx context.Context, queue string) error

    // Close 释放连接资源
    Close() error
}
方法用途说明
Push立即入队body 是已编码的 envelope 字节,driver 不需要理解其内部结构
Later延迟入队delay 为 0 时等同于 Push;不支持延迟的 driver 可返回 ErrUnsupportedOperation
Bulk批量入队返回 BulkResult{Accepted} 表示 transport 已接收数量;不支持时可逐条调用 Push
Pop拉取任务queues 按优先级排列;wait 省略时默认 PopWaitAvailable,传入 PopNoWait 为非阻塞
Size队列长度返回待处理任务数;不支持时返回 0, nil
Clear清空队列删除指定队列中所有待处理消息
Close关闭连接释放底层资源(数据库连接、客户端等)

ReservedJob — 已保留任务 ​

ReservedJob 表示 Worker 已从队列中取出的任务,Worker 通过该接口确认或释放任务。

go
type ReservedJob interface {
    // ID 返回任务 ID
    ID() string

    // Name 返回任务类型名
    Name() string

    // Payload 返回原始已编码 payload 字节
    Payload() Payload

    // Attempts 返回当前任务已尝试次数
    Attempts() int

    // Delete 确认任务完成并从队列中删除
    Delete(ctx context.Context) error

    // Release 将任务释放回队列,delay 指定下次可执行前的等待时间
    Release(ctx context.Context, delay time.Duration) error
}
方法用途说明
ID任务标识来自 envelope 中的 UUID
Name类型名用于 Registry 反序列化
Payload原始字节Worker 会用 codec 解码为 envelope,再恢复为 Job
Attempts已尝试次数Worker 用于判断是否超过 Tries
Delete确认完成任务成功执行后调用
Release释放重试任务失败但可重试时调用,delay 控制退避时间

可选接口 ​

接口方法用途
PopSessionProviderNewPopSession() Queue为每个 Worker 创建独立的队列 view,用于安全关闭生命周期资源
ConsumerIntentLeaserAcquireConsumerIntent(queues) (func() error, error)push-consumer 驱动(如 RabbitMQ)的 Worker 生命周期管理

完整示例:内存队列驱动 ​

以下示例展示一个最简的自定义驱动实现:

go
package memoryqueue

import (
    "context"
    "fmt"
    "sync"
    "time"

    queuecontract "github.com/prismgo/framework/contracts/queue"
    "github.com/prismgo/framework/queue"
)

// --- Connector ---

type MemoryConnector struct{}

func (c MemoryConnector) Connect(_ context.Context, name string, config map[string]any) (queuecontract.Queue, error) {
    // 从 config["_spec"] 读取连接配置
    spec, ok := config["_spec"].(queue.ConnectionConfig)
    if !ok {
        return nil, fmt.Errorf("memory queue: missing connection config for %q", name)
    }
    return NewMemoryQueue(spec.Queue), nil
}

// --- Queue ---

type MemoryQueue struct {
    mu    sync.Mutex
    name  string
    items []item
}

type item struct {
    queue string
    body  queuecontract.Payload
}

func NewMemoryQueue(name string) *MemoryQueue {
    return &MemoryQueue{name: name}
}

func (q *MemoryQueue) Push(_ context.Context, queue string, body queuecontract.Payload) error {
    q.mu.Lock()
    defer q.mu.Unlock()
    q.items = append(q.items, item{queue: queue, body: body})
    return nil
}

func (q *MemoryQueue) Later(ctx context.Context, queue string, body queuecontract.Payload, delay time.Duration) error {
    // 简化实现:忽略 delay,直接入队。生产驱动应使用定时器或延迟队列。
    return q.Push(ctx, queue, body)
}

func (q *MemoryQueue) Bulk(ctx context.Context, queue string, bodies []queuecontract.Payload) (queuecontract.BulkResult, error) {
    for _, body := range bodies {
        if err := q.Push(ctx, queue, body); err != nil {
            return queuecontract.BulkResult{}, err
        }
    }
    return queuecontract.BulkResult{Accepted: len(bodies)}, nil
}

func (q *MemoryQueue) Pop(_ context.Context, queues []string, _ ...queuecontract.PopWaitMode) (queuecontract.ReservedJob, error) {
    q.mu.Lock()
    defer q.mu.Unlock()
    for _, queueName := range queues {
        for i, it := range q.items {
            if it.queue == queueName {
                q.items = append(q.items[:i], q.items[i+1:]...)
                return &memoryReservedJob{id: "job-1", name: "test", body: it.body, attempts: 1}, nil
            }
        }
    }
    return nil, queue.ErrEmpty
}

func (q *MemoryQueue) Size(_ context.Context, _ string) (int64, error) {
    q.mu.Lock()
    defer q.mu.Unlock()
    return int64(len(q.items)), nil
}

func (q *MemoryQueue) Clear(_ context.Context, _ string) error {
    q.mu.Lock()
    q.items = nil
    q.mu.Unlock()
    return nil
}

func (q *MemoryQueue) Close() error { return nil }

// --- ReservedJob ---

type memoryReservedJob struct {
    id        string
    name      string
    body      queuecontract.Payload
    attempts  int
}

func (j *memoryReservedJob) ID() string                          { return j.id }
func (j *memoryReservedJob) Name() string                        { return j.name }
func (j *memoryReservedJob) Payload() queuecontract.Payload      { return j.body }
func (j *memoryReservedJob) Attempts() int                       { return j.attempts }
func (j *memoryReservedJob) Delete(_ context.Context) error      { return nil }
func (j *memoryReservedJob) Release(_ context.Context, _ time.Duration) error {
    // 简化实现:直接重新入队。生产驱动应将任务放回队列并设置延迟。
    return nil
}

// --- 注册 ---

func init() {
    queue.Extend("memory-custom", MemoryConnector{})
}

注册与配置 ​

queue.Extend 可以在业务包 init() 中调用,不要求当前 Application 容器或 Queue Manager 已经创建。Manager 在首次解析连接时读取该注册表。

go
// 在业务包 init() 中注册
func init() {
    queue.Extend("memory-custom", MemoryConnector{})
}

在配置中新增连接:

go
// config/queue.go
"connections": map[string]interface{}{
    "memory-custom": map[string]interface{}{
        "driver": "memory-custom",
        "queue":  "default",
    },
},

Connector 的 Connect(ctx, name, config) 会收到连接名和配置副本,配置中的原始字段保存在 config["_spec"].(ConnectionConfig).Options 中,便于自定义 driver 自行解析扩展参数:

go
func (c MyConnector) Connect(_ context.Context, name string, config map[string]any) (queuecontract.Queue, error) {
    spec, _ := config["_spec"].(queue.ConnectionConfig)
    // 从 spec.Options 读取 driver 私有配置
    tableName, _ := spec.Options["table"].(string)
    dbConnection, _ := spec.Options["connection"].(string)
    // ...
}

错误常量 ​

错误常量说明
ErrEmpty当前队列没有可消费任务
ErrJobNotRegisteredJob 类型未在注册表中注册
ErrDuplicate唯一任务锁已存在,本次投递被跳过
ErrSkipped任务被 middleware 主动跳过
ErrBatchCancelled任务所属批次已取消
ErrManagerClosed队列管理器已关闭
ErrConnectionClosed连接已关闭
ErrUnsupportedOperation当前 driver 不支持该操作
ErrPoisonEnvelope消息体无法解码为 Envelope
ErrUnsupportedRetryAfterRabbitMQ 不支持 retry_after
ErrRabbitMQDialFailedRabbitMQ 初始建连失败
ErrRabbitMQTopologyMissingdeclare=false 时 exchange 或 queue 不存在
ErrRabbitMQPublishNackedBroker 拒绝了 publisher confirm
ErrRabbitMQPublishTimeout等待 publisher confirm 超时
ErrRabbitMQPublishConfirmClosedConfirm 通道已关闭
ErrRabbitMQPublishUnroutedMandatory 发布未路由到任何队列
ErrRabbitMQReleaseRepublishFailedRelease 替换发布失败

连接能力矩阵 ​

能力syncredisrabbitmq
异步投递否(同步执行)是是
延迟投递否是是(plugin / ttl_dlx)
批量投递否是是
阻塞等待否是(block_for)是(push consumer)
retry_after否是否(返回 ErrUnsupportedRetryAfter)
Publisher confirm——是(RABBITMQ_CONFIRM)
自动重连——是(有界退避)
Poison envelope—是是(Reject(false))
Size—是是(ready count only)
Clear—是是(purge ready only)

高级功能与连接的关系:

高级功能依赖
唯一任务github.com/prismgo/framework/cache(不依赖队列连接)
防抖任务github.com/prismgo/framework/cache(不依赖队列连接)
WithoutOverlappinggithub.com/prismgo/framework/cache 锁(不依赖队列连接)
RateLimitgithub.com/prismgo/framework/cache(不依赖队列连接)
ThrottlesExceptionsgithub.com/prismgo/framework/cache(不依赖队列连接)
失败任务命令FailedStore
queue:restartRestartStore
批量任务BatchStore

Laravel Queue 映射 ​

Laravel 方法/概念PrismGo 等价
Job::handle()Job.Handle(ctx context.Context) error
dispatch(new Job)queue.Dispatch(ctx, &Job{})
Job::dispatch($job)->delay(...)queue.Dispatch(ctx, job, queue.Delay(...))
Job::$connectionConnectionProvider.QueueConnection()
Job::$queueQueueProvider.QueueName()
Job::$triesTriesProvider.Tries()
Job::$timeoutTimeoutProvider.Timeout()
Job::$backoffBackoffProvider.Backoff()
Job::retryUntil()RetryUntilProvider.RetryUntil()
Job::$maxExceptionsMaxExceptionsProvider.MaxExceptions()
Job::$failOnTimeoutFailOnTimeoutProvider.FailOnTimeout()
ShouldBeEncryptedEncryptedProvider.ShouldEncrypt()
Job::uniqueId()UniqueIDProvider.UniqueID()
Job::$uniqueForUniqueForProvider.UniqueFor()
Job::uniqueVia()UniqueViaProvider.UniqueVia()
Job::middleware()MiddlewareProvider.Middleware()
Job::failed()FailedProvider.Failed(ctx, err)
Bus::chain([...])->dispatch()queue.Chain(...).Dispatch(ctx)
Bus::batch([...])->dispatch()queue.Batch(...).Dispatch(ctx)
Queue::pushOn('queue', job)queue.Dispatch(ctx, job, queue.OnQueue("queue"))
Queue::later(60, job)queue.Later(ctx, 60, job)
php artisan queue:workgo run ./ queue
php artisan queue:restartgo run ./ queue:restart
php artisan queue:failedgo run ./ queue:failed
php artisan queue:retry {id}go run ./ queue:retry {id}
php artisan queue:forget {id}go run ./ queue:forget {id}
php artisan queue:flushgo run ./ queue:flush
WithoutOverlappingqueue.WithoutOverlapping(key)
ThrottlesExceptionsqueue.ThrottlesExceptions(max, decay)
SkipIfqueue.SkipIf(predicate)
$job->release(delay)queue.ReleaseAfter(delay, err)
$job->fail()queue.Fail(err)