异步队列
后端异步任务队列位于 core/queue,提供与业务解耦的「点对点工作队列」,底层内置 Redis 后端。典型用途:AI 处理、流程邮件/Webhook 通知、邮箱验证码、通知邮件补推。
一、设计目标
- 统一接口:调用方只依赖
queue.Queue接口,不感知底层后端。 - 可插拔后端:内置
redis实现,新增后端只需实现Queue接口并在config.New注册。 - 无业务语义:消息体统一为
[]byte,由业务层自行序列化(JSON 等)。 - 点对点语义:同一条消息只会被一个消费者取出(多 worker 竞争消费)。广播语义请使用
core/pubsub。
二、接口定义
go
// core/queue/interface.go
type Handler func(ctx context.Context, body []byte) error
type Queue interface {
Enqueue(name string, body any) error // 投递消息
Consume(ctx context.Context, name string, handler Handler, concurrency int) error // 启动消费者
Close() error
}Enqueue:任意实例投递的任务可被任一实例的 worker 取到(适合多实例部署)。Consume:concurrency为 worker 并发数(<=0 回落为 1);ctx取消后所有 worker 优雅退出。Handler返回error仅记录日志,当前实现不自动重试(重试/死信队列需业务层自行实现)。
三、Redis 后端实现
位置:core/queue/redis.go,基于 Redis List:
- 投递:
RPUSH追加到队列尾部。 - 消费:单连接
BRPOP阻塞拉取,投入有界 channel(cap = 并发数),再由concurrency个 worker 并发处理。- 有界 channel 提供天然背压:channel 满则阻塞取消息协程,避免内存无限堆积。
- 仅占用 1 条 Redis 连接(而非
concurrency条),大幅减少连接数。 - Redis 保证同一消息只被一个 worker 取到。
- 网络抖动时短暂退避(100ms)重试,避免 CPU 空转。
ctx取消后先停止取消息,再 drain 在途任务后退出。
go
core.Queue.Enqueue("transcribe", payload) // 投递
core.Queue.Consume(ctx, "transcribe", handler, 3) // 3 并发消费四、消费者注册(app/queue)
所有消费者集中在 app/queue/start.go 的 Start(ctx) 中统一注册并启动,由 main.go 在进程启动时调用。当前已注册队列:
| 队列名 | 并发 | 处理内容 |
|---|---|---|
flow_notify_email | 3 | 流程邮件通知(投递体为 util.EmailOptions,消费时 json.Unmarshal 后 util.EmailUtil.SendEmail) |
flow_notify_webhook | 3 | 流程 Webhook 通知(投递体 flow_schema.FlowNotifyWebhookPayload,调用 FlowHistoryService.ProcessFlowWebhook) |
email:code:send(util.QueueEmailCode) | 3 | 邮箱验证码发送,投递体 util.EmailCodeTask;入队超过 60 秒则丢弃(验证码时效性) |
notice:email(notice_service.QueueNoticeEmail) | 3 | 通知邮件补推,投递体 notice_service.NoticeEmailTask;失败/成功均回写状态(2 成功 / 3 失败),不重试 |
notice:email:delay(notice_service.QueueNoticeEmailDelay) | 3 | 通知邮件延迟补推,投递体 queue_schema.NoticeEmailDelayTask(receiver_id/preview_count);由 core.QueueDelay.ConsumeDelayed 消费,延迟 EmailDelaySeconds 后合成邮件再入 notice:email |
image_webp | 3 | 图片异步转 webp,投递体 queue_schema.ImageWebpPayload(file_id),转码完成后回写文件记录,失败则标记原图 |
operate_log | 32 | 操作日志落库,投递体 queue_schema.OperateLogPayload;由中间件投递,defer 兜底写库,消费幂等 |
示例(来自 app/queue/start.go):
go
// app/queue/start.go
func Start(ctx context.Context) {
core.Queue.Consume(ctx, queue_schema.QueueFlowNotifyEmail, flowNotifyEmailHandler, 3)
core.Queue.Consume(ctx, queue_schema.QueueFlowNotifyWebhook, flowNotifyWebhookHandler, 3)
core.Queue.Consume(ctx, util.QueueEmailCode, emailCodeHandler, 3)
core.Queue.Consume(ctx, queue_schema.QueueNoticeEmail, noticeEmailHandler, 3)
core.Queue.Consume(ctx, queue_schema.QueueImageWebp, imageWebpHandler, 3)
core.Queue.Consume(ctx, queue_schema.QueueOperateLog, operateLogHandler, 3)
// 延迟队列(与即时队列独立,基于 core/queue_delay)
core.QueueDelay.ConsumeDelayed(ctx, queue_schema.QueueNoticeEmailDelay, noticeEmailDelayHandler, 3)
}五、扩展新的异步任务
- 定义 payload:在对应
schema包定义结构体(或直接复用util.EmailOptions等)。 - 投递:业务代码中
core.Queue.Enqueue("your_queue", payload)。 - 注册消费者:在
app/queue/start.go的Start中core.Queue.Consume(ctx, "your_queue", handler, N)。 - 无需改动
core/queue本身——队列不关心业务类型,仅传输字节。
六、与 pubsub 的区别
| 能力 | core/queue | core/pubsub |
|---|---|---|
| 语义 | 点对点(一条消息一个消费者) | 广播(所有订阅者都收到) |
| 用途 | 异步作业、后台任务 | 实时事件通知(如 WebSocket 跨实例推送) |
| 后端 | Redis List | Redis Pub/Sub |
详见 邮件发送 与「异步队列与核心组件」中的 pubsub 说明。