跳到主要内容
LeedyBox
支持我们

Unix 时间戳详解

Unix 时间戳用一个数字表示某个时刻。它便于存储和比较,但单位、时区和文本格式会导致许多转换错误。

什么是 Unix 时间

Unix 时间(也称为纪元时间或 POSIX 时间)计算的是自 1970-01-01 00:00:00 UTC(即 Unix 纪元)以来经过的秒数。0 就是纪元本身,1700000000 是 2023-11-14 22:13:20 UTC,负值表示 1970 年之前的时刻:-1 是 1969-12-31 23:59:59 UTC。

这个数字不包含时区:它在任何地方都表示同一时刻,因此非常适合用来存储、排序和比较事件。

Unix 时间忽略闰秒:每一天都恰好是 86400 秒。当 UTC 增加一个闰秒时,系统会重复一秒,或把它分摊到更长的时间段中(即“闰秒平滑”),因此跨越闰秒计算出的时间差会比实际流逝的时间少一秒。

秒、毫秒、微秒还是纳秒?

许多系统以更小的单位计数,而数字本身并不会说明单位。同一时刻可能表现为四个相差悬殊的数字:

单位 2023-11-14 22:13:20 UTC 位数(2001–2286 年) 常见于
秒 1700000000 10 date +%s、许多 API、JWT 的 exp 声明
毫秒 1700000000000 13 JavaScript Date.now()、Java System.currentTimeMillis()
微秒 1700000000000000 16 一些数据库和日志系统
纳秒 1700000000000000000 19 Go UnixNano()、Python time.time_ns()

对于当前的日期,数一数位数是最快的检查方法。LeedyBox 转换器也采用同样的思路:不超过 1011 的值按秒读取,不超过 1014 的按毫秒,不超过 1017 的按微秒,更大的值按纳秒。对于离今天很远的日期,这条经验法则会失效,因此当结果看起来不对劲时,请检查单位。

精度同样重要。JavaScript 数字只能精确表示不超过 253(约 9 × 1015)的整数,因此像 1700000000123456789 这样的纳秒值在按普通数字解析时会丢失末尾几位。请把这类值保留为字符串,或使用 BigInt;转换器内部就是这样处理的。要读取令牌的 exp 声明,请用 Base64 / URL / JWT 工具解码。

时区与夏令时

要把时间戳显示为日期和时间,需要一个时区。UTC 是基准;本地时间会加上一个偏移量,例如首尔为 +09:00,纽约冬季为 −05:00。

请优先使用 Asia/Seoul 或 America/New_York 这样的 IANA 时区名称,而不是固定偏移量和缩写:名称包含了该时区的历史和夏令时规则,而 CST 这样的缩写可以代表好几个时区。这些规则来自时区数据库,该数据库每年会更新好几次,而浏览器、操作系统和编程语言各自附带自己的副本,因此对过去或未来日期的计算结果可能不同。

把本地时间转换为时间戳时,夏令时会造成两种棘手的情况:

  • 间隙:时钟拨快时,有些本地时间并不存在。在 America/New_York 时区,2026-03-08 02:30 会被跳过,因为时钟从 02:00 直接跳到 03:00。
  • 重叠:时钟拨回时,有些本地时间会出现两次。2026-11-01 01:30 先在 EDT(UTC−04:00)中出现一次,一小时后又在 EST(UTC−05:00)中再出现一次。

不同的库处理这些情况的方式各不相同。LeedyBox 转换器在间隙中采用较晚的有效时间(此例中为 03:30),在重叠中采用较早的那一次,并会在提示中加以说明。定时任务也面临同样的问题;请参阅如何读懂 cron 表达式。

ISO 8601、RFC 3339 与 HTTP 日期

当某个时刻需要便于人阅读时,请使用 ISO 8601 或其更严格的互联网规范 RFC 3339 来书写:

2023-11-14T22:13:20Z
2023-11-15T07:13:20+09:00
2023-11-14T22:13:20.500Z

Z 表示 UTC。带有 Z 或偏移量的字符串没有歧义。两者都没有的字符串,例如 2023-11-14T22:13:20,表示时区未知的本地时间,不同解析器对它的解读也不一致。

Last-Modified 和 Expires 等 HTTP 头使用一种较旧的格式,它源自电子邮件的日期格式(RFC 2822,现为 RFC 5322),并以 GMT 表示:Tue, 14 Nov 2023 22:13:20 GMT。

2038 年问题

许多较旧的系统用有符号 32 位整数存储 Unix 时间。它的最大值 2147483647 对应 2038-01-19 03:14:07 UTC。再过一秒,计数器就会溢出为 -2147483648,被解读为 1901-12-13 20:45:52 UTC。当前的操作系统和编程语言大多使用 64 位时间值,可以覆盖数千亿年,但 32 位字段仍然存在于嵌入式设备、文件格式、数据库列和网络协议中。如果您的代码或数据用 32 位存储秒数,请尽早规划迁移,并且现在就测试 2038 年之后的日期。

代码和数据库中的时间戳

JavaScript 以毫秒为单位:

Math.floor(Date.now() / 1000);                  // 当前 Unix 时间(秒)
new Date(1700000000 * 1000).toISOString();      // "2023-11-14T22:13:20.000Z"
Date.parse("2026-10-03T09:00:00+09:00") / 1000; // 1790985600

Python 的 time.time() 以浮点数返回秒数。请传入 tz=timezone.utc,否则 fromtimestamp() 会返回不带时区信息的本机本地时间:

import time
from datetime import datetime, timezone

int(time.time())                                     # 秒
datetime.fromtimestamp(1700000000, tz=timezone.utc)  # 2023-11-14 22:13:20+00:00
datetime.fromisoformat("2026-10-03T09:00:00+09:00").timestamp()  # 1790985600.0

在 SQL 中,不同数据库的函数各不相同:

-- PostgreSQL
SELECT extract(epoch FROM now());   -- 秒,带小数部分
SELECT to_timestamp(1700000000);    -- timestamp with time zone
-- MySQL
SELECT UNIX_TIMESTAMP();
SELECT FROM_UNIXTIME(1700000000);   -- 按会话时区

在存储方面,养成几个习惯就能避免大多数问题:

  • 已经发生的时刻请用 UTC 存储:可以存入带时区的时间戳列,也可以存为 64 位整数,并在列名中注明单位,例如 created_at_ms。
  • 未来的本地事件(例如首尔 09:00 的会议)请存储为本地日期和时间加上 IANA 时区名称,在需要时再计算时间戳,这样日后的规则变更就不会让会议时间发生偏移。
  • 不要在同一列中混用不同的单位;单位一旦弄错,就会相差 1000 倍甚至更多。

常见问题

为什么我的时间戳转换出来是 1970 年的日期?

这个值很可能是按错误的单位读取的:把秒当作毫秒读取,会落在 1970 年 1 月;把毫秒当作秒读取,则会落在数万年之后。请数一下位数,或在转换器中选择单位。

Unix 时间戳取决于我所在的时区吗?

不取决于。它从 UTC 中一个固定的时刻开始计算秒数。只有为它显示的日期和时间才取决于时区。

Unix 时间能表示闰秒吗?

不能。Unix 时间把每一天都视为 86400 秒,因此像 23:59:60 这样的 UTC 时间没有属于自己的时间戳。