← 返回博客
语言: English 中文
Time 2026-06-05 7 分钟

秒、毫秒、2038 与时间戳背后的时区故障

Unix 纪元是 1970-01-01 00:00:00 UTC。这个日期在天文、文化、数字上都没有任何特殊意义。最早的 Unix 设计者只是挑了系统发布那年的元旦,因为这比挑别的更省事。五十五年后,这个随手挑的起点成了数字时间的全球坐标原点。

Unix 时间戳EpochISO 8601Y2K38时区

它到底是什么

Unix 时间戳是从纪元开始算的秒数,忽略闰秒。最后这一句比大多数人以为的更重要——下面会讲。

数字本身没有内置单位,它就是一个整数。这个整数代表秒、毫秒、微秒还是纳秒,是你必须带外知道的约定。没有元数据,没有标记,没有 schema。各语言 / 生态约定不同:

  • 秒(今天 10 位数): C 的 time_t、Go time.Time.Unix()、PHP time()、Python time.time()(返回浮点秒)、绝大多数 UNIX 命令行工具。
  • 毫秒(今天 13 位数): JavaScript Date.now()、Java System.currentTimeMillis()、Kotlin / Android、绝大多数返回数字的 JSON API。
  • 微秒(16 位): PostgreSQL timestamptzepoch、部分 C++ API、性能剖析输出。
  • 纳秒(19 位): Go time.Time.UnixNano()、新式内核 API。

两秒钟看穿小技巧:10 位时间戳落在 2001~2286 之间;13 位是毫秒;更长就是微秒或纳秒。如果某层把 A 当成 B,你会得到 1000 倍误差——通常表现成"这是公元 50000 年"或者"这是 1970 年"。

怎么读懂一个陌生的时间戳

如果你拿到一个不知出处的整数:

  1. 数位数。10 → 秒,13 → 毫秒,16 → 微秒,19 → 纳秒。(这条规则在 2286 年前都成立。)
  2. 转 UTC 看年份。年份明显不对,单位就猜错了。
  3. 如果是浮点数,整数部分是秒,小数部分是亚秒精度。别四舍五入,保留全精度。

不要相信变量名。你会见到 timestamp_ms 装秒、created_at_seconds 装毫秒、unix_time 两者皆有的情况。

Y2K38

Unix 上 time_t 历史上是有符号 32 bit 整数。最大值 2_147_483_647,对应 2038-01-19 03:14:07 UTC。03:14:08 起,time_t 溢出成负数,回到 1901。

几乎所有桌面与服务器系统都已经迁到 64 bit time_t。没迁的恰恰是没人愿意碰的那些——工业设备里的嵌入式固件、ATM、传感器、POS 终端。Y2K38 不会像 Y2K 那样像末日,但从大约 2035 年起,今天写的软件开始向后看 13 年时,就会开始持续出现一连串"莫名其妙的故障"。

如果你存的时间戳要保存很久,用 64 bit 整数(int64)。数据库列类型,毫秒时间戳字段一律 BIGINT 而不是 INT——INT 的溢出点 ≈ 1970 + (2³¹ − 1)ms,已经在 1995 年就翻车了。

闰秒:那个谎言

Unix 时间假装每一天都正好 86,400 秒。地球不同意,每 18 个月左右两者会差大约 1 秒。国际时间机构定期插入"闰秒"——23:59:60 UTC——让原子时和平均太阳时对齐。

Unix 时间表示不出 23:59:60。约定是:要么跳过这一秒(Unix 时钟暂停),要么重放前一秒(时钟"涂抹")。绝大多数 NTP 同步的服务器走"涂抹"路线:把闰秒摊到 24 小时里,每秒都被几乎察觉不到地拉长一点点。Google 在 2008 年公开了自己的涂抹算法,现在已经是事实标准。

这件事在三个地方真的会咬人:

  • 金融系统——闰秒窗口里需要毫秒级排序时。
  • 任何存"时间间隔"的地方。 跨过一个闰秒的"10 秒间隔"实际上是 11 秒。
  • 日志和指标——会出现整秒边界上的重复时间戳,或者看起来的"空缺"。

IERS 在 2022 年宣布闰秒计划在 2035 年废止。在那之前还会偶尔插入。

