FlowingDev

Epoch 时间揭秘:那个只会滴答往前的时钟

了解什么是 Unix 时间(或 Epoch 时间):它是自 1970 年 1 月 1 日以来经过的秒数,被计算机用来统一追踪时间。

试用工具: Unix 时间戳转换器

一言以蔽之

Epoch 时间就是计算机追踪时间的方式,它使用一个单一、不断增长的数字:自 1970 年 1 月 1 日午夜 UTC 时间以来所经过的总秒数。

它解决的问题

人类和时间的关系相当复杂。我们有时区、夏令时,还有像 MM/DD/YYYY 和 DD/MM/YYYY 这样的格式之争。我们写下“2024 年 10 月 8 日下午 3 点”,但在东京和在多伦多,这代表的可是不同的时刻。简直是一团乱麻,充满了歧义。

而计算机呢,最讨厌的就是这种模棱两可。它们需要一种单一、通用、数学上简单的方式来表示一个时间点。想用“10 月 8 日”来做数学运算简直是噩梦。但用一个普普通通的数字来计算?那可是计算机的拿手好戏。

这正是 Unix 时间(也叫 Epoch 时间或 POSIX 时间)被创造出来要解决的问题。早在 20 世纪 70 年代 Unix 操作系统的早期,它的创造者们需要一个简单直接的计时系统。他们决定选择一个任意的起点——一个“纪元”(epoch)——然后就……开始数数。

被选中的纪元是 1970 年 1 月 1 日 00:00:00 UTC。为什么是这个时间点?因为它是个整数,挺好记的,而且对于当时的技术来说,这个起点也足够近。

从那一刻起,每过一秒,一个全球通用的计数器就会加一。所以,计算机不必再去解析“多伦多时间(EDT)2024 年 10 月 8 日下午 3 点”,它只需要存储数字 1728409200 就行了。这个数字代表了地球上任何地方在同一时刻的那个确切瞬间。没有时区,没有格式,也没有“是上午还是下午?”的疑问。就一个数字。问题解决。

底层工作原理

从核心上讲,这个概念简单得要死。但就像科技圈里所有的事情一样,魔鬼藏在细节中。

纪元(Epoch)和单位

整个系统建立在两个概念之上:

  1. 起点(纪元): 固定在 1970-01-01T00:00:00Z。这里的 Z 代表 Zulu,是军事和航空领域对 UTC(世界协调时间)的术语。在 Epoch 时间的世界里,这一刻就是 0。
  2. 度量单位: 标准的、官方的单位是秒。

所以,时间戳 1 代表 1970-01-01T00:00:01Z。而第二天开始的时间戳,1970-01-02T00:00:00Z,就是 86400(因为一天有 60 秒 * 60 分钟 * 24 小时 = 86,400 秒)。

// 一个遥远未来的日期
const humanDate = new Date('2035-10-26T10:00:00Z');

// 其对应的秒级 Epoch 时间戳
const epochTimestamp = 2071754400;

当你的电脑把这个时间戳显示为本地时间时,它在幕后做了一次转换。它会取这个通用的 UTC 时间戳,然后应用你系统的时区偏移量,最后以一种你能看懂的方式显示出来。然而,底层的那个数字,依然是纯粹且通用的。

变体:毫秒、微秒和纳秒

有时候,你需要测量比秒还快的事件。为此,系统会使用更精确版本的 Epoch 时间戳。原理相同,只是单位变了。

单位 示例值(代表同一时刻) 常见位数 典型用例
秒 1728409200 10 POSIX 标准;API、数据库。
毫秒 1728409200123 13 JavaScript (Date.now())、现代 API。
微秒 1728409200123456 16 高性能系统、某些数据库。
纳秒 1728409200123456789 19 科学计算、Go 语言。

这是处理时间戳时 bug 的头号来源。如果一个系统给你一个 13 位的数字,而你把它当作秒来处理,那你计算出的将是几千年后的日期。永远记得检查文档,或者看看数字的位数,搞清楚你到底在和什么打交道。

