在程序员的世界里,有一类问题被公认为“看似三行代码、实则葬送职业生涯”:日期与时间的处理。Stack Overflow 联合创始人 Jeff Atwood 曾在博客里感叹,几乎所有编程语言的历史都伴随着一套糟糕的日期时间 API;而把这件事讲得最透彻、传播最广的,是一段不到十分钟的视频——Tom Scott 在 Computerphile 频道客座主讲的《The Problem with Time & Timezones》(时间与时区的问题)。
这条视频2013年底上线,十余年间被全球程序员反复搬运,长期位列 Computerphile 播放量最高的节目之列,是无数团队新人入职时会被老手甩过来的“必看链接”。它火的原因很朴素:它不是教你怎么写日期代码,而是先让你承认——你根本不理解时间。挂钟上的“下午两点半”和物理意义上的“那一刻”是两回事;把两者混着存进数据库,就是十年后凌晨三点被告警叫醒的开始。
本文按视频的叙事顺序拆解其论证,补充它背后的标准、数据库与真实事故链条,再加上一层这条视频不会讲、但对通信行业读者格外切身的东西——当时间是物理层硬需求时,工程界怎么解决。最后给出三张可以直接抄进代码评审清单的操作建议。
| 项目 | 内容 |
|---|---|
| 视频标题 | The Problem with Time & Timezones(时间与时区的问题) |
| 频道 | Computerphile(英国诺丁汉大学背景的计算机科普频道,订阅量为千万级) |
| 主讲 | Tom Scott(英国科普创作者,以讲述地理、语言与工程冷知识闻名,此片为客座录制) |
| 发布时间 | 2013年12月(据频道页面) |
| 播放量 | 数百万级,长期为该频道头部内容(精确数字以YouTube页面为准,本轮无法实时核实) |
| 时长 | 约10分钟 |
| 核心主题 | 为什么日期时间是编程中的“天坑”,以及存取时间的正确姿势 |
视频从一个最常见的开发需求讲起:显示用户所在时区的当前时间。听起来简单——取当前时间,加一个偏移量,完事。Tom Scott 开场就把这个念头掐死:如果你要写日期时间相关代码,接下来踩的坑是每一代程序员都踩过的。他要论证的命题是:时间的复杂性不在代码,而在人类社会本身。
全片的论证基石,是区分两种“时间”:
混乱之源在于:把挂钟时间当时刻存进数据库。一旦存了“2026-03-29 02:30”这种本地时间,你就同时欠下了两笔债——它属于哪个时区?那个时区当时的规则是什么?Tom Scott 给出的核心结论浓缩成一句话就是:存储一律用 UTC(协调世界时),只在展示给用户的那一毫秒才转换成本地时间。
这是全片最精彩的部分,论证层层递进:
第一层:时区根本不是“整齐的24个”。 时区边界跟着行政区划走而非经线,一些地区偏移量甚至是半小时或45分钟的倍数(如印度UTC+5:30、尼泊尔UTC+5:45)。你无法用简单的经度公式算出时区,因为那是立法问题,不是地理问题。
第二层:夏令时让挂钟一年“跳”两次。 春季切换时,凌晨从2:00直接跳到3:00——2:30这个时间压根不存在;秋季切换时,2:30会出现两次——同一个本地时间字符串对应两个不同的物理时刻。仅此一条,“把本地时间当唯一键”“对本地时间排序”“计算本地时间差”这些常见操作就全部失效。
第三层:规则还会随历史改写。 这是视频里最让人脊背发凉的案例:英国在1968年到1971年间搞过“英国标准时间”实验,全年不回拨、常年比格林尼治时间快一小时。也就是说,一个1970年出生的英国人的“出生时刻”的本地表达方式,取决于那一年政策处在实验期还是恢复期。Tom Scott 以此说明:历史日期计算需要一部完整的“当年钟表如何调整”的规则史,任何拍脑袋的偏移量公式都会算错。类似的现实极端案例还有:萨摩亚在2011年为更换时区直接跳过了12月30日一整天——那一天在萨摩亚的日历上从未存在。
论证到这里水到渠成:既然时区规则是一部不断增补修订的“法典”,就不该由程序员在代码里硬编码。正确做法是:数据库存UTC或Unix时间戳;使用 IANA 时区数据库(即社区熟知的 tzdata/Olson 数据库)这类持续有人维护的规则库;用成熟的语言级 API 做转换。Tom Scott 留下的著名忠告大意是:别发明你自己的时间数学——你想到的每一种简化,都已经在某个国家、某个年代被现实证伪过。
视频本身没有引用文献,但它串联的每一个环节都有扎实的工程史:
Z 结尾表示 UTC,其来历是航海与航空通信中用音标字母称呼零时区为"Zulu"。这是“时刻唯一化”在表示法上的落点。java.util.Date/Calendar 是著名灾难,社区催生 Joda-Time(Stephen Colebourne 主导),最终反哺为官方的 JSR-310(java.time,JDK 8 起内置);Python 从 pytz 的种种反直觉走到 3.9 版本按 PEP 615 内置 zoneinfo;JavaScript 的 Date 被诟病二十年后,TC39 的 Temporal 提案已打磨多年,其核心设计正是“时刻(Instant)”与“挂钟时间(ZonedDateTime)”分离。真实事故对账单(视频论点的现实注脚):2012年2月29日,微软 Windows Azure 全球性宕机约12小时,官方事后报告确认根因是证书有效期计算——有效期一年,从2月29日加一年得到不存在的“次年2月29日”,证书创建失败引发连锁故障;2011年元旦前后,iPhone 闹钟在欧洲因夏令时规则处理错误集体失灵(据当时媒体广泛报道);32位 time_t 将在2038年1月19日溢出的“Y2K38问题”,至今仍是嵌入式领域的定时炸弹。
一般编程解读到上面为止。但这里补一层视频不会讲、却与“51学通信”读者最相关的东西:在电信网络里,时间同步不是应用层便利,而是物理层生死线。
3GPP 对 TDD 制式基站的空口相位同步有微秒级硬性要求(典型值为±1.5微秒,见 3GPP TS 38.104 及 ITU-T G.8271.1 定义的网络同步架构)——TDD上下行靠时间切分,邻区之间差几微秒就会交叉时隙互相干扰,网络直接掉话。为此运营商网络里跑着一整套独立于 NTP 的时间分配体系:GPS 授时(注意 GPS 时间不含闰秒,与 UTC 之间有一个累计秒差,近年为18秒)、地面 PTP(IEEE 1588v2,电信档 G.8275.1)逐跳补偿。闰秒发生的那一秒,恰好是电信网管最紧张的23小时59分59秒之后——UTC 会“凭空多出一秒”,一些老旧系统当场卡死或日志乱序,这正是云厂商宁可“抹平”也不硬跳的原因。
看懂这层,你就明白视频里“存UTC、显示才转换”的建议背后有一个更深的世界观:时刻属于物理学,挂钟属于政治,而工程学的任务,是在两者之间修一条不塌方的隧道。通信业花了几十年和几个标准组织(ITU-T、3GPP、IEEE)修这条隧道,程序员的 tzdata 和 Temporal,是同一条隧道的软件段。
诚实地说,以2026年的眼光回看,这条2013年的视频有一处已被社区反思超越的重要简化:
“一律存UTC”会丢信息。 对“记录过去发生了什么”,存UTC完美成立;但对“预约未来”类业务(日程、账单、票务),存UTC反而是错的——正确做法是存“当地时间+时区标识”,展示时再按届时的时区规则转换。原因在于时区规则会变:假设某国立法取消夏令时,你提前存下的UTC时刻会整体错位一小时,而存“当地挂钟时间+时区ID”的系统则自动跟上新规。这正是 Temporal API 把 Instant 与 ZonedDateTime 设计成两种不同类型的原因,也是近十年"Store UTC"教条被细化成“存时刻还是存日历语义,取决于业务”的行业共识(此讨论广泛见于社区博客与语言设计文档)。
“用现成库”需要加上限定词:用设计良好的现代时间库。java.util.Date 和 JS 的 Date 也曾是“现成库”,照样是坑。真正的教益不是“用库”,而是“用承认上述复杂性、并随 tzdata 一起更新的库”。
闰秒问题视频未展开,而它有了新进展:国际计量大会(CGPM)2022年已通过决议,计划在2035年前废除闰秒机制,业界将改用其他方式吸收地球自转偏差(据该决议,实施细节仍在讨论)。Tom Scott 告诉你的“别自己算时间”,在大约十年后依然成立,且理由更充分了。
至于播放量、订阅数等动态数据,本轮无法实时核实,均以频道页面为准。
1. 把“时间高危点”写进代码评审清单。 任何 date + 1 year、明天0点、本月末 运算都是高危代码;数据库列一律用带时区的 timestamptz 或 UTC;日志和接口里的时间字符串必须带偏移量(ISO 8601 的 Z 或 ±hh:mm),不带偏移的时间字符串等于没写完。
2. 按业务语义选择存储模型。 记录历史→存UTC;预约未来→存“当地时间+时区ID”。测试用例必须包含:夏令时切换日、2月29日、月底最后一天、闰秒邻近时刻、以及历史日期(例如1968–1971年的英国、2011年的萨摩亚)。这五个场景抓得住绝大多数时间bug。
3. 把 tzdata 当成“会过期的数据”来运维。 时区规则库需要随系统更新——这不是可选项。电信网管领域的教训值得每个行业记取:网元许可证、计费账期如果建立在过期的时区规则上,出错的不只是显示,还有钱。
flowchart TD
A["需求:显示/存储用户时间"] --> B{"存的是什么?"}
B -->|"本地挂钟时间"| C["债务1:不知道属于哪个时区"]
B -->|"本地挂钟时间"| D["债务2:不知道当时规则"]
C --> E["夏令时:2:30出现两次或不存在"]
D --> F["历史规则:1968-71英国实验<br/>萨摩亚跳过一整天"]
E --> G["排序/计算/去重全部失效"]
F --> G
G --> H["真实事故:Azure闰日宕机<br/>iPhone闹钟失灵/Y2K38"]
H --> I["结论1:存储一律用UTC"]
H --> J["结论2:用IANA tzdata等<br/>持续维护的规则库"]
I --> K["遗留缺口:未来日程存UTC会错位<br/>(时区规则会变)"]
J --> L["现代方案:java.time / zoneinfo<br/>/ JS Temporal(Instant与ZonedDateTime分离)"]
K --> L
L --> M["世界观:时刻属物理,挂钟属政治<br/>工程学负责在两者间修隧道"]
M --> N["电信侧同构:GPS/PTP/闰秒平滑<br/>±1.5微秒的空口同步硬需求"]
说明:微软 Azure 2012年闰日宕机报告、谷歌闰秒平滑博客、CGPM 2022闰秒决议等事件依据公开报道与官方文件转述,原始链接因本轮无法联网检索而未逐一附上,读者可按名称检索原文。 本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
