Uzair Aslam:生产环境中的 Vercel Cron Jobs:用 Next.js 构建安全、可安全重试的任务

Uzair Aslam:生产环境中的 Vercel Cron Jobs:用 Next.js 构建安全、可安全重试的任务

💡 原文英文,约2100词,阅读约需8分钟。
📝

内容提要

文章介绍如何用 Next.js 与 PostgreSQL 在 Vercel 上构建可靠的定时报表任务:将定时触发仅视为唤醒信号,以 UTC 报告日为业务键,用单条 SQL 原子地锁定游标、补录最多七天缺失任务并推进游标;处理时用 FOR UPDATE SKIP LOCKED 认领任务,在同一事务内写入报表并标记完成,从而支持崩溃恢复。外部副作用通过 outbox 与幂等键处理,同时需校验 CRON_SECRET、限制执行时长,并测试重复、漏跑与中断场景。

🔎

延伸解读

定时触发只是信号,业务状态必须落库

文章强调 Vercel Cron 的调用可能延迟、重复或缺失,且失败后不会自动重试。因此不能把“收到请求”当作任务已执行,而应在 PostgreSQL 中记录每个报告日的排队与完成状态。这样即使某次唤醒丢失,后续调用也能根据游标补录缺失日期,避免漏跑或重复处理同一天。

单条 SQL 原子补录,避免并发竞争

补录逻辑用一条 SQL 同时锁定调度行、生成最多七天缺失任务并推进游标。由于 PostgreSQL 保证语句原子性,失败时游标和队列都不变,成功时任务已持久化。第二个并发调用会等待锁释放后看到新游标,不会重复入队。七天上限则把单次执行控制在可预期范围内。

事务内认领并完成,崩溃后可恢复

处理任务时用 FOR UPDATE SKIP LOCKED 认领一个待处理日,并在同一事务内写入报表、标记完成。若进程在提交前崩溃,锁释放后该任务仍为待处理,下次可重新认领。这种设计适合纯数据库报表;若涉及发邮件等外部副作用,则需改用 outbox 与幂等键,避免事务长时间持有。

安全与可观测性不可省略

路由需校验 CRON_SECRET,缺失时直接失败,且不记录该请求头。执行时长应低于函数上限,数据库语句超时也要留有余量。文章还建议记录脱敏的错误类别与耗时,并基于“最后完成日落后于截止时间”或“游标停止推进”设置告警,因为触发成功不等于报表已产出。

❓

Q&A

Vercel Cron Jobs 触发失败后会自动重试吗?

不会。Vercel 不会重试失败的 cron 调用,因此需要将定时触发仅视为唤醒信号,并在数据库中记录哪些天到期、哪些已完成,通过补偿逻辑处理漏跑。

如何防止 Vercel Cron 端点被未授权访问?

设置随机 CRON_SECRET 环境变量,Vercel 会以 Bearer token 发送。在路由中校验 Authorization 头,使用 timingSafeEqual 比较摘要,缺失变量时返回 503,错误凭证返回 401,且不要记录该头。

在 PostgreSQL 中如何原子地补录漏掉的报表任务并推进游标?

使用单条 SQL 语句:先锁定 report_schedule 行,计算截止日期(当前 UTC 时间减2小时宽限),生成最多7个缺失日期,插入 daily_report_job 并忽略冲突,然后更新游标到最大日期加1。整个操作原子完成,失败则回滚。

处理报表任务时如何避免多个 worker 竞争同一任务?

使用 SELECT ... FOR UPDATE SKIP LOCKED 认领一个待处理任务,其他 worker 会跳过已锁定的行,从而并行处理不同日期。在同一事务内写入报表并标记完成,崩溃时锁释放,任务保持待处理。

报表任务涉及发送邮件等外部副作用时应该怎么处理?

不要在数据库事务中调用外部服务。应在同一事务中写入 outbox 记录,然后由单独的 worker 认领并投递。使用稳定的幂等键(如日报+日期+收件人ID)并记录结果,若响应丢失需先对账再重试。

如何测试 cron 任务的重复、漏跑和中断场景?

在测试数据库中:用不同时间戳调用入队查询,验证补录和去重;在事务中执行处理语句后回滚,确认未持久化;模拟两个连接测试 SKIP LOCKED;用 curl 测试认证和重放。

🏷️

标签

➡️

继续阅读