“2038 年问题”

这是一个经典的计算机传说。许多早期系统为了节省宝贵的内存,将 Epoch 时间戳存储为32 位有符号整数。

“位”(bit)就是 1 或 0。“32 位”意味着你有 32 个槽位来存放 1 和 0。“有符号”意味着其中一位被用来表示数字是正数还是负数。这就给数字本身留下了 31 位,其能表示的最大值为 2^31 - 1,即 2,147,483,647。

当自 1970 年以来的秒数达到这个极限时会发生什么?这一刻将发生在 UTC 时间 2038 年 1 月 19 日,星期二,03:14:07。就在下一秒,这个整数会溢出。就像汽车的里程表从 999999 翻转到 000000 一样,这个 32 位的时间戳会回绕到它的最大负值(-2,147,483,648)。这对应的日期是 1901 年 12 月。

对于任何没有打补丁的 32 位系统来说,这将引发一场时间上的大混乱。想想那些老旧汽车、工业设备或网络路由器里的嵌入式系统。

解决方法?使用64 位整数。一个 64 位整数可以存储一个大到令人难以置信的数字,它在大约 2920 亿年内都不会溢出。到那个时候,太阳早就膨胀并吞噬了地球,所以我们大概可以称之为永久解决方案了。大多数现代操作系统和语言都已经完成了切换。

闰秒:美中不足

地球的自转并不是完全规律的;它正在轻微地变慢。为了让我们的原子钟(它们超级规律)与太阳日保持同步,国际机构偶尔会在日历中增加一个“闰秒”。这意味着某一分钟可能会有 61 秒(例如,23:59:60)。

那么 Epoch 时间是怎么处理这个问题的呢?它不处理。

官方地讲,POSIX 标准忽略了闰秒。它假定每天都不多不少,正好 86,400 秒。当闰秒发生时,系统有几种处理方式,但常见的一种是有效地重复前一秒。23:59:59 的时间戳可能会出现两次。这维持了秒数连续、不间断的计数,但也意味着 Unix 时间戳并不总是能完美地映射回现实世界中的 UTC。对于 99.9% 的应用来说,这不是问题。但对于高频交易或科学测量来说,这是个巨大的麻烦。

真实世界的案例

时间旅行缓存奇案

一个开发团队正在发布一个由缓存系统支持的新功能。为了提高性能,他们将数据缓存一小时。逻辑很简单:expiration_time = current_time() + 3600。他们将代码部署到了整个服务器集群。

突然间,奇怪的 bug 铺天盖地而来。数据几乎立刻就从缓存中消失了。经过数小时的疯狂调试,他们找到了罪魁祸首。集群中的一台新服务器的系统时钟设置不正确——它比所有其他服务器都慢了五分钟。

当一个用户的请求打到一个正确的服务器上,它会用比如 1678886400 (下午 12:00) 的过期时间来缓存数据。如果后续对同样数据的请求打到了那台“慢”服务器上,它的时钟显示是上午 11:55。当它检查缓存时,看到过期时间是下午 12:00,于是正确地提供了数据。但如果第一个请求就打到了慢服务器上,它会设置一个过期时间为 1678882800 (它自己的时间上午 11:00,加上一小时是下午 12:00)。但当时是上午 11:55。等等,这不对。

我们再来试一次。服务器 A 的时间是 12:00。它为缓存设置了 12:00 + 1 小时 = 13:00 的过期时间。服务器 B 的时钟慢了,它觉得现在是 11:55。当服务器 B 需要写入缓存时,它设置的过期时间是 11:55 + 1 小时 = 12:55。现在,如果服务器 A 看到一个在 12:55 过期的条目,它会认为还剩 55 分钟,而服务器 B 认为还有整整一小时。这就导致了不一致。

真正的大混乱发生在时钟偏差很大的时候。如果服务器 B 的时钟慢了一个小时(当实际时间是 12:00 时,它认为是 11:00),它会设置一个过期时间为 11:00 + 1 小时 = 12:00。从服务器 A 的角度来看,这个新的缓存项在它被创建的那一刻就立即过期了。数据就这样凭空消失了。

