时间戳的零点就是 Unix 纪元

Unix 时间戳 0 精确对应 1970-01-01 00:00:00 UTC。那一刻就是 Unix 纪元,正负时间戳都以它为基准来度量。

Unix 时间戳 UTC 日期 含义
-86400 1969-12-31 00:00:00 纪元前一天
-1 1969-12-31 23:59:59 纪元前一秒
0 1970-01-01 00:00:00 Unix 纪元本身
1 1970-01-01 00:00:01 纪元后一秒
86400 1970-01-02 00:00:00 纪元后一天
1000000000 2001-09-09 01:46:40 十亿秒里程碑
2147483647 2038-01-19 03:14:07 32 位有符号上限

时间戳不是“你所在时区的某个日期”,它是一个全球统一的瞬间。你的时区只改变盖在这个瞬间上面的那层标签。

想看当前值每秒跳一下,可以打开实时纪元时钟;想核对本文里的任意数字,用 Epoch 转日期工具

说人话:什么叫“纪元”

纪元就是一个零点,是某个系统选定的、开始计数的那一天。

Unix 时间选了 1970-01-01 00:00:00 UTC 作为零点。于是时间戳只回答一个简单问题:我们离那一刻有多少秒?

有了这个模型就好理解了:

  • 0 是“正好在零点上”。
  • 10 是“零点之后十秒”。
  • -10 是“零点之前十秒”。
  • 1700000000 是“零点之后 1,700,000,000 秒”。

这也解释了为什么同一个数字在不同地方会显示成不同的钟面时间。0 在 UTC 是午夜,在纽约却显示为 1969 年 12 月 31 日的傍晚。瞬间没变,变的只是本地那层标签。

Unix 时间、POSIX 时间、纪元时间戳

在普通应用代码里,Unix 时间Unix 时间戳POSIX 时间纪元时间戳通常说的都是同一个以 1970 为基准的计数。名字的差别在你写接口约定的时候才真正要紧:

  • 产品文档和 API 文档里用 Unix 时间戳,然后把单位写清楚。
  • 当标准定义和它那套“每天 86,400 秒”的模型很关键时,用 POSIX 时间
  • 纪元时间戳这个说法,只有在同时讲明纪元和单位之后才用;Windows FILETIME、NTP、GPS 时间和 .NET ticks 都是基于纪元的计数器,但它们不是 Unix 时间。
  • time_t 当成一个实现层的类型,而不是可移植的字段格式——它的宽度和符号性依平台而定。

createdAtUnixSecondsevent_time_ms 这样的名字本身就携带了有用信息。而一个只叫 timestamp 的字段,逼着每一个使用方重新去把纪元、单位和含义都猜一遍。

为什么偏偏是 1970 年 1 月 1 日?

1970 年 1 月 1 日并没有藏着什么天文事件。务实的答案是:Unix 大约在 1969 到 1970 年间在贝尔实验室成型,开发者需要一个干净、离得又近的日期来做系统时间的参考点。

Dennis Ritchie 写的 Unix 发展史里提到,这个系统诞生于 1969 年,1970 到 1971 年间经历了 PDP-7 和 PDP-11 时期,随后成为后来 Unix 接口的基础。在那个背景下,1970-01-01 离项目起点足够近、好记,往前推得也够远,足以覆盖新系统里普通文件和进程的时间戳。

POSIX 后来把这个惯例正式化:The Open Group 的 POSIX 文本把纪元定义为 1970 年 1 月 1 日 UTC 的零时零分零秒,并规定“自纪元以来的秒数”按每个被表示的日子恰好 86,400 秒来计算。

对今天的开发者来说,重要的其实不是这个日期本身有多浪漫,而是:每一个服务端运行时、数据库、浏览器、日志系统、消息队列、API 和分析管道,都认同同一个参考瞬间。

负的 Unix 时间戳

负时间戳不是什么特殊语法,它就是纪元之前的有符号数字而已。

时间戳 UTC 日期 常见用途
-1 1969-12-31 23:59:59 Unix 时间开始前的最后一秒
-86400 1969-12-31 00:00:00 纪元前一天
-31536000 1969-01-01 00:00:00 纪元前一个平年
-2208988800 1900-01-01 00:00:00 NTP 的纪元换算成 Unix 时间戳
-11644473600 1601-01-01 00:00:00 Windows FILETIME 的纪元换算成 Unix 时间戳

现在主流语言的库大多都能表示这些值。JavaScript 里 new Date(-86400000).toISOString() 会得到 1969-12-31T00:00:00.000Z;Python 可以用 datetime.fromtimestamp(-86400, tz=timezone.utc);Java 可以用 Instant.ofEpochSecond(-86400)

麻烦出在边界上:

  • 一些电子表格和商业工具拒绝 1900 年之前的日期。
  • 有的数据库函数接受负纪元,有的却会截断或者返回 NULL
  • 一些老代码把时间戳存在无符号整数里,压根表示不了 1970 年之前。
  • 部分历史日期对历法本身敏感,尤其是在现代时区规则标准化之前。

