后台任务队列选型

后台任务队列选型

# 后台任务队列选型

结论

用高吞吐队列框架(BullMQ + 托管 Redis)跑每天几百个低频任务,是架构与成本模型不匹配 —— 过度设计。 选型的尺度是任务频率,不是框架的先进性:低频 + 要可靠 → 一张数据库任务表;高频 + 要吞吐 → 专用队列。

一、先校准口径:计费单位不是任务数

托管 Redis 统计的是底层 Redis command 次数,不是业务任务数,也不是存储量。

「才 192 个 RSS 任务,500K 怎么一下就没了」—— 两个量纲根本不同:

  • BullMQ 一次任务 = 入队 + 置状态 + 领取 + 加锁 + 续期 + 完成 + 事件记录等多条命令
  • 空队列每 5 秒重新阻塞读取:3 个消费者 ≈ 51,840 次/天
  • 每 30 秒的卡死检查:3 个消费者 ≈ 8,640 次/天
  • 存储 95 KB / 256 MB 完全说明不了调用量

所以「用量突然见底」不等于「任务变多了」。先问分子分母(见 口径先于数值)。

二、为什么可靠队列既有「超时」又有「持续检查」

BullMQ 同时用了两套机制,不是它设计啰嗦:

  1. Worker 领到任务后创建一把带 TTL 的锁(如 30 秒)
  2. Worker 活着就不断续期
  3. Worker 崩溃 → 锁自动超时消失
  4. 但锁消失不会自动把任务从「处理中」改回「等待中」
  5. 所以还需要一个存活的观察者定期找「仍在处理中但锁已不存在」的任务,重新入队

为什么不能只靠超时:Node 的 setTimeout 随进程崩溃一起消失;Redis TTL 只能删锁, 做不了「改状态 + 重新入队」这种多步迁移;Redis 过期事件通知不是可靠消息,断线会丢。

结论:问题不在于「存在卡死检查」,而在于这套机制按高吞吐假设配置了检查频率,又被多份消费者放大。

三、替代方案:一张数据库任务表

background_jobs
  id / type / payload / status / run_at
  locked_until / attempts / last_error
  created_at / finished_at
  • Worker 每 30 秒一次数据库查询,领取所有类型的待执行任务(≈ 2,880 次查询/天)
  • 领取时写 locked_until(如 5 分钟);正常完成标记成功
  • 每 5 分钟统一回收已超过 locked_until 的任务
  • 只有真正可能跑超 5 分钟的任务才续锁
  • 失败最多重试 3 次并记录错误
  • 空闲时可逐步拉长到 60 秒,有任务时缩短

它保留了超时、锁、失败重试、崩溃恢复,但不再为低频业务维持高频轮询。 适用边界示例(RSS 抓取 + AI 总结共表共 Worker),未来 Agent 自动化继续复用同一张表。

四、两个常见放大器

  1. 同一调度职责有多个所有者:Web runtime 与常驻 Worker 都启动 Feed scheduler, 同一批订阅被重复检查。生产应默认只留 Worker,Web 侧要显式 opt-in(FEED_REFRESH_SCHEDULER=on), 并把它加进全局环境变量白名单。
  2. 每次入队重建连接:快捷入队函数各自 createQueue 会泄漏 producer 连接, 表现为连接数异常(重启后 42 → 6)。所有入队入口要复用同一个 queue helper。

五、「不要 Redis」的真正含义

不是 Redis 无用,而是当前规模下没有匹配它的职责:任务改走数据库表 + Cron 后,BullMQ 不再需要它;页面加速由浏览器缓存与路由优化负责。不要为了已经不存在的问题继续保留一套基础设施。

两个容易被顺带误判的点:

  • 把用户凭证放进 Redis 不会更快:JWT 会话本来就是「浏览器存签名 Cookie → 服务端验签名」,不需要每次查 Session;再复制进 Redis 等于多一次网络请求,还增加串用户与泄露风险。
  • Redis 只能缩短「API 查数据」这一小段。用户返回已访问页面时,浏览器缓存是 0 次网络请求,而 Redis 仍要经过 浏览器 → Vercel → Upstash 一跳,反而更慢。私有且频繁修改的内容不适合放服务端缓存。

未来真正需要 Redis 的场景:限流、验证码、公共热点内容缓存、多服务器锁。

六、执行形态:Cron 与常驻 Worker 怎么分

判据不是「哪个技术更先进」,而是这件事能不能等:

  • 能等的(批量、可延迟、可重试):剪藏 / RSS 自动摘要、Embedding 生成、相关推荐更新、失败重试与卡死任务接管、日报周报 → PostgreSQL 任务表 + 每分钟 one-shot Cron。
  • 不能等的(用户正盯着屏幕):Sidebar 问答、润色扩写、用户确认后的即时反馈 → 必须在 Web 请求里实时调用模型。

交互式 Agent 若也排进 Cron,用户每次对话要等一分钟。所以这是两套并存的执行形态,不是「用 Cron 替换 Worker」: 工作流层 = 队列 + Cron;Agent 层 = Web 请求实时执行。

既然部署方向已经是 Cron、常驻 Worker 实际没在用,就不该为了 AI 再引入一套常驻进程,更不该连带引入 BullMQ / Redis。

AI Cron 的领取与容错(与 §三 的数据库任务表同构):每分钟一次、主机 flock 防重叠、条件更新原子领取、5 分钟 lease 允许接管卡死任务、每批限量、单任务超时隔离、最多三次指数退避、内容版本变化时旧任务标记 superseded(不允许旧任务覆盖新内容)。

命名上要区分:「每分钟启动一次、做完就退出」与「24 小时常驻监听队列」是两种运行形态,即使放在同一个 app 目录里也不该混成一个概念(见 本地优先知识产品的架构演进 §十二)。

相关