如何在Web应用中运行后台任务:队列、调度器与Cron
要在Web应用中运行后台任务,首先要把工作分成两类。触发式任务(发送欢迎邮件、处理上传文件)可以保存到任务队列中,通常是数据库中的一张表,并由带有重试机制的工作进程处理。定时任务(每晚生成报告、每15分钟发送提醒)则需要按时间表触发:可以使用主机的cron功能,或由外部调度器调用你应用中受保护的端点。务必让任务可以安全地重复运行,记录失败情况,并且永远不要依赖一个可能休眠或重启的服务器内部的计时器。
本指南介绍了各种选项、每种选项的适用场景,以及如何借助AI应用构建器进行设置,包括在支持缩容至零的主机上的做法。
什么算作后台任务
任何缓慢、不可靠或对用户即时响应并非必需的工作:
- 发送邮件、通知和收据
- 调整图片大小、转码视频、生成PDF
- 调用缓慢的第三方API,包括用于长时间任务的AI模型
- 从其他系统导入或同步数据
- 每晚的清理、报告和提醒
如果把这些工作放在请求过程中同步完成,页面会变慢且脆弱:如果邮件服务商宕机,用户的注册就会失败。将其移到后台,用户可以立即得到响应,而任务也可以重试。
可选方案
| 方案 | 工作方式 | 优点 | 缺点 |
|---|---|---|---|
| 同一进程中的“即发即忘” | 先响应,然后在服务器中继续执行工作 | 无需设置 | 服务器停止时会丢失;没有重试 |
进程内计时器(setInterval、setTimeout) | 在运行中的服务器中设置计时器 | 非常简单 | 服务器休眠时不运行;多个副本时会重复执行 |
| 数据库支持的队列 | 任务以行的形式保存;工作进程领取并执行它们 | 使用你已有的数据库;持久可靠;便于检查 | 需要自己实现重试和加锁;需要有东西来运行工作进程 |
| 专用队列(基于Redis或托管式) | 队列服务加上工作进程 | 成熟的重试和调度功能 | 另一项需要运行和付费的服务 |
| 托管的任务或工作流服务 | 第三方服务带着重试和调度调用你的端点 | 调度和重试由服务商处理 | 依赖供应商;需要另一个账户和密钥 |
| 外部调度器调用端点 | 其他地方的计时器按时发送HTTP请求 | 适用于任何主机,包括缩容至零的主机 | 存在最小间隔和延迟;需要保护端点 |
| 主机内置的cron | 平台按计划运行命令 | 在支持的地方很简单 | 并非每个主机都提供 |
没有唯一正确的答案。一个小型应用通常只需要一个数据库队列加上一个外部调度器。
缩容至零的主机如何改变了情况
许多现代主机在没有人使用应用时让它休眠,并在下一次请求时将其唤醒。这样可以节省费用,但休眠的服务器不运行任何代码:计时器不会触发,工作进程也不会轮询。在这些主机上,定时工作必须由一个传入的请求来启动,而最简单的请求来源就是外部调度器。每次调用都会唤醒应用(通常还包括其数据库),因此每分钟调用一次的成本高于每15分钟调用一次。应选择满足需求的最慢调度频率。
方案一:数据库任务队列
适用于触发式任务:
- 创建一张
jobs表:类型、载荷(JSON)、状态(pending、running、done、failed)、尝试次数、run_after、last_error、时间戳。 - 当某件事发生时,在引起该变化的同一事务中插入一条任务记录,使两者同时成功或同时失败。
- 工作进程领取到期的任务,将其标记为
running,执行工作,然后标记为done。 - 失败时,增加尝试次数,将
run_after设置到更晚的时间(例如1、5、30分钟后),并保存错误信息。达到最大次数后,将其标记为failed并通知相关人员。
如果可能有多个工作进程同时运行,任务必须被安全地领取。在Postgres上,SELECT ... FOR UPDATE SKIP LOCKED可以让每个工作进程领取不同的行而不会发生冲突。
什么来运行工作进程?在始终运行的服务器上,可以是一个独立进程中的循环。在缩容至零的主机上,方案二中的定时端点可以在每次调用时处理一批到期的任务。
方案二:外部调度器调用你的应用
适用于定时任务:
- 添加一个端点,例如
POST /api/cron/send-reminders。 - 保护它:要求在请求头中携带一个密钥令牌,该令牌在双方都作为密钥保存,并拒绝其他任何请求。
- 让它具备幂等性:在同一分钟内运行两次不能发送两条提醒。记录已经发送过的内容。
- 让每次调用都简短:处理一批(例如100条),由下一次调用处理剩余的内容。
- 将调度器指向该端点。
调度器的选择:
- GitHub Actions定时工作流。 在GitHub的使用限制内免费。截至2026年9月,GitHub的文档说明,最短间隔为每5分钟一次;运行可能会因高峰时段(尤其是每个整点)而延迟;调度默认使用UTC时间;如果公共仓库在60天内没有任何仓库活动,其定时工作流将被禁用。
- 托管的cron服务。 有许多服务的唯一用途就是按计划调用某个URL。在依赖它之前,请检查其可靠性、最小间隔和告警功能。
- 云服务商的调度器。 如果你已经在使用大型云服务商,它会提供可以调用HTTPS端点的托管调度器。
- 托管的任务或工作流服务。 这些服务结合了调度、队列和重试,并在每个步骤中调用你的端点。请在各供应商的网站上比较其价格和限制。
方案三:交给服务商处理
有时最好的后台任务是根本不需要自己做:
- 付款。 Stripe会重试失败的订阅付款并发送Webhook;你只需对此做出响应,而无需轮询。参阅什么是Webhook。
- 邮件。 当你把邮件交给服务商后,服务商会对投递进行排队和重试。
- 批量AI工作。 OpenAI和Anthropic为非紧急请求提供批量API,你提交请求后再稍后收取结果。
提交给AI构建器的提示词
对于队列:
使用我们数据库中的jobs表(类型、JSON载荷、状态、尝试次数、run_after、last_error)添加一个后台任务队列。当用户注册时,在同一事务中插入一条send_welcome_email任务,而不是同步发送。添加一个processJobs(limit)函数,使用SELECT ... FOR UPDATE SKIP LOCKED领取到期的任务并执行它们,失败后在1、5和30分钟后重试,并在4次尝试后将其标记为failed。
对于定时触发:
添加一个由密钥CRON_SECRET中的bearer令牌保护的POST /api/cron/tick;拒绝任何不带该令牌的请求。每次调用运行processJobs(100),并发送未来24小时内到期且尚未发送的预订提醒,记录已发送的提醒,确保不会重复发送。以JSON摘要的形式返回运行结果。然后编写一个GitHub Actions工作流,每15分钟调用一次该端点,并从仓库密钥中读取URL和令牌。
常见误区
- 依赖
setInterval。 当服务器休眠或重启时,它会悄无声息地停止;当两个副本同时运行时,它又会重复执行。 - 未受保护的cron端点。 任何发现它的人都可以触发你的任务,或者让你的邮件账单增加。
- 任务不具备幂等性。 重试和重复的调度器调用一定会发生。设计时要考虑到这一点。
- 没有故障可见性。 一个悄无声息地失败了一周的任务,比明显地失败更糟。记录错误,并在反复失败时发出告警。
- 在单个请求中进行长时间运行。 请求会超时。应分批处理。
- 调度过于频繁。 在缩容至零的主机上,每次调用都会唤醒并计费应用。应使调度间隔与实际需求相匹配。
- 时区。 “早上9点”对调度器(通常是UTC)和你的用户而言含义不同。应以UTC存储时间,并为每个用户进行转换。
- 工作流文件中的密钥。 应把令牌保存在调度器的密钥存储中。参阅如何安全地保管API密钥。
检查清单
- 已将较慢的工作移出用户请求
- 任务持久保存,而不只是存在内存中
- 重试采用递增的延迟,并设有最大次数
- 每个任务都可以安全地重复运行
- 定时端点受密钥令牌保护
- 每次运行处理的批量大小有上限
- 失败的任务可见,并设有告警
- 已结合托管成本选择了调度间隔
- 时间以UTC存储
Mythex中的后台任务
Mythex的文档没有描述定时任务或cron任务,而且已发布的应用及其数据库在闲置时会休眠,并在下一次请求时唤醒(参阅托管限制)。因此,不要依赖应用内部的计时器。行之有效的做法是上面所述的方法:请智能体为你添加一张任务表和一个受令牌保护的端点,然后将外部调度器指向你已发布的URL。将令牌保存为项目密钥,并且要注意,每次唤醒都会按应用和数据库保持唤醒的时间消耗积分,因此请选择合理的间隔。如果你的应用需要为此使用API后端,REST API后端教程是一个起点,而如何构建REST API则涵盖了端点的设计。如果你的产品核心是常驻的工作进程或机器人,那么专为持续运行进程设计的主机可能更适合你。
常见问题
什么是后台任务?
后台任务是应用在用户等待的请求之外完成的工作,例如发送邮件、调整图片大小、生成报告或同步数据。用户能够快速得到响应,任务则单独运行,失败时还可以重试。
如果我的主机没有cron,如何运行定时任务?
添加一个受保护的端点来完成工作,然后让外部调度器按时间表调用它。可选方案包括定时运行的GitHub Actions工作流、托管的cron服务,或云服务商的调度器。使用密钥令牌保护该端点。
我可以用setInterval或setTimeout来运行定时任务吗?
只适用于在始终运行的服务器上执行的小型、不重要的任务。如果服务器重启、缩容至零或运行多个副本,计时器就会丢失,或者被执行多次。应把定时工作存储在数据库中,或使用调度器。
Mythex支持cron任务吗?
Mythex的文档没有描述定时任务或cron任务,而且已发布的应用在闲置时会休眠。最可靠的做法是让外部调度器调用你应用中受保护的端点,这样会唤醒应用并执行工作。