返回博客
Reference 2026-04-24

时间戳和ISO 8601解释

在系统间处理日期和时间而不发生时区灾难。

生产环境中的每一个时区bug,追根溯源都是少数几个概念性错误之一:把时刻(instant)和挂钟时间混为一谈、把偏移量当成时区,或者相信解析器能猜出你的意思。Unix时间戳和ISO 8601这些格式本身是容易的部分,知道某条数据需要哪种表示才是真正的技能。

时刻 vs 挂钟时间

时刻是全球时间线上的一个点:「支付完成的那一瞬间」。挂钟时间是本地时钟显示的内容:「会议在首尔时间09:00」。它们是不同的数据类型。时刻可以永远安全地存为UTC;未来的挂钟事件则不行——下文详述,这是最常被发布上线的时区bug。

Unix时间

1745939938          秒            (经典Unix)

1745939938123 毫秒 (JavaScript Date.now())

1745939938123456 微秒 (许多数据库)

Unix时间计数自1970-01-01T00:00:00Z以来的秒数,并忽略闰秒——每天在定义上恰好是86,400秒,闰秒通过让某一秒变成两倍长(或像Google和AWS的时钟那样在数小时内平滑抹开)来吸收。它没有时区、紧凑、天然可排序。它的风险在于单位混淆——把秒值当毫秒读会落在1970年1月,把毫秒值当秒读会落在公元57,000年——以及在日志中不可读。如果一个时间戳字段两种单位都可能出现,那就是bug工厂;统一到一种单位并在字段名中注明(created_at_ms)。

ISO 8601与RFC 3339

2026-04-30T15:32:18Z             UTC时刻

2026-04-30T15:32:18.123+09:00 带毫秒和偏移量

2026-04-30 无时间的日历日期

ISO 8601是一个庞大的标准,涵盖时长(P3Y6M4D)、区间、周日期(2026-W18-4)和序数日期。API几乎总是指 RFC 3339,即互联网剖面:完整的日期和时间、带连字符和冒号的扩展记法、以及强制的偏移量或 Z。规范里两个有用的细节:小写的 tz 合法但按惯例避免;-00:00 在历史上表示「偏移量未知」——输出时请规范化为 Z 和大写分隔符。

RFC 3339字符串的杀手级特性:(在同一偏移量内)时间顺序等于字典序,因此按字符串排序的日志就是按时间排序的日志。

偏移量不是时区

+09:00 是偏移量——一个固定数字。Asia/Shanghai 是时区——来自IANA tz数据库的一套政治规则,把挂钟时间映射到偏移量,包含所有历史和已排定的夏令时切换。由于各国政府会临时改规则,tz数据库每年更新数次。

这就是未来事件需要时区名而不是偏移量的原因。存「2027-03-10 09:00 Europe/Berlin」,而不是「2027-03-10T09:00+01:00」——如果德国在那之前废除夏令时,存下的偏移量会悄悄把会议钉在错误的钟点上。相反,过去的时刻用纯UTC最安全。

值得写进代码的夏令时边界情况

  • 被跳过的时间: 春季拨快时,本地02:30根本不存在。调度器必须定义策略(通常是向后顺移)。
  • 重复的时间: 秋季拨回时,01:30出现两次;需要偏移量或消歧标志来选定其一。
  • 日期运算: 「加一天」和「加24小时」在切换日是不同的。日历运算必须在事件所属的时区里进行,绝不能在裸epoch上做。

实用存储规则

  • 数据库中的时刻: Postgres的 timestamptz (注意:它存的是UTC并在出入时转换——不会记住原始时区),或epoch毫秒的BIGINT。
  • API和JSON: 带明确 Z 或偏移量的RFC 3339字符串。
  • 未来的排程事件: 挂钟时间加IANA时区名,只在执行时才物化为UTC。
  • 生日等仅日期的值: 纯 YYYY-MM-DD,永远不要是某个时区的午夜。一旦附上时间,格林尼治以西用户的生日就会偏移一天。

JavaScript特有的陷阱

Date 解析有一个规范强制的不一致:new Date('2026-04-30') 被解释为UTC午夜,而带时间但无偏移量的 new Date('2026-04-30T09:00') 被解释为本地时间。加上明确的偏移量,两个问题都消失。渲染时让平台来做本地化工作:

new Intl.DateTimeFormat('zh-CN', {

dateStyle: 'long',

timeStyle: 'short',

timeZone: 'Asia/Shanghai'

}).format(new Date(1745939938123));

Temporal API(Temporal.InstantTemporal.ZonedDateTimeTemporal.PlainDate)终于把时刻/挂钟/仅日期的区分编码进了类型系统;随着运行时逐步支持,它是新代码的正确目标,而date-fns或Luxon是当下务实的替身。

测量时长: 错误的时钟与正确的时钟

挂钟时间戳是用来记录事情何时发生的,不是用来测量花了多久的。系统时钟会在NTP校正时向后回拨、在手动修改时跳变,所以生产环境的追踪里 Date.now() 的差值偶尔会是负数。每个平台都为区间测量提供单调时钟:JavaScript的 performance.now()、Python的 time.monotonic()、POSIX的 CLOCK_MONOTONIC。同样的道理适用于跨机器场景:客户端时钟会漂移几秒到几分钟,所以永远不要拿客户端生成的时间戳和服务端生成的时间戳相减来计算延迟——要么让两者出自同一个时钟,要么在单一观测点测量往返时间。

用 sdk.is/timestamp-converter 的时间戳转换器在epoch、ISO字符串和人类可读格式之间转换。