2026年8月9日
Unix 时间戳与时区:开发者完全指南
每一行日志、每一条数据库记录、每一个 JWT 的 exp 声明、每一个缓存 TTL,本质上都在编码”某个时刻”。而它们迟早会咬你一口:日志的时间”来自未来”八个小时、定时任务在同一晚跑了两次、用户还在登录态里令牌却已过期。这类 bug 十有八九可以追溯到同一个根源:把一个时刻(instant)和一处墙上时钟的读数混为一谈,或者没想清楚时区转换该由谁负责——你的代码、数据库,还是浏览器。
这篇文章从第一性原理把整个模型讲透:Unix 时间戳数的是什么、UTC 为什么是一套协调规则而不是时区、ISO 8601 怎么拼写一个时刻、夏令时会在哪些地方把朴素的算术写坏,以及跨时区的系统和团队怎么保持清醒。文中出现的每一个换算结果都是真实数值,可以直接用本站的时间戳转换器和世界时钟复核——两个工具都完全在浏览器里计算。
Unix 时间戳到底在数什么#
Unix 时间戳就是一个整数:从 1970-01-01T00:00:00Z 这个被称为”纪元(epoch)“的时刻起,已经流过的秒数。契约就这么多。
由此推出两个性质,它们几乎解释了这个格式的全部魅力:
- 它没有时区。数字
1723455667指代唯一一个时刻,句号。它同一瞬间既是伦敦的 09:41,也是上海的 17:41。时区只在你想让人读懂这个数的时候才出现——把时刻渲染成某个地区的墙上时钟。 - **它没有日历。**没有月份、没有星期几、没有”一天的分界线”。如果你要”周二的所有事件”,你问的是日历问题,必须先把时间戳渲染进某个具体时区的日历,“周二”才有含义。
几个经典参考点,都可以在转换器里验证:
| 时间戳 | UTC 表示 | 备注 |
|---|---|---|
0 | 1970-01-01T00:00:00Z | 纪元本身 |
1000000000 | 2001-09-09T01:46:40Z | 十亿——一个周日 |
1234567890 | 2009-02-13T23:31:30Z | ”顺子”时刻 |
1723455667 | 2024-08-12T09:41:07Z | 下文实例 |
2000000000 | 2033-05-18T03:33:20Z | 二十亿 |
2147483647 | 2038-01-19T03:14:07Z | 32 位天花板 |
最后一行就是著名的2038 年问题:带符号 32 位整数最大只能表示纪元后 2147483647 秒。再过一秒它回绕为负数,在仍以这种方式存时间的系统上会被读成 1901 年 12 月。主流 64 位平台的 time_t 是 64 位,足够用两千九百多亿年;真正的风险藏在老的嵌入式设备、过时的文件格式和把字段定义成 int32 的线上协议里。今天设计二进制结构或数据库表结构,时间戳字段一律给 64 位——或者干脆存 ISO 8601 字符串。
秒还是毫秒?1e12 判别法#
Java(System.currentTimeMillis())、JavaScript(Date.now())和多数日志采集端用毫秒;Unix 工具、Python 的 time.time() 整数部分和多数 API 用秒。两者搞混会得到几万年后的日期——好在错得离谱,反而容易被发现。
一个实用的判别方法,也是本站转换器采用的规则:**绝对值小于 1,000,000,000,000(1e12)按秒处理,否则按毫秒。**十位数的秒级时间戳要到 2286 年才会涨过 1e12,而毫秒值早在 2001 年 9 月就越过了 1e12(回看上表:1000000000 秒是 2001 年,而 1000000000000 毫秒是同一时刻),两个区间在任何现实场景里都不会重叠。试试看:往转换器里粘 1723455667,得到 2024 年 8 月;粘 1723455667000,得到同一时刻——工具自动判别单位。
UTC、GMT、本地时间:把词义捋清楚#
开发者习惯把这几个词混着用,而坑恰好就埋在这里。各自的准确含义:
- UTC(协调世界时)是全球的时间基准:地球上所有时钟共同认定的坐标系。它不是 IANA 意义上的时区——没有哪条夏令时规则挂在它身上。大家说”用 UTC 存储”,真正的意思是”存那个无歧义的时刻,别把任何地区的偏移量焊进去”。
- GMT(格林尼治平均时间)是 UTC 的前身。两者实际相差不到一秒,
GMT+00:00这个标签至今活在格式化输出里(邮件头和Intl的输出中都能看到),作为零偏移的同义词。读没问题,新设计里写它就略显随意了。 - 偏移量(offset)(
+08:00、-05:00)只是一个数字:“这个墙上时钟比 UTC 快/慢 N 分钟”。它不包含任何规则。 - **时区(time zone)**是”地区 + 规则”:什么时间用哪个偏移量、什么时候切换夏令时。时区有 IANA 标识符,如
Asia/Shanghai、America/New_York、Europe/Berlin。一个时区在一年里可能对应不同偏移量;而一个偏移量永远反推不出时区。
一条能挡掉一整类 bug 的实践规则:**存储和传输”时刻”(时间戳或带 UTC 标记的 ISO 8601),只在展示的最后一刻、按观看者的偏好转成时区。**数据库存 UTC;日志存 UTC 或秒级时间戳;API 交换 UTC。用户的浏览器比你的服务器清楚得多用户自己在哪个时区、用哪种语言习惯——把最终渲染留给它。
半小时和 45 分钟的时区#
如果你从小到大只见过整点偏移,世界比你想象的更”碎”。印度(Asia/Kolkata)是 UTC+05:30;尼泊尔(Asia/Kathmandu)是 UTC+05:45。在 1723455667 这一瞬,上海 17:41:07、伦敦 09:41:07,而加尔各答读数是 15:11:07——多出来的半小时明晃晃写在分钟上。任何假设偏移量都是整小时的代码(“本地小时差乘 3600”)会对十几亿人静悄悄地算错时间。用”分钟”做单位的偏移量(世界时钟工具就是这么算的)才是安全表达。
ISO 8601:消灭歧义的字符串格式#
裸整数紧凑但人眼难读,所以 API 之间绝大多数用 ISO 8601 字符串交换时间。值得背下来的语法,仍以 1723455667 为例:
2024-08-12T09:41:07Z UTC,"Z" = Zulu time = 零偏移
2024-08-12T09:41:07+00:00 同一时刻,显式写出偏移
2024-08-12T17:41:07+08:00 同一时刻,上海墙上时钟
2024-08-12T09:41:07.000Z 带毫秒
三件事必须刻进肌肉记忆:
Z是承重墙。2024-08-12T09:41:07Z和2024-08-12T09:41:07(无后缀)是两种东西。前者指认一个全局时刻;后者是一段赤裸的墙上读数——解析器拿它怎么办,各家不同。JavaScript 的Date.parse对带时间分量但无偏移的 ISO 字符串按本地时间处理(而纯日期字符串又按 UTC 处理,这对称性堪称著名陷阱);其他语言和库的默认行为也各奔东西。永远输出偏移量,没有例外。- 偏移量不代表时区。
+08:00同时被上海、新加坡、香港、珀斯、伊尔库茨克使用——这些地区的夏令时历史各不相同。偏移量回答”此刻离 UTC 多远”;时区名回答”规则是什么”。用户配置日历时你要的是时区;序列化一个时刻时偏移量就够。 - **RFC 2822 是它邮件时代的老表亲。**你会在邮件的
Date:头里见到Mon, 12 Aug 2024 09:41:07 +0000。可读性不错,但月份名是英文、格式早于 Unicode——自己设计系统一律优先 ISO 8601。时间戳转换器把两种格式并排展示,对应关系一眼可见。
再给一条格式化提醒:同一时刻在不同 locale 下渲染不同(8 月 12 日可能显示成 08/12/2024 或 12.08.2024)。永远不要回头去重新解析展示用的日期字符串;让机器表示(时间戳或 ISO)做唯一事实源,每次展示都从它格式化。
夏令时:朴素代码的坟场#
夏令时(DST)就是”往明天加 86400 秒”这种写法会出错的根源。春天拨快那晚,时区里只有 23 小时;秋天拨回那晚有 25 小时。用 America/New_York 2024 年的两次切换举两个具体翻车现场(均可用世界时钟工具验证):
- **春拨快,2024-03-10,02:00 → 03:00。**UTC 时刻
2024-03-10T06:59:00Z在纽约是 01:59 EST(偏移 −05:00)。两分钟后,2024-03-10T07:01:00Z在纽约是 03:01 EDT(偏移 −04:00)。墙上时钟从 01:59 直接跳到 03:01——02:00 到 02:59 那天根本不存在。任何落在空洞里的本地时间算术都没有答案,数据库要么报错、要么悄悄往前挪。 - 秋拨回,2024-11-03,02:00 → 01:00。
2024-11-03T05:59:00Z时纽约读 01:59 EDT(−04:00);2024-11-03T06:01:00Z时纽约读 01:01 EST(−05:00)。01:00 到 01:59 在墙上走了两遍。那晚说”01:30 见”是有歧义的——必须选定一个时刻。这正是按墙上分钟走字的调度器会把小时任务跑两遍的原因。
在夏令时下活下来的职业习惯:
- 算术在 UTC 或时间戳空间里做,做完再渲染给人看。“现在 + 90 分钟”在时间戳空间永远正确,不管窗外有没有夏令时。
- “加一天”是例外。对人相关的语义——“明天同一时间”——你往往想要墙上时钟行为(自然包含 23 或 25 小时的天)。成熟的库把这两种语义分开表达:日历级算术(“在时区 X 加 1 天”)与时长算术(“加 86400 秒”)。按需选择,别靠巧合。
- **永远不要从偏移量反推时区。**夏天的
America/New_York和别的时区共享偏移量;存下来的−04:00对十二月只字未提。未来还要做换算的场景,存 IANA 时区 ID。 - **当心时区规则在脚下变化。**政府会修改夏令时法令;tzdata 冻结的老系统会永远错下去。保持 tzdata(操作系统的或 ICU 的)更新——浏览器随版本更新规则,世界时钟工具通过
Intl实时读取它们。
还有一个能绊倒资深工程师的坑:Asia/Shanghai 今天是 UTC+8 且无夏令时,所以国内部署常把 +08:00 写死。可这段代码一旦服务 Australia/Sydney(标准 UTC+10、夏令 UTC+11)的用户——下文实例里八月悉尼比东京靠前、二月又比东京靠后——写死的偏移量就会在一年里的某几个月产生整整一小时的排班漂移。
实例演算:一个时刻、五座城市#
把一个时间戳从头到尾走一遍,用的正是本站工具的流程。从日志里取出 1723455667——十位数,按 1e12 判别法是秒。
- 粘进时间戳转换器。它解析出时刻
2024-08-12T09:41:07Z,并一次给出全部表示:ISO 8601(2024-08-12T09:41:07.000Z)、RFC 2822(Mon, 12 Aug 2024 09:41:07 +0000)、Unix 秒与毫秒(1723455667/1723455667000)、UTC 渲染、你本机时区的本地渲染,以及一句相对时间(截至写作时是”一年多以前”)。 - 注意它在 UTC 里是星期一。这个日历事实在裸整数里不可见,而且跨时区就会变:
2024-08-12T01:00:00Z时上海已是周一,纽约却还是周日晚上。“按天分组”若不先钉死时区,事件会被撒进两个桶。 - 再打开世界时钟,切到参考时间模式,输入
2024-08-12T09:00并把上海设为锚定时区。每一行都把同一时刻渲染进自己的时区(八月 = 北半球夏天 / 南半球冬天):
Asia/Shanghai 周一 09:00 UTC+08:00
Asia/Tokyo 周一 10:00 UTC+09:00
Australia/Sydney 周一 11:00 UTC+10:00 (南半球冬季标准时间)
Europe/Berlin 周一 03:00 UTC+02:00 (夏令时 CEST)
Europe/London 周一 02:00 UTC+01:00 (夏令时 BST)
Asia/Kolkata 周一 06:30 UTC+05:30 (半小时时区)
America/New_York 周日 21:00 UTC−04:00 (前一天!)
最后一行请读两遍:上海一个再正常不过的”周一早晨站会”,在纽约是周日夜里。世界时钟工具存在的全部意义就在这里——它把”这个时间对他们来说是几点”变成一眼可查,还带各时区的工作时间徽标,纽约那一行立刻能看出不在 09:00–18:00 区间内。
服务端、数据库与 API 的实践默认值#
压缩成今天就能落地的规则:
- 存储用秒/毫秒时间戳或带显式偏移的 ISO 8601(
Z)。字段一律 64 位,绝不 int32。数据库有时间戳类型就保持存 UTC。 - 传输用始终带偏移量的 ISO 8601。如果对端需要感知时区的值(日历事件),把 UTC 时刻和 IANA 时区 ID 一起发过去,对方的渲染器才能正确解析。
- 渲染放在边缘,按用户的时区和 locale,从存储的时刻出发。在 JavaScript 里就是
Intl.DateTimeFormat加timeZone——本站两个工具用的同一套引擎能力——而不是手搓偏移量算术。 - 测试要用夏令时边缘日期:切换的那个周日、本地 02:00 两侧各取一个点,再加一个半小时时区(
Asia/Kolkata)和一个南半球时区(Australia/Sydney)。调度逻辑能扛住这组日期,就扛得住整年日历。 - **定时任务(cron)**要格外小心:
0 9 * * 1-5的含义是”系统所在时区的 09:00”——显式指定时区,别继承容器的默认值,并确认你的 cron 实现在夏令时空洞处的行为(跳过、跑两次、还是延迟)。cron 表达式查看器能列出接下来的触发时间,信任之前先核一遍。
常见问题#
为什么 new Date("2024-08-12") 按 UTC 解析,new Date("2024-08-12T09:41") 却按本地时间?#
ECMAScript 规范规定:纯日期的 ISO 字符串按 UTC;带时间但无偏移量的字符串按本地时间。这是写进规范的”不对称”,也是”差一天” bug 的著名产地。解法永远一样:要么带上偏移量(2024-08-12T09:41:07Z),要么用显式数值构造日期。
时间戳存整数还是 ISO 字符串?#
两种都对,前提是处理得当。整数紧凑、索引友好、可直接比较、便于单测——但必须白纸黑字固定单位(秒还是毫秒)。ISO 8601 字符串人类可读、日志里可直接排查、自带偏移量信息,代价是体积和解析纪律。选哪种都行,但整个系统必须统一;混用存储正是”差 1000 倍”错误的源头。
“几分钟前”这种相对时间怎么算才对?#
用当前时刻减去目标时刻得到秒差,再换算成最大的合适单位(“5 分钟前""2 小时前”),并按观看者 locale 格式化——每种语言的措辞都不一样。本站时间戳转换器就是用 Intl.RelativeTimeFormat 做的,粘任意时间戳即可在状态行看到效果。千万不要在服务端算好相对时间再下发:你计算时用的”现在”立刻就会过期。
UTC 和 GMT 是一回事吗?“Z” 又是什么?#
日常编程层面可以画等号——GMT 与 UTC 相差不到一秒,格式化输出里经常写着 GMT 而底下的值是 UTC。ISO 8601 里的 Z 后缀表示”零偏移”(历史上叫”Zulu 时间”)。新系统行文用 UTC,字符串里输出 Z 或 +00:00。
我的日志时间恰好差了 8 小时(或 5 小时、9 小时),怎么回事?#
期望与现实之间差一个固定的整小时数,几乎必然是偏移量被应用了两次(或一次都没应用):服务器把本地时间当 UTC 存了,或者展示层给一个已经带时区的值又套了一层观看者时区。找到值被转换的那条边界,让恰好一层负责时区转换——展示边缘那一层。
总结与工具#
整个心智模型三句话讲完:Unix 时间戳是从 1970 年起数的秒数,无关时区、无关日历。UTC 是坐标系,时区是把时刻映射到墙上时钟的地区规则集,一年之内映射还会变。存储和计算用时刻,渲染用观看者的时区,对墙上时钟字符串做的一切算术都要起疑。
来本站动手练一遍:
- 时间戳转换器——粘入秒、毫秒或 ISO 字符串,一次得到 ISO 8601、RFC 2822、UTC、本地时间和相对时间,自动判别单位。
- 世界时钟——钉住一个参考时间,对比任意一组时区,附 UTC 偏移与工作时间徽标,排会议直接用。
- cron 表达式查看器——上线前核一遍定时表达式的真实含义和接下来几次触发时间。