时区不是时间戳的一部分

这一点几乎让所有人栽过跟头。Unix 时间戳没有时区。它就是从某个 UTC 时刻开始的秒数。时区只在你把它转换成"墙钟"表示(如 "2026-06-05 14:30")时才出场。

实践含义:

  • 存一个不带时区的墙钟字符串 "2026-06-05 14:30" 是数据腐败。你不知道它指向哪一个时刻。
  • 存 Unix 时间戳不会丢失"什么时候"的信息,只会丢失"那时候用户在哪儿"的信息。如果业务关心"在哪儿",单独存 IANA 时区名(Asia/ShanghaiAmerica/New_York),不要存偏移量(+08:00)——偏移量不携带 DST 规则。
  • ISO 8601 字符串带 Z 或带偏移后缀(2026-06-05T06:30:00Z)是无歧义的。不带后缀的,应当拒绝。

DST:那个反复出现的 bug

夏令时让某些墙钟时间根本不存在(春天向前那个空缺),让另一些时间发生两次(秋天向后那个重叠)。

美国东部时区,3 月第二个星期天,2:00 到 3:00 不存在——时钟从 1:59:59 直接跳到 3:00:00。日历里"每周二 2:30"这种条目,那天会怎么解读?(通常会被解读成 3:30,因为大部分日历系统会安静地跳过缺失的那一小时。)

11 月第一个星期天,1:00 到 2:00 会发生两次。那一天的墙钟值 01:30 是有歧义的——它对应两个不同的 Unix 时间戳,相差正好 3600 秒。

DST 处理错的工具会让 cron 任务跑零次(落在空缺里)或两次(落在重叠里)。不要跨 DST 转换地存"墙钟 + 偏移量"。直接存时间戳。

单调时钟 vs 墙钟时间

系统时间可以倒流。时钟可以被手动改。NTP 可以把时钟推前或推后来纠正漂移。笔记本睡了一小时再唤醒,对"现在"的定义就和睡前不一样了。

如果你在测时间间隔——请求延迟、按钮按下到现在的时间、动画帧时间——用单调时钟,不要用墙钟。单调时钟从某个系统自定义的原点(通常是开机)开始计秒,永不倒退。各语言 API 名字不同:C 的 clock_gettime(CLOCK_MONOTONIC)、Node 的 process.hrtime()、Python 的 time.monotonic()、Rust 的 Instant

如果你用 Date.now() 测间隔,正好碰上 NTP 中途纠错,间隔可能是负数。更糟的是当人为把时钟向前调时,间隔可能是个荒唐大的正数。这类"时钟干了奇怪的事"的 bug 报告,答案永远是"换单调时钟"。

常见坑

  • 秒和毫秒搞混。1000 倍误差是世界上最常见的时间戳 bug。
  • 存本地时间不带时区。早晚有人在另一台机器上读这份数据。
  • 用偏移字符串(+08:00)做存储。它不携带 DST 规则;6 月 1 号和 12 月 1 号同一座城市的偏移量未必相同。
  • 用墙钟时间测时间间隔。换单调时钟。
  • 取整。一个时间戳取整到秒再转回去,已经不是原来的值了——丢了最多 999ms 信息。显示前都保持精度
  • 比较来自不同精度源的时间戳而不归一化。Date.now() > some_seconds_field 永远是 true。

实用规则

  • 存 UTC,只在显示边界转本地时间。
  • 新代码用 64 bit 整数,数据库用 BIGINT
  • 一个系统选一个精度(秒或毫秒)并写进文档,不要混用
  • 测人类感知的间隔用单调时钟;存绝对时刻用墙钟。
  • "在哪儿"重要时,时区存 IANA 名而不是偏移量。
  • 拒绝不带时区信息的墙钟字符串。

主要参考资料

用于核对本文技术细节的标准与官方文档。

在线转换与排查

本站时间戳工具支持秒、毫秒、微秒在任意时区互转,并会标记歧义场景(DST 重复、秒/毫秒混淆)。所有运算在浏览器内完成。

打开时间戳工具

相关文章

继续阅读同一主题领域的实践指南。

查看全部文章

Cookie 同意

我们使用 Cookie 来增强您的体验并展示相关广告。您可以自定义您的偏好。