·更新于 2026年10月27日· 1 次浏览 Node.jsMongoDBRedis工程实践BUG解决

Node.js 定时发布总差一小时:我用 UTC 存储、时区转换和任务校准修复排期

博客定时发布不是提前就是延后一小时,最后发现是前端本地时间、服务器时区和扫描任务边界混在一起。这里记录我用 UTC 存储、展示层转换、幂等扫描和漏发补偿稳住排期的做法。

Node.js 定时发布总差一小时:我用 UTC 存储、时区转换和任务校准修复排期 封面
ZHOUYI

作者

ZHOUYI

前阵子博客后台定时发布总出怪事:约好 22:00 上线,有时 21:00 就发出去,有时又晚一小时。第一反应以为是 cron 配错,后来发现是时间在不同地方各说各话:前端按本地时间选,MongoDB 存了 Date,服务器又是另一个时区,扫描任务还拿当前时间直接比较。

存储统一用 UTC,展示再转时区

我把规则改成:用户选择的是墙钟时间,提交时必须带上时区或明确换算成带偏移的时间字符串,服务端统一转成 UTC 存入 publishAt。后台列表和小程序展示时,再按访问者时区或站点默认时区格式化。这样数据库里只有一个绝对时间,不会因为容器时区变化而漂移。

js
const publishAt = new Date('2026-06-20T22:00:00+08:00');
await Post.create({ title, status: 'scheduled', publishAt });

const show = new Intl.DateTimeFormat('zh-CN', {
  timeZone: 'Asia/Shanghai', dateStyle: 'short', timeStyle: 'short'
}).format(publishAt);

扫描任务别只信 cron,要看边界

定时扫描也不能只依赖 cron 准点。服务重启、任务延迟、实例重复,都可能让某一分钟被跳过或重复执行。我把扫描逻辑改成到期即发:只要状态还是 scheduled,并且 publishAt 小于等于当前时间,就进入处理。这样即使晚跑几分钟,也能把到期文章补发出来。

  1. 查询条件用 publishAt <= now 且 status = 'scheduled',避免错过整分钟边界。
  2. 发布动作加文章 ID 幂等锁,防止多实例同时扫到同一篇。
  3. Redis 记录 lastScanAt,服务重启后从一个短窗口补扫。
  4. 失败任务进入重试队列,超过阈值就告警,不静默吞掉。

一段扫描补偿代码

js
const now = new Date();
const due = await Post.find({ status: 'scheduled', publishAt: { $lte: now } }).limit(100);
for (const post of due) {
  const ok = await redis.set(`publish:lock:${post._id}`, '1', 'EX', 60, 'NX');
  if (!ok) continue;
  await publishPost(post);
  await Post.updateOne({ _id: post._id }, { $set: { status: 'published', publishedAt: now } });
}
await redis.set('publish:lastScan', now.toISOString());

“定时发布看似只是时间比较,实际是存储、展示、调度和补偿四层一致性问题。先把 UTC 定死,再把扫描做成到期补偿,比到处修补 cron 更稳。”

修完后我还补了一个小面板:显示文章原始提交时区、UTC 时间、预计发布时间和实际发布时间。这样下次再有人说时间不对,至少能一眼看出是输入问题、展示问题,还是任务真的漏跑了。

Guestbook

留言

若这篇文章也曾停在你心上,留一行字吧,我会看的。

先确认一下邮箱

多这一步,是为了少些机器骚扰,也让我能更踏实地读每一条真人写下的字。验证码 10 分钟内有效;同一邮箱 60 秒内只能发一封。

本文留言

共 0 条已加载 0 条

这里还静悄悄的——愿意做第一个留下脚印的人吗?