杂记 › 技术笔记 › Unix时间戳对不上日期?10位秒级、13位毫秒级与时区差一次讲清

Unix时间戳对不上日期?10位秒级、13位毫秒级与时区差一次讲清

凌晨一点二十,群消息把我叫醒:运营贴了张后台截图,三月那批订单的"创建时间"整列变成了 1970-01-21 00:13:20。值班的前端爬起来看代码,五分钟就定位到了——接口返回的 create_time 是 10 位秒数,而他写的 new Date(create_time) 把这个数字当成毫秒。同一个 1700000000,按秒读落在 2023 年 11 月,按毫秒读就掉回 1970 年 1 月。那次之后,我们组把时间戳相关的约定写成一张卡片钉在 wiki 首页,新人接手排障基本不用再问人。

一、位数就是精度:先学会一眼判断单位

时间戳本身只是一个整数,含义完全取决于它被按什么单位生产出来。我的第一判断依据永远是数位数,而不是猜后端用了哪门语言:

位数单位典型来源与秒级的关系
10 位秒PHP time()、Go Unix()、Linux date +%s基准
13 位毫秒JavaScript Date.now()、Java currentTimeMillis()秒 × 1000
16 位微秒Go UnixMicro()、Python time_ns()//1000秒 × 1000000
19 位纳秒Python time_ns()、Go UnixNano()秒 × 10⁹

再补一条值域检查:2020-01-01 到 2030-01-01 之间,秒级落在 1577836800 到 1893456000,毫秒级是 1577836800000 到 1893456000000。把 13 位当 10 位解析,日期会飞到公元五万年附近;反过来把 10 位当 13 位,就掉回 1970 年。我遇到的"时间显示异常"基本都落在这两种。

二、差的 8 小时不在时间戳里,在读它的那一层

时间戳是从 1970-01-01 00:00:00 UTC 起算的绝对量,不带任何时区信息。同一个数字在伦敦还是上海都是同一个值,分歧只发生在"把整数翻译成人能读的日期"这一步。以 1700000000 为例,它的 UTC 时刻是 2023-11-14 22:13:20,换成北京时间是 2023-11-15 06:13:20,正好差 8 小时。

实际项目里造成 8 小时错觉的常见四个位置,值得挨个排除:

  • 容器默认时区是 UTC、宿主机是 Asia/Shanghai,日志墙钟时间和业务看到的差 8 小时。TZ=Asia/Shanghai date -d @1700000000 和不带 TZ 各跑一次,就能确认错在哪一层。
  • MySQL 的 FROM_UNIXTIME() 按会话时区输出。先 SELECT @@session.time_zone,再和 UNIX_TIMESTAMP() 双向核对,字段本身往往没错。
  • PHP 没配 date_default_timezone 时按 UTC 走,date() 输出整体平移。
  • 前端 new Date(ts).toISOString() 永远返回 UTC 且末尾带 Z,toLocaleString() 按系统时区渲染。同一份数据在两个函数里"不一致",是调试里最常见的自我怀疑。

三、我现在的排查顺序,以及在哪一步用上工具

  1. 先数位数定单位,写下"这批数字按秒还是按毫秒"的结论,避免后面两个人各按一种理解算。
  2. 再定基准:我们最后把规矩压成一句——库里只存 10 位 UTC 秒,接口字段名后缀写清单位(create_time_ms),展示层负责转本地时区。
  3. 双向验一次:拿一个已知时刻(比如"昨晚 23:07")转成时间戳,再转回来,看是否落回同一个值。这一步能同时暴露单位和时区两处错。
  4. 批量核对时,我把可疑数字贴进时间戳转换,它会自己分辨秒级还是毫秒,同时给出北京时间、UTC、ISO 8601 和"几天前"这类相对描述,省掉我心算时区偏移;日期反推时间戳也在同一页做,页面上还备了几种语言取时间戳的写法,换语言时直接抄那一段。
  5. 要算两个时间点的间隔,或推"往后 90 天是哪天",用时间加减计算,结果连星期几和时间戳一起给出,比手动除以 86400 可靠。

四、容易忽略的边界情况

这几条踩过之后才会主动检查,写在这里当清单用:

边界具体表现处理方式
32 位溢出有符号 32 位存秒级,到 2038-01-19 03:14:07 UTC 翻负;MySQL TIMESTAMP 字段上限也是这个时刻字段改 BIGINT,迁移前先用 SELECT MAX() 看余量
负数时间戳1970 年之前是负值,部分数据库函数和图表库显示成空或直接报错历史档案用日期字符串存,时间戳只留给近几十年业务
闰秒时间戳不表达闰秒,对齐 UTC 秒的调度器在闰秒当天可能看到重复的一秒关键计时走单调时钟(CLOCK_MONOTONIC),别拿墙上时间做倒计时
夏令时美国 2024-03-10 02:00 直接跳到 03:00,这一小时的本地时间不存在;11-03 回拨又重复一小时存 IANA 时区名(如 America/New_York),别只存 UTC+4 这种固定偏移
同秒并发拿时间戳当文件名或缓存 Key,同一秒内的请求会互相覆盖毫秒级再拼随机后缀,或用 UUID

跨国排会议是另一类需求:真正要的往往不是时间戳,而是"对方现在算不算工作时间"。这种我会先看世界时间里两城的当前时刻和时差,定下来再把时刻转成时间戳写进日程接口。

五、常见疑问

Q:转换出来差 8 小时,是时间戳错了吗?

几乎不是。先确认解析用的时区,再确认位数单位;时间戳数值本身没变,变的只是渲染它的这一层。

Q:JavaScript 里到底该乘 1000 还是除 1000?

看输入位数。JS 的 Date 只认毫秒,拿到 10 位要乘 1000;反过来把 Date.now() 发给一个按秒解析的接口,就要除 1000 并取整。最稳的办法是在字段名里写清单位。

Q:怎么快速判断一批混乱的日志时间是秒还是毫秒?

取最大值数位数,再用值域对照:2020 到 2030 年之间,10 位是秒、13 位是毫秒。同一份日志里两种混用并不罕见,先分桶再统一转换。

Q:2038 问题离我们远吗?

只要还有 32 位整型字段在跑老系统就不算远。成本主要在改字段类型和重建索引,越晚动越麻烦,最好在下一轮表结构调整里顺手做掉。

回头看那次故障,真正的修复不是那一行 new Date(),而是"谁生产数字谁标注单位、谁展示时间谁负责时区"这条约定终于落进了接口文档。以后再看到 1970 或者公元五万年,先数位数,再看时区,两件事按顺序查完,绝大多数时间错乱都到不了第二步。

📎 相关文章

PNG 压了三遍只减 426 字节:无损压缩的真实边界 2026-10-22 WebP 转 PNG 后体积涨了几倍:什么时候必须转,怎么把涨幅压住 2026-10-22 截图缩到四分之一,小字糊成一团的三种原因 2026-10-22 同样裁 300×300,头像为什么总偏右下角半格 2026-10-22 按每月 1.7 倍预估 18 个月,脚本和表格为什么给出两个数 2026-10-22