Skip to content

时间戳换算出来差了 8 小时,问题出在哪 ​

时间戳出错的形态相当固定:要么差 8 小时,要么差 1000 倍,要么直接跳到 1970 年。三种症状对应三个不同的原因,认出来就好办了。

「时间戳转换」工具页初始界面:时间值 / 输入 / 参数配置 / 展示时区 / 本地时间 / UTC / 北京时间 / 实时预览;页头标签「浏览器本地执行 · 免费」

打开「时间戳转换」看到的初始界面。在浏览器本地完成、数据不离开浏览器,免费。

时间戳本身不含时区 ​

这是理解所有时间戳问题的前提。

Unix 时间戳是「从 1970-01-01 00:00:00 UTC 起经过的秒数」,全世界同一时刻的时间戳完全相同。北京的中午和伦敦的凌晨四点,如果是同一个物理瞬间,时间戳一模一样。

所以时间戳没有「北京时间的时间戳」这种东西。时区只在两个地方起作用:把时间戳显示成人能读的日期时,以及把人写的日期解析成时间戳时。

差 8 小时的问题,一定出在这两个环节,不在时间戳本身。

差 8 小时:显示环节 ​

最常见的场景:数据库里存的是 UTC,程序按本地时区显示,或者反过来。

排查顺序是固定的:

  1. 先确认这个时间戳对应的 UTC 时间是多少;
  2. 再确认它应该按哪个时区显示;
  3. 最后看代码里的格式化函数用的是哪个时区。

一个实用的判据:如果偏差恰好是 8 小时的整数倍,几乎必然是时区问题,不用怀疑数据本身。中国全境统一使用 UTC+8,没有夏令时,所以偏差总是整 8 小时,不会出现 7 小时或 9 小时这种夏令时特有的情况。

跨国系统就没这么幸运了:涉及有夏令时的地区,同一个地点的偏移量一年里会变两次。

差 1000 倍:秒和毫秒 ​

第二类错误更容易一眼认出来:日期跑到了 1970 年,或者跑到了公元五万年。

  • Unix 标准时间戳是秒,10 位数字(当前约 17 亿);
  • JavaScript 的 Date.now()、Java 的 currentTimeMillis() 返回毫秒,13 位数字。

把毫秒当秒解析,日期会跑到遥远的未来;把秒当毫秒解析,日期会落在 1970 年 1 月的某一天。

判据比时区问题还简单:数一下位数。10 位是秒,13 位是毫秒。看到 16 位那是微秒(某些数据库和链路追踪系统在用),19 位是纳秒。

跳到 1970 年的另一种原因 ​

如果时间戳是 0 或者空值,格式化出来就是 1970-01-01。这通常意味着上游没赋值,而不是时间算错了。

看到一批数据整齐地显示 1970-01-01 08:00:00(注意这个 08:00:00,正是 UTC 0 点按 UTC+8 显示的结果),基本可以判定是字段没填,顺带还印证了显示端确实按东八区在格式化。

2038 年问题 ​

用 32 位有符号整数存秒级时间戳的系统,会在 2038 年 1 月 19 日 溢出,时间跳回 1901 年。

这不是遥远的传说:一些嵌入式设备、老数据库字段、以及用 int 而非 bigint 存时间戳的表都会中招。如果你的系统需要处理 2038 年之后的日期(比如 20 年期的合同、长期贷款到期日),现在就值得检查一下存储类型。

存时间该存什么 ​

由上面几类问题可以倒推出一条实践原则:存储用时间戳或 UTC,显示时才转本地时区。

把「2026-09-05 14:30」这样不带时区的字符串直接存进数据库,是所有时间戳事故的源头——过几个月没人说得清它当初是按哪个时区写进去的。同一张表里如果有两个服务写入,各自的时区设置又不同,数据就彻底混了,而且无法事后修复。

带时区信息的写法(2026-09-05T14:30:00+08:00)或纯时间戳都没有这个问题:它们唯一确定一个瞬间。

顺带一提,跨时区业务里还有一类不该用时间戳的场景:生日、纪念日、法定假日这类「日期」而非「瞬间」的东西。用户的生日是 9 月 5 日,不管他人在哪个时区都是 9 月 5 日;存成时间戳再按时区渲染,就会出现有些用户看到日期差一天的诡异现象。这类数据应该按纯日期存。

换算 ​

时间戳转换 支持秒与毫秒双向互转,会同时显示 UTC 与本地时间——把两个时区并排放出来,是因为绝大多数时间戳纠纷,看一眼这两个数就能定位在哪一端出的问题。

时间戳只是一个数字。它记录的是「哪一个瞬间」,不记录「在哪里看这个瞬间」——后者永远由显示端决定。

最后更新:2026-09-05