如果你的数据里有出生日期、档案、证书、法律记录、老媒体文件,或者任何 1970 年以前的东西,请用 64 位有符号字段来存,并且把单位写进列名,比如 event_time_sevent_time_ms

秒、毫秒、微秒、纳秒

严格意义上的 Unix/POSIX 时间是自纪元起的秒数。而真实系统经常换个单位,纪元却保持不变。

2026 年的位数 大概率的单位 示例 读作 UTC
10 1700000000 2023-11-14 22:13:20
13 毫秒 1700000000000 2023-11-14 22:13:20
16 微秒 1700000000000000 2023-11-14 22:13:20
19 纳秒 1700000000000000000 2023-11-14 22:13:20

下面这个 bug,在生产日志里几乎天天能见到:

  • 1700000000 按秒解释 = 2023-11-14 22:13:20 UTC
  • 1700000000 按毫秒解释 = 1970-01-20 16:13:20 UTC
  • 1700000000000 按毫秒解释 = 2023-11-14 22:13:20 UTC
  • 1700000000000 按秒解释 = 一个遥远的未来年份,很多库都处理不干净

把单位写进字段名里。created_at 是有歧义的,而 created_at_screated_at_mscreated_at_unix_ms 能让下一个人不用猜。

时区:混乱真正的起点

Unix 时间戳里并不存 UTCPSTESTCET 或任何时区标签,它存的是一个瞬间。标签是后来由格式化代码挑的。

拿时间戳 0 来说:

  • UTC:1970-01-01 00:00:00
  • America/New_York(标准时期间):1969-12-31 19:00:00
  • Asia/Tokyo:1970-01-01 09:00:00

这三行说的是同一个瞬间。它们看起来不一样,只是因为显示用的时区换了。

经验法则很简单:瞬间就存成 Unix 时间戳或 UTC 日期时间。只有在展示的时候,或者要把用户填的本地钟面预约转成瞬间的时候,才用到用户的 IANA 时区。

比较的法则同样简单:比较瞬间,别比较格式化之后的字符串。某个本地时区里的 2026-03-08 02:30 可能因为夏令时而根本不存在,可 Unix 时间戳始终是时间轴上一个明确的点。

存储上限与 2038 年问题

真正的限制不在 Unix 时间戳这个格式上,而在存储类型上。

存储类型 范围 实际影响
32 位有符号秒 -21474836482147483647 1901-12-13 20:45:522038-01-19 03:14:07
32 位无符号秒 04294967295 1970-01-012106-02-07 06:28:15,但表示不了 1970 年之前
64 位有符号秒 约正负 2920 亿年 对普通应用的时间戳来说绰绰有余
JavaScript 的 Date 距 1970 前后各 1 亿天 范围很宽,但基于毫秒,且受数字精度规则约束

2038 年问题指的就是 32 位有符号的那个边界。过了 2147483647,32 位有符号的系统可能溢出、拒绝该值、把它截断,或者把存进去的位重新解释成一个负时间戳——看上去像 1901 年的某一天。现代 64 位操作系统和运行时基本已经跨过这道坎,但嵌入式设备、老 C 代码、二进制协议、数据库表结构和文件格式里,风险依然可能潜伏着。

闰秒:最别扭的那部分

POSIX 风格的 Unix 时间把每一个被表示的日子都当作恰好 86,400 秒。而 UTC 作为民用时标,可以插入闰秒来让原子时跟上地球自转。

这个错位就是闰秒在 Unix 系统里显得别扭的原因。民用钟面可能显示 23:59:60,但 POSIX 时间并不会为这个标号生成一个干净的额外整数。不同系统会用跳变、重复、在一个窗口内抹平,或者干脆依赖上游的时间同步来应付这件事。

对于常规的日志、API 时间戳、JWT 过期值、数据库行和界面上的日期,Unix 时间依然是趁手的工具。但如果场景是精密计时、分布式链路追踪、金融、卫星或科学测量,就不该把 Unix 时间戳当成一个完备的时标——那时候你可能需要单调时钟、支持 TAI/GPS 的时间,或者一张明确的闰秒表。

还有一个细节值得说准确:国际上的那个决定,并不是笼统的“2026 年投票废除闰秒”。第 27 届 CGPM 在 2022 年通过了第 4 号决议,决定在 2035 年或之前放宽 UT1-UTC 差值的允许上限,并要求为 2026 年的 CGPM 会议准备实施方案。

你迟早会碰上的其他纪元

Unix 是 Web 软件里的主流纪元,但不是唯一的。别的系统选了别的零点和别的滴答粒度。

系统 纪元 单位 转成 Unix 的捷径
Unix / POSIX 1970-01-01 00:00:00 UTC 本身就是 Unix 秒
Unix 毫秒 1970-01-01 00:00:00 UTC 毫秒 除以 1000
Windows FILETIME 1601-01-01 00:00:00 UTC 100 纳秒 tick filetime / 10000000 - 11644473600
.NET DateTime.Ticks 0001-01-01 00:00:00 100 纳秒 tick (ticks - 621355968000000000) / 10000000
NTP 1900-01-01 00:00:00 UTC ntp_seconds - 2208988800
Apple Core Data / CFAbsoluteTime 2001-01-01 00:00:00 UTC core_data_seconds + 978307200
GPS 时间 1980-01-06 00:00:00 秒,不含 UTC 闰秒 用当前的 GPS-UTC 偏移换算
Excel 1900 日期系统 1899-12-30 式的序列基准 显式处理 1900 闰年 bug

