D
开发工具箱

Unix 时间戳:那个从 1970 年开始计数的数字

编程转换 2026年6月15日 约 1 分钟阅读

什么是 Unix 时间戳?

Unix 时间戳,就是从 1970 年 1 月 1 日 00:00:00 UTC 开始,到当前时刻为止,一共经过了多少秒

一个整数。仅此而已。

比如我写下这句话的时候,时间戳大概是 1734288000。这个数字本身没有任何格式、没有时区、没有冒号分隔符——它就是纯粹的秒数。

你可能会觉得:这也太简陋了吧,连年月日都没有,算什么时间?

但正是这种简陋,让它成了全世界计算机系统之间交换时间信息的”通用语”。Unix 时间戳就像全球统一的秒表——不管你在北京、纽约还是伦敦,同一个时刻的时间戳永远是一样的。时区是展示层的事,和底层的秒数没关系。

Unix 时间戳的工作原理

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

说实话,没有特别神圣的理由。

Unix 操作系统诞生于 1969 年前后,当时贝尔实验室的 Ken Thompson 和 Dennis Ritchie 需要一个简单的计时方式。他们选了一个”足够近”的起点——1970 年 1 月 1 日。这个日子被称为 Unix Epoch(Unix 纪元)。

我当时以为这个日期有什么天文意义——比如某个星球的运行周期起点。后来才知道,纯粹是图方便。早期 Unix 的系统时钟用 32 位有符号整数存储秒数,每秒钟加 1。如果起点选 1970 年,到溢出还有三十多年,看起来”够用了”——谁知道 Unix 能活这么久呢?

这个 32 位整数的范围是 -2,147,483,648 到 2,147,483,647,分别对应 1901 年 12 月 13 日和 2038 年 1 月 19 日。换句话说,Unix 不仅向前数,还能向后数——你可以用负数时间戳表示 1970 年之前的日期。

时间戳的三种精度

日常说的”时间戳”其实有三个常见版本:

精度长度例子典型场景
10 位1734288000MySQL TIMESTAMP、Unix 文件时间、大部分 API
毫秒13 位1734288000123JavaScript Date.now()、Java System.currentTimeMillis()
微秒/纳秒16-19 位1734288000123456高频交易、性能日志、分布式追踪

坑就在这。我之前对接一个第三方支付接口,文档写的是”时间戳”,我以为就是秒级的,传了个 10 位数字过去。结果对方用的 Java,默认毫秒级,我的时间戳比他们的预期小了 1000 倍,时间被解析到了 1970 年 1 月 18 日——直接触发”订单已过期”的错误。排查了半天才发现是精度没对齐。现在每次接新 API,第一件事就是确认对方的时间戳到底是秒还是毫秒。

核心特性

特性说明
起点1970-01-01 00:00:00 UTC(Unix Epoch)
类型32 位 / 64 位有符号整数
无时区性时间戳本身不携带时区信息,始终基于 UTC
单调递增忽略闰秒,每秒钟严格 +1
可计算性整数运算——两个时间戳相减就是间隔秒数,不需要解析字符串
32 位溢出点2038-01-19 03:14:07 UTC(Year 2038 Problem)

这里说说闰秒。地球自转不是绝对均匀的,所以 UTC 偶尔会插入闰秒来对齐天文观测。但 Unix 时间戳不认闰秒——它假装每分钟都是 60 秒,每天 86400 秒。当闰秒发生时,POSIX 时间要么暂停要么重复同一秒。这个处理虽然”不科学”,但对大多数软件来说,简单比精确更重要。

实际应用场景

1. 数据库时间字段

MySQL 的 TIMESTAMP 类型内部存的就是 Unix 时间戳(32 位),这也是为什么 MySQL 的 TIMESTAMP 只能表示 1970 到 2038 年之间的时间。相比之下,DATETIME 类型存储的是人类可读的日期格式,范围从 1000 年到 9999 年——但它不随连接时区变化。

选型的经验法则是:如果你的数据需要跨时区展示,用 TIMESTAMP;如果你的时间代表一个”人类约定的时刻”(比如生日、会议时间——不会因为时区不同而改变),用 DATETIME。

2. API 中的时间传输

REST API 传时间有两种流派:传 Unix 时间戳,或者传 ISO 8601 字符串。时间戳的好处是没歧义——就是个整数,接收方自己按本地时区格式化。ISO 8601 的好处是可读性强——2024-12-15T14:30:00Z 一眼就知道是几点。一般建议是:内部系统之间传时间戳(性能更好),对外给第三方或前端传 ISO 8601(人类友好、自带时区信息)。

3. JWT 的过期判定

JWT 的 Payload 里,exp(过期时间)和 iat(签发时间)用的都是 Unix 时间戳——秒级。服务端验证时直接拿当前时间戳和 exp 比较,整数比大小,一行代码的事。如果用字符串格式的时间,还得解析、转换、考虑时区,性能差了一个数量级。

4. 缓存过期与 CDN

缓存的 ExpiresCache-Control 背后,服务器内部用的就是时间戳比较。CDN 节点遍布全球,不同节点在不同时区,但大家都用 UTC 时间戳做缓存有效性判断——时区差异完全不会造成逻辑错误。

5. 日志与监控

生产环境的日志里,每一行前面那个时间戳——[2024-12-15T14:30:00.123Z]——底层采集的时候,通常先拿到 Unix 毫秒时间戳,再格式化成字符串。ELK、Splunk、Prometheus 这类系统在做时间范围查询时,内部也是先把你的查询时间转成时间戳再做数值比较,而不是做字符串匹配。

常见误区

误区一:时间戳包含时区信息