教训: Unix 时间戳是绝对的,但它们是由可能并不同步的系统时钟生成的。在分布式系统中,保持时钟同步(通常使用网络时间协议,即 NTP)不仅仅是好习惯,而是至关重要。

那个用毫秒说话的 API

一名前端开发者正在构建一个用于显示用户活动的仪表盘。后端 API 提供了一个 last_login 字段,带着一个时间戳,比如 1678886400。这位开发者用一个 JavaScript 库来显示它:new Date(1678886400)。

结果非常诡异。每个用户的最后登录时间都显示为“1970 年 1 月 20 日”。这是怎么回事?

这位开发者花了一个小时,怪库、怪自己的代码、甚至怪月亮的阴晴圆缺。最后,他尝试集成了同一个 API 的另一个端点。这次,时间戳是 1678886400123。它有 13 位数字!他突然茅塞顿开了。JavaScript 的 Date 对象构造函数期望的是毫秒级的时间戳,而不是秒级的。

后端发送的是一个标准的 10 位秒级时间戳。前端却把 1,678,886,400 解释为自纪元以来的毫秒数,这算出来是 1970 年纪元开始后仅几周的一个日期。解决方法很简单:new Date(1678886400 * 1000)。

教训: 一定,一定,一定要核实时间戳的精度。三个零的差别,就是今天和 1970 年的差别。

常见的错误和陷阱

  • 忘记时区。 Unix 时间戳永远,毫无例外地,是 UTC 时间。当你把它转换成人类可读的日期时,你的编程语言或工具几乎总是会使用你电脑的本地时区。如果你不考虑到这一点,就会造成巨大的混乱。1728409200 是一个确切的时间点,但它在纽约会显示为 06:00,在东京则显示为 19:00。数字才是真相,显示出来的只是解释。

  • 混淆秒和毫秒。 这是经典的“差 1000 倍”错误。它是处理时间戳时最最常见的 bug。经验法则是:10 位数字是秒,13 位数字是毫秒。如果你看到其他位数的,就要非常警惕了。

  • 忽略 2038 年问题。 如果你用现代语言构建一个标准的 web 应用,你可能没事。但如果你在为嵌入式 IoT 设备、汽车信息娱乐系统编写 C 代码,或者在维护一个遗留的 32 位系统,Y2038 bug 就是一个非常真实、滴答作响的定时炸弹。

  • 使用模糊的字符串生成时间戳。 从像“March 15, 2025 10:00 PM”这样的字符串创建时间戳是在自找麻烦。这是你的本地时区?服务器时区?还是 UTC?始终应该从支持时区的对象生成时间戳,或者使用明确的 UTC 字符串(比如 ISO 8601 格式:2025-03-15T22:00:00Z)。

为什么你应该关注它

作为一个现代开发者,你不可能不了解 Epoch 时间。它是计算领域里关于时间的“通用语”(lingua franca)。你会在任何地方遇到它:

  • API: JSON 负载中像 createdAt、updatedAt 和 expires_at 这样的字段会频繁使用它。
  • JWT: exp (expiration)、iat (issued at) 和 nbf (not before) 这些 claim 都是标准的 Unix 时间戳。
  • 数据库: 将时间存储为单个整数,在索引和存储方面通常比使用复杂的 DATETIME 类型更高效。
  • 日志文件: 使用数字时间戳可以非常轻松地关联来自几十个不同服务器和服务(即使它们在不同时区)的事件。
  • 文件系统: 大多数文件系统都将文件的创建和修改日期存储为 Unix 时间戳。

理解它的工作原理能让你自信地调试一大类棘手的、与时间相关的 bug。它让你能穿透人类世界中混乱的时区和日历,像计算机一样思考时间:把它看作一条简单、有序的数字线。

深入了解

理论搞定,动手试试吧——100% 在你的浏览器中运行。

试用工具: Unix 时间戳转换器