套路永远一样:认出纪元,认出单位,套上偏移,再把结果这个瞬间格式化出来。偏移量本身算起来并不难,真正出错的地方在于——想当然地以为所有大数字都是 Unix 毫秒。

微软的 FILETIME 文档把它定义为自 1601 年 1 月 1 日 UTC 起的 100 纳秒间隔数;.NET 文档把 DateTime.Ticks 定义为格里高利历中自 0001 年 1 月 1 日起的 100 纳秒间隔数。这些同样是基于纪元的体系,只不过它们不是 Unix 时间。

一份快速排查清单

时间戳看着不对劲时,先做完下面几项再去怪库:

  1. 数位数:10、13、16、19 位,通常分别对应秒、毫秒、微秒、纳秒。
  2. 确认显示用的时区。同一个瞬间,UTC 输出和本地输出可能落在不同的日历日上。
  3. 确认符号性。无符号存储表示不了 1970 年之前的日期。
  4. 确认上界。2147483647 就是 32 位有符号那道 2038 年的坎。
  5. 确认来源系统。FILETIME、.NET ticks、NTP、GPS、Excel、Core Data 用的纪元各不相同。
  6. 确认这个值是“瞬间”还是“时长”。掐表测时长通常该用单调时钟,而不是墙上时钟的 Unix 时间。

如果结果落在 1970 年 1 月,多半是把秒当成了毫秒。如果落在几千年后的未来,多半是把毫秒当成了秒。如果只差一小时,去查时区或夏令时换算。如果差了好几年,那就是纪元用错了。

这篇文章在整个时间戳系列里的位置

这一篇是概念上的起点:纪元是什么、时间戳 0 为什么重要,以及这个数字身上都附带了哪些假设。

下一步该看哪篇,取决于你手头的任务:

真正能长期靠得住的心智模型其实很简单:一个 Unix 时间戳标识的是相对 1970-01-01 00:00:00 UTC 的某一个瞬间。单位、时区和显示格式是三份独立的约定,每一份都得在系统边界上明确写出来。

Frequent questions:

Q: Unix 时间戳的零点是什么?
A: Unix 时间戳 0 就是 1970-01-01 00:00:00 UTC。它是 Unix 时间起算的那个参考瞬间,也就是纪元(epoch)。
Q: Unix 时间为什么从 1970 年开始?
A: Unix 大约在 1969 到 1970 年间诞生于贝尔实验室,1970-01-01 就顺理成章地成了系统时间计数器一个既近又好用的零点。POSIX 后来把这个纪元正式定为 1970 年 1 月 1 日 UTC 的零时零分零秒。
Q: Unix 时间戳里带时区信息吗?
A: 不带。这个数字表示的是一个瞬间。时区只影响它被显示成什么样子,比如显示成 UTC、America/New_York、Europe/London 还是 Asia/Tokyo。
Q: Unix 时间戳可以是负数吗?
A: 在支持有符号时间戳的系统上可以。-1 是 1969-12-31 23:59:59 UTC,-86400 是 1969-12-31 00:00:00 UTC。
Q: 纪元时间的单位一定是秒吗?
A: POSIX 的 Unix 时间是自纪元以来的秒数,但很多接口用的是同一个纪元、不同的单位:毫秒、微秒或纳秒。在 2026 年,当代的 Unix 秒通常是 10 位,毫秒 13 位,微秒 16 位,纳秒 19 位。
Q: Unix 时间戳最大能到多少?
A: 取决于存储类型。32 位有符号的 Unix 时间戳到 2147483647 为止,也就是 2038-01-19 03:14:07 UTC。而 64 位有符号的秒值,对普通软件的日期需求来说基本等于没有上限。
Q: POSIX 时间和 Unix 时间有什么区别?
A: 在日常开发里它们指的是同一个时间戳。POSIX 时间是标准定义的那个版本:自 Unix 纪元起的秒数,并且把每一个被表示的日子都当作恰好 86400 秒。
Q: Unix 时间会数闰秒吗?
A: 不会。POSIX 风格的 Unix 时间不会为 23:59:60 单独生成一个时间戳。各个系统会用跳变、重复、抹平或别的办法在这个简单数字之外去处理闰秒。
Q: 为什么有些时间戳显示成 1970 年?
A: 最常见的原因是单位搞混了:一个秒值被按毫秒解释了。举例来说,1700000000 秒是 2023-11-14 22:13:20 UTC,可 1700000000 毫秒却是 1970-01-20 16:13:20 UTC。
Q: Windows FILETIME 和 Unix 时间是一回事吗?
A: 不是。Windows FILETIME 计的是自 1601-01-01 UTC 起的 100 纳秒间隔数。要把 FILETIME 转成 Unix 秒,先除以 10000000,再减去 11644473600。