不喜欢广告? 无广告 今天

时区只是一个谎言(以及在代码中如何处理时区)

更新于

时区看起来只是UTC偏移量。其实不然。本指南详细解释了时区为何会破坏代码——夏令时间隙、半小时偏移、不严谨的日期时间——以及如何在JavaScript、Python、PHP和SQL中正确处理时区问题。

时区是谎言(以及如何在代码中处理它们)1
广告 移除?

你询问了服务器当前时间,它回答是14:00。你将其保存下来,之后又查询了它,现在显示为16:00。你并没有做任何更改。欢迎来到时区的世界。

时区是软件中最令人迷惑的问题之一。表面上看,它们似乎只是UTC偏移——只需加上或减去若干小时。但实际上,它们是政治决策、历史偶然性、半小时偏移以及随通知变化的夏令时规则的复杂混合。本文将解释为何时区如此棘手,并重点说明如何在代码中正确处理它们。

为什么“只使用UTC”只是半个答案

你最常听到的建议是:所有数据都以UTC存储。这个建议是正确的——但并不完整。以UTC存储解决了(一致的存储)问题,却留下了更大的问题:显示和输入。 一个 问题

一位东京的用户在其本地时间安排会议,时间为上午9点。你将其存储为UTC时间。后来,一位纽约的用户打开同一个事件,你将其显示为他们的本地时间。但当用户旅行时,应该以谁的本地时间为准?当他们更新系统时间时?当夏令时生效时?“存储UTC,显示本地”是一项明智的策略——但它 执行 是大多数团队失败的原因。

时区问题的实际难点

在我们探讨解决方案之前,先明确我们真正面对的问题。

1. UTC偏移并非时区

UTC+5:30 它不是“印度时间”。它只是一个静态偏移。印度标准时间是 Asia/Kolkata ——一个使用+5:30且从未实行夏令时的命名时区。这些是不同的概念。如果你硬编码一个偏移,你实际上只是存储了一个数字。如果你存储一个命名时区,你实际上是在存储意图。

命名时区存在于 IANA时区数据库 (也称为tzdata或Olson数据库)。每个主流语言和操作系统都内置了该数据库的副本。使用它。始终优先选择 America/New_York 而非 UTC-5.

2. 夏令时会移动时钟(不可预测地)

夏令时转换会产生两个真正危险的边缘情况:

  • 春季前进的空隙: 时钟从凌晨2点跳到凌晨3点。凌晨2:30的时间实际上不存在。如果你尝试在那一天安排2:30的活动,你可能会得到错误或静默的异常行为,具体取决于你使用的库。
  • 秋季后退的歧义: 时钟从凌晨2点倒退至凌晨1点。凌晨1:30现在出现了两次。 两次。如果没有额外的上下文(折叠标志),你就无法判断你指的是哪一个时间点。

而且,夏令时规则会变化。各国和美国各州在最近的记忆中已经更改、废除或修改了这些规则。你的代码行为取决于是否拥有最新的tzdata包——这是一个运维问题,而不仅仅是开发问题。

3. 并非所有偏移都是整点

印度是UTC+5:30,尼泊尔是UTC+5:45,澳大利亚部分地区是UTC+9:30,伊朗在冬季是UTC+3:30,夏季是UTC+4:30。如果你的系统假设所有时区都是整点,它将默默地破坏数百万用户的时钟时间戳。

4. 服务器上的“本地时间”毫无意义

服务器有系统时钟。这些时钟有一个配置的时区——通常 UTC,有时是托管提供商默认设置的,有时是多年前系统管理员设置的。调用 new Date()datetime.now() 而不指定时区,实际上是在依赖该服务器配置。不同环境会产生不同的结果。这是一个在每次部署中都会出现的错误。

正确的做法,按语言划分

JavaScript / TypeScript

JavaScript中的原生 Date 对象是一个围绕UTC毫秒时间戳的薄包装。它看起来友好,但实际上并非如此。避免手动格式化它——在显示时使用 Intl.DateTimeFormat API,或使用一个库。

现代标准是Temporal ——一个TC39提案,已在Chrome 121中发布,并将推广到所有主要运行环境。它对时区有第一类支持,是长期的正确解决方案:

// Store an instant (UTC-equivalent)
const meeting = Temporal.Instant.from("2025-06-15T14:00:00Z");

// Display in a specific zone
const nyTime = meeting.toZonedDateTimeISO("America/New_York");
console.log(nyTime.toString()); // 2025-06-15T10:00:00-04:00[America/New_York]

// Convert to Tokyo time
const tokyoTime = meeting.toZonedDateTimeISO("Asia/Tokyo");
console.log(tokyoTime.toString()); // 2025-06-16T23:00:00+09:00[Asia/Tokyo]

如果你现在还不能使用Temporal, date-fns-tz 搭配date-fns是一个可靠的选择。 Luxon 是另一个可靠的选择。Moment.js功能齐全但已不再维护——请尽快迁移到其他库。

Python

Python 的 datetime 模块区分了“朴素”时间(无时区信息)和“有意识”时间(带有tzinfo)。朴素时间是陷阱——它们看起来有效,但在系统间传递时毫无意义。

始终使用有意识的时间。使用 zoneinfo 模块 (Python 3.9+)以获得IANA时区支持:

from datetime import datetime
from zoneinfo import ZoneInfo