这是最常见的误解。时间戳就是 UTC 秒数。北京时间的下午 3 点和纽约时间的凌晨 2 点,如果它们是同一个 UTC 时刻,那时间戳完全一样。时区转换是展示时的事。我之前做跨国会议排期功能,前端在北京时区选了”明天上午 9 点”,直接把这个 9 点转成了时间戳——但用户在美国登录时,时间戳又被转回了美国本地时间展示。看起来像是会议时间”变早了”,用户投诉了好几个工单。问题的根源不是时间戳,而是存的时候到底应该按哪个时区去算——最后定了一个规则:用户选时间的时区信息必须和会议”所在地”绑定,存储时统一转 UTC 时间戳。

误区二:时间戳和日期字符串是一一对应的

不是。一个时间戳可以对应不同的”日历时间”——取决于你怎么处理时区。时间戳 0 在 UTC 下是 1970-01-01 00:00:00,在北京时区(UTC+8)下是 1970-01-01 08:00:00。两组字符串不同,但底层是同一个时刻。反过来也是:同样是”2024 年 1 月 1 日上午 0 点”,北京的 0 点和伦敦的 0 点是两个完全不同的时间戳,差了 8 小时。这个坑我踩过不下三次。

误区三:所有系统的时间戳起点都是 1970 年

Unix 和 Linux 用 1970,但 Windows 的 FILETIME 用的是 1601 年 1 月 1 日(公历改革年),Macintosh 的 HFS+ 用 1904 年,NTP 协议用 1900 年。不同系统间传时间数据时,要确认 epoch 是否一致。一般情况下 Web API 和编程语言的标准库都统一了 Unix epoch,但如果你在做嵌入式或跨系统底层对接,这里是可能出诡异的。

Unix 时间戳 vs ISO 8601

Unix 时间戳ISO 8601
形式整数(1734288000字符串(2024-12-15T14:30:00Z
可读性差——人类无法直接读懂好——一眼看出年月日
时区信息无(永远 UTC)有(Z+08:00 等)
存储空间4-8 字节至少 20 字节
计算友好性极好——整数加减差——需要解析和归一化
精度灵活(秒/毫秒/纳秒)灵活但解析更复杂
排序天然支持(整数排序)需要字典序且格式必须统一
跨语言兼容极好好,但不同实现可能有细微格式差异

时间戳的优势在于它是”计算机语言”,ISO 8601 的优势在于它是”人类语言”。我的习惯是:数据库存时间戳,日志打 ISO 8601,API 输出 ISO 8601(让调用方省心),API 输入两种都接受。

2038 年问题

到底会怎样?

32 位有符号整数能表示的最大秒数是 2,147,483,647。在这一秒之后,计数值会溢出变成 -2,147,483,648,对应的日期是 1901 年 12 月 13 日。

这就是所谓的 2038 年问题(Year 2038 Problem)——部分还在用 32 位 time_t 的系统,到了 2038 年 1 月 19 日 03:14:07 UTC 这一秒之后,时间会”穿越”回 1901 年。

听起来很像 Y2K(千年虫)对吧?本质上就是同一种问题——设计时没预料到系统会活这么久。

谁会受影响?

32 位嵌入式设备风险最大——工业控制器、老式路由器、车载系统、早期的 IoT 设备。这些设备软件更新困难,很多至今还在用 32 位 time_t。相对来说,64 位系统(time_t 扩展到 64 位后可以表示到 2920 亿年后)和现代的 Web 服务(大部分语言在 64 位平台上 time_t 已经是 64 位)基本上不受影响。

怎么修复?

time_t 从 32 位升级到 64 位就搞定了。Linux 内核从 5.6 版本(2020 年)开始完成了 2038 年问题的内核侧修复,glibc 在 64 位架构上也早就用了 64 位 time_t。真正的难点不是”怎么修”,而是”找到那些还在用 32 位的角落”——老旧嵌入式设备、某些文件系统(ext3 的 inode 时间戳就是 32 位)、还有个别数据库的古董版本。

常见问题

Q: 怎么获取当前 Unix 时间戳?

命令行最简单:Linux/macOS 用 date +%s,Windows PowerShell 用 [int][double]::Parse((Get-Date -UFormat %s))。代码里:JavaScript 用 Math.floor(Date.now() / 1000),Python 用 int(time.time()),Java 用 System.currentTimeMillis() / 1000

Q: 时间戳 0 表示什么时间?

1970-01-01 00:00:00 UTC。如果在 MySQL 里插入一个 TIMESTAMP 值为 0,会变成 1970-01-01 00:00:01(因为 MySQL 把 0 视为无效值,自动加 1 秒)。这个差异也要留意。

Q: 时间戳和时区到底什么关系?

时间戳永远是 UTC。你可以把时间戳想象成宇宙中的一个绝对瞬间,时区是把这个瞬间映射到当地钟表的方式。new Date(timestamp * 1000) 在你的电脑上会显示你本地时区的时间,但同一个 timestamp 在别的时区会显示不同的钟表读数——底层瞬间是一样的。

Q: 2038 年问题会影响我的 Web 应用吗?

大概率不会。如果你的后端跑在 64 位 Linux 上、用 Node.js/Python 3/Go/Java 等主流语言的最新版本,time_t 已经是 64 位了。需要担心的是:你的数据库类型(MySQL TIMESTAMP 32 位还没改的话,2038 年会炸)、你用的第三方 SDK(一些 C 语言写的扩展可能还在用 32 位)、以及你身边有没有那种跑了十几年没人敢动的老服务器。

Q: 为什么不用人类可读的日期字符串来代替时间戳?

不是替代关系,是互补关系。时间戳适合计算、比较和存储;日期字符串适合展示、日志和人工排查。最好的实践是:内部逻辑全程用时间戳,只在输入和输出的边界上做格式转换。我一贯的做法是”存用时间戳,显用 ISO”——两者不是二选一,是各司其职。