Unix 时间详解:从 1970 年到 2038 年
2026 年 11 月更新
像 1767225600 这样的 Unix 时间戳,表示从一个固定起点——Unix 纪元,即 UTC 时间 1970 年 1 月 1 日 00:00:00——开始经过的秒数。这个数字恰好对应 UTC 时间 2026 年 1 月 1 日零点。时间戳只是一个不带时区的数字,因此成了计算机存储和交换时间点最常用的方式。本文介绍如何读懂时间戳、如何在常用语言中正确转换,以及那些真正会引发 bug 的边界情况:毫秒、时区、闰秒和 2038 年问题。
Unix 时间是怎样计算的
Unix 时间每秒加 1,在同一时刻,地球上任何地方的值都相同。下面是几个参考点:
| 时间戳 | 日期和时间(UTC) | 说明 |
|---|---|---|
0 | 1970-01-01 00:00:00 | 纪元起点 |
86400 | 1970-01-02 00:00:00 | 一天是 86,400 秒 |
-86400 | 1969-12-31 00:00:00 | 负数表示 1970 年之前 |
1000000000 | 2001-09-09 01:46:40 | 第一个 10 位数的时间戳 |
1767225600 | 2026-01-01 00:00:00 | 下文示例使用 |
2000000000 | 2033-05-18 03:33:20 | |
2147483647 | 2038-01-19 03:14:07 | 有符号 32 位整数的最大值 |
在 Unix 时间里,每天都恰好是 86,400 秒,所以计算很简单:1,767,225,600 ÷ 86,400 = 20,454,也就是说 2026 年 1 月 1 日是纪元之后的第 20,454 天;给时间戳加上 7 × 86,400,就正好往后推一周。至于当地时间,那是另一回事,下面的“时区”部分会讲到。
秒、毫秒和更小的单位
传统的 Unix 约定以秒为单位,但很多系统使用更小的单位。通常看位数就能分辨:对于 2001 年到 2286 年之间的日期,以秒为单位的时间戳是 10 位数。
| 位数 | 单位 | 常见来源 | UTC 2026 年 1 月 1 日 0 点 |
|---|---|---|---|
| 10 | 秒 | Unix 命令行、PHP、大多数数据库、JWT 的 exp 声明 | 1767225600 |
| 13 | 毫秒 | JavaScript 的 Date.now()、Java 的 System.currentTimeMillis() | 1767225600000 |
| 16 | 微秒 | 部分 API、日志和分析系统 | 1767225600000000 |
| 19 | 纳秒 | Go 的 UnixNano()、Python 的 time.time_ns() | 1767225600000000000 |
单位搞混是非常常见的 bug。把毫秒当成秒,会得到几万年后的日期;把秒当成毫秒,则会回到 1970 年 1 月。如果转换出来的日期是 1970 年,首先怀疑这一点。Unix 时间戳转换工具会根据位数自动识别单位。
时区与 UTC
时间戳本身没有时区,它只标记一个时刻;只有在显示时才需要考虑时区。时间戳 1767225600 在不同城市是:
- 伦敦:2026 年 1 月 1 日 00:00(冬季为 UTC+0)
- 北京、上海:2026 年 1 月 1 日 08:00(UTC+8)
- 首尔、东京:2026 年 1 月 1 日 09:00(UTC+9)
- 纽约:2025 年 12 月 31 日 19:00(冬季为 UTC−5)
由此可以总结出几条好习惯:
- 以 UTC 存储和传输时间点,用 Unix 时间戳,或以
Z或时区偏移结尾的 ISO 8601 字符串。 - 只在“边缘”转换成当地时间:给人看日期,或从人那里读取日期的时候。
- 使用
Asia/Shanghai、America/New_York这样的 IANA 时区名称,而不是固定偏移或缩写。名称包含该地区的夏令时规则及其历史;而像 −05:00 这样的偏移,在纽约一年中只有部分时间是对的。中国目前不实行夏令时,全年都是 UTC+8,但处理欧美时间时,这个区别就很重要了。 - 对于按当地时间安排的未来事件,比如“每周一柏林时间 9 点开会”,应保存当地时间和时区名称,而不是预先算好的时间戳。各国政府会调整夏令时规则,今天算出的时间戳将来可能差一个小时。
要比较不同城市的时间,可以用自动处理夏令时的时区转换工具;要计算两个日期之间相差多少天,或给日期加上若干天,可以用日期计算器。
在代码中转换时间戳
所有示例都使用 1767225600,即 2026-01-01 00:00:00 UTC。
JavaScript
| 任务 | 代码 |
|---|---|
| 当前时间(秒) | Math.floor(Date.now() / 1000) |
| 时间戳 → ISO 字符串 | new Date(1767225600 * 1000).toISOString() 结果为 "2026-01-01T00:00:00.000Z" |
| 按指定时区显示 | new Date(1767225600 * 1000).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }) |
| 日期字符串 → 时间戳 | Date.parse("2026-01-01T08:00:00+08:00") / 1000 结果为 1767225600 |
JavaScript 的日期以毫秒为单位,所以传入时要乘以 1,000,取出时要除以 1,000。另外,new Date("2026-01-01")(只有日期)会被当作 UTC 零点,而 new Date("2026-01-01T00:00")(有日期和时间但没有偏移)会被当作当地时间。在中国运行时,这两个值相差 8 小时。只要你指的是一个确定的时刻,就一定要加上 Z 或时区偏移。
Python
| 任务 | 代码 |
|---|---|
| 当前时间(秒) | time.time()(浮点数) |
| 时间戳 → UTC datetime | datetime.fromtimestamp(1767225600, tz=timezone.utc) |
| 时间戳 → 指定时区 | datetime.fromtimestamp(1767225600, tz=ZoneInfo("Asia/Shanghai")) |
| datetime → 时间戳 | datetime(2026, 1, 1, tzinfo=timezone.utc).timestamp() 结果为 1767225600.0 |
需要导入 from datetime import datetime, timezone 和 from zoneinfo import ZoneInfo(Python 3.9 及以上)。避免使用没有 tzinfo 的“naive”datetime:对它调用 .timestamp() 时,会假定它是计算机的本地时区;而返回 naive 值的 datetime.utcfromtimestamp() 从 Python 3.12 起已被弃用。在 Windows 上,系统没有自带 IANA 时区数据库,所以使用 zoneinfo 还需要安装 tzdata 包(pip install tzdata)。
SQL
| 数据库 | 时间戳 → 日期 | 日期 → 时间戳 |
|---|---|---|
| PostgreSQL | to_timestamp(1767225600) | EXTRACT(EPOCH FROM TIMESTAMPTZ '2026-01-01 00:00:00+00') |
| MySQL | FROM_UNIXTIME(1767225600) | UNIX_TIMESTAMP('2026-01-01 00:00:00') |
| SQLite | datetime(1767225600, 'unixepoch') | unixepoch('2026-01-01 00:00:00') |
PostgreSQL 的 to_timestamp 返回 timestamptz,按会话的 TimeZone 设置显示。MySQL 的 FROM_UNIXTIME 和 UNIX_TIMESTAMP 都使用会话时区,所以同一条查询在两台服务器上可能得到不同结果;如果想用 UTC,先执行 SET time_zone = '+00:00'。SQLite 以 UTC 工作;unixepoch() 需要 SQLite 3.38 或更高版本,旧版本可以用 strftime('%s', '2026-01-01 00:00:00'),它返回的是文本。
命令行
| 任务 | Linux(GNU date) | macOS(BSD date) |
|---|---|---|
| 当前时间(秒) | date +%s | date +%s |
| 时间戳 → 日期(UTC) | date -u -d @1767225600 | date -u -r 1767225600 |
| 日期 → 时间戳 | date -u -d '2026-01-01 00:00:00' +%s | date -u -j -f '%Y-%m-%d %H:%M:%S' '2026-01-01 00:00:00' +%s |
闰秒
为了跟上地球略不规则的自转,UTC 偶尔会插入一个闰秒。自 1972 年以来共增加了 27 次,最近一次是在 UTC 时间 2016 年 12 月 31 日 23:59:60,也就是北京时间 2017 年 1 月 1 日 7:59:60。Unix 时间假装闰秒不存在:每天都恰好是 86,400 秒,所以在闰秒期间,Unix 时钟会重复或停住一秒;一些大型服务商则把多出来的这一秒分摊到好几个小时里慢慢消化(称为“闰秒平滑”)。对大多数软件来说,这是察觉不到的。在测量短时间间隔这类会受影响的场景中,请使用单调时钟,例如 JavaScript 的 performance.now() 或 Python 的 time.monotonic(),而不是挂钟时间。2022 年,国际计量大会通过决议,将在 2035 年前实际上取消闰秒。
2038 年问题
许多旧系统用有符号 32 位整数存储 Unix 时间,它的最大值是 2,147,483,647,即 UTC 时间 2038 年 1 月 19 日(星期二)03:14:07,北京时间同日 11:14:07。再过一秒,计数器就会溢出变成 −2,147,483,648,这样的系统会把它读成 UTC 时间 1901 年 12 月 13 日 20:45:52。
现代的 64 位操作系统和编程语言使用 64 位时间戳,大约 2,920 亿年内都不会溢出。风险在于仍使用 32 位的旧设备和嵌入式设备、文件格式和数据库字段。例如,MySQL 的 TIMESTAMP 列类型最大只能到 2038-01-19 03:14:07 UTC,而 DATETIME 没有这个限制。那些需要计算遥远未来日期的软件,比如 15 年期的贷款还款计划或有效期很长的证书,可能在 2038 年到来之前很久就会碰到这个上限。
ISO 8601:便于人类阅读的格式
如果时间既要给程序读,也要给人看,就使用 ISO 8601,或者它面向互联网的更严格版本 RFC 3339。各字段从大单位排到小单位:
2026-01-01T00:00:00Z:UTC 时间,Z表示偏移为零2026-01-01T08:00:00+08:00:同一时刻,用北京时间表示2026-01-01:不带时间的日历日期
由于字段从年排到秒,格式和偏移相同的 ISO 8601 字符串按纯文本排序就是时间顺序,非常适合用在文件名和日志里。而 01/02/2026 这样的写法在美国表示 1 月 2 日,在欧洲大部分地区却表示 2 月 1 日,最好避免。
相关工具
- Unix 时间戳转换 – 在秒、毫秒、微秒、纳秒时间戳与日期之间互相转换。
- 时区转换 – 比较不同城市的日期和时间,自动处理夏令时。
- 日期计算器 – 计算两个日期相差的天数,或给日期加上天、周、月。