# Aware datetime — always do this
utc_time = datetime(2025, 6, 15, 14, 0, 0, tzinfo=ZoneInfo("UTC"))

# Convert to New York time
ny_time = utc_time.astimezone(ZoneInfo("America/New_York"))
print(ny_time)  # 2025-06-15 10:00:00-04:00

# Never do this — naive datetime, meaningless
bad = datetime(2025, 6, 15, 14, 0, 0)  # what zone is this?

对于Python 3.8及更早版本,使用 pytz 库——但要小心 pytz‘的localize/normalize API,该API存在陷阱 zoneinfo 避免。

PHP

良好序列化。 DateTimeDateTimeImmutable 类通过 DateTimeZone原生支持IANA时区。优先选择 DateTimeImmutable ——因为它更安全,因为修改操作返回新对象而不是原地修改。

$utc = new DateTimeImmutable('2025-06-15T14:00:00', new DateTimeZone('UTC'));

// Convert to Sydney time
$sydney = $utc->setTimezone(new DateTimeZone('Australia/Sydney'));
echo $sydney->format('Y-m-d H:i:s T'); // 2025-06-16 00:00:00 AEST

// Store timestamps as ISO 8601 strings or Unix timestamps
echo $utc->getTimestamp(); // 1749996000

SQL / 数据库

数据库时区处理是另一个雷区。

  • PostgreSQL: 使用 TIMESTAMPTZ (timestamp with time zone)——它将所有内容存储为UTC,并在输出时进行转换。永远不要使用 TIMESTAMP (无时区)用于面向用户的数据;它会以你提供的内容存储,没有任何转换。
  • MySQL:DATETIME 类型是朴素的(无时区)。如果你想存储UTC,使用 TIMESTAMP ,但请注意其范围限制在2038年。对于新架构,存储为 VARCHAR ISO 8601格式或作为Unix时间戳存储在 BIGINT 通常更安全。
  • SQLite: 没有原生时区类型。存储为ISO 8601文本(2025-06-15T14:00:00Z)或作为Unix整数。转换在应用程序代码中处理。

无论你使用哪种数据库,都应显式设置服务器会话时区,而不是依赖默认值。

实际可行的规则

在深入理论之后,以下是能防止大多数时区错误的实际规则:

  1. 处处存储UTC。 数据库中的每个时间戳都应以UTC存储。不要为“仅用于内部使用”而例外。
  2. 使用IANA时区名称,而不是偏移量。 当需要重建原始本地时间时,存储 America/Chicago 与UTC时间戳一起。仅偏移量在夏令时转换后无法恢复。
  3. 永远不要在没有时区的情况下调用“现在”。 datetime.now(), new Date(), time() ——始终传递一个明确的时区参数,或立即附加一个时区。
  4. 在最后时刻转换为本地时间。 所有日期计算都应在UTC中进行。仅在显示前将时间转换为用户的本地时区。
  5. 保持tzdata更新。 时区规则会变化。将tzdata包的更新纳入你的常规依赖管理流程。
  6. 在夏令时转换日期进行测试。 每年的两个危险日期(春季前进、秋季后退)应包含在你的测试套件中,如果你处理的是日程安排或时间敏感的数据。
  7. 明确向用户请求其时区。 不要依赖IP地理位置或浏览器猜测来处理重要事项。显示一个时区选择器,并将结果存储下来。

关于Unix时间戳的一句话

Unix时间戳——自1970-01-01T00:00:00Z以来的秒数——本质上是时区无关的。它是一种完全有效的存储格式,尤其适用于日志、API和缓存,其中你希望有一个明确的单一数字。

问题在于:Unix时间戳不携带用户的 原始意图。如果有人为“周五上午伦敦时间”预订航班,而你只存储一个Unix时间戳,你就失去了他们原本想表达的伦敦时间。当三个月后重新预订航班,而伦敦进入或退出夏令时时,你可能会显示错误的本地时间。当用户意图重要时,务必在时间戳旁边存储时区信息。

最棘手的情况:周期性事件

周期性事件——每周站会、每月账单周期、每日提醒——会一次性暴露所有时区边缘情况。

考虑一个用户在洛杉矶的“每周一上午9点”规则。你存储的是太平洋时间9点吗?当他们前往东京时会发生什么?当时钟在春季前进且该周一正好落在转换日时会发生什么?

唯一正确的模型是:

  • 将周期性规则存储为墙钟时间 + IANA时区:“每周一 09:00 America/Los_Angeles”
  • 在调度时计算下一个UTC时间点,而不是在规则创建时计算
  • 在任何夏令时转换落在周期窗口内时重新计算

rrule.js (JS) 和 dateutil.rrule (Python) 这样的库在提供正确时区上下文时能正确处理这种情况。手动实现这一逻辑是导致细微错误的可靠路径,这些错误可能在数月后才显现。

想要享受无广告的体验吗? 立即无广告

安装我们的扩展

将 IO 工具添加到您最喜欢的浏览器,以便即时访问和更快地搜索

添加 Chrome 扩展程序 添加 边缘延伸 添加 Firefox 扩展 添加 Opera 扩展

记分板已到达!

记分板 是一种有趣的跟踪您游戏的方式,所有数据都存储在您的浏览器中。更多功能即将推出!

广告 移除?
广告 移除?
广告 移除?

新闻角 包含技术亮点

参与其中

帮助我们继续提供有价值的免费工具

给我买杯咖啡
广告 移除?