← 返回博客首页

时间戳转换原理与常见坑:Unix 秒、毫秒与本地时区

什么是 Unix 时间戳?

Unix 时间戳(Unix Timestamp)是从 1970-01-01 00:00:00 UTC(称为 epoch)起经过的秒数(或毫秒数)。它是一个与时区无关的绝对时刻——无论你在北京、伦敦还是纽约,同一瞬间的时间戳数值完全相同。

这种「用一个整数表示时刻」的设计,让时间可以像数字一样被存储、比较、做差值,而不必关心人类日历的复杂规则。

三个容易被忽略的细节:

  • epoch 是人为选的。1970-01-01 UTC 只是早期 Unix 团队拍定的起点,换任何一天都不影响机制。
  • 它是有符号的。1970 年之前的时间戳是负数,比如 1969-12-31 23:59:59 UTC = -1。处理生日、历史档案这类数据时会遇到。
  • 它是整数,不是浮点。用浮点存时间戳会引入精度误差,比较和排序都可能出错。

精度:不只有秒和毫秒

实际开发中最常踩的坑是单位混用。四种精度都在生产环境真实存在:

精度 位数 示例 常见来源
10 位 1700000000 Linux date +%s、MySQL UNIX_TIMESTAMP()、多数后端 API
毫秒 13 位 1700000000000 JavaScript Date.now()、Java System.currentTimeMillis()
微秒 16 位 1700000000000000 Python time.time_ns()//1000、MySQL DATETIME(6)
纳秒 19 位 1700000000000000000 Go time.Now().UnixNano()、Python time.time_ns()

快速判断表

位数 精度 对应年份量级
10 2026 年前后
13 毫秒 2026 年前后
16 微秒 2026 年前后
19 纳秒 2026 年前后

关键规则:13 位值 ÷ 1000 ≈ 10 位值。如果前端拿到的 13 位直接当秒存进数据库,时间会错误地跳到几万年后。转换工具应能自动识别精度(看位数即可判断),避免手动算错——时间戳转换工具 就是按位数自动判定并同时给出各时区结果。

⚠️ 位数判断在两种边界会失效:一是 2001-09-09 前后(秒级正好 10 亿,10 位→仍 10 位,无歧义,此规则安全);二是未来纳秒值已达 19 位、秒级仍是 10 位不变。真正危险的是把「微秒 16 位」误判成「毫秒」,差 1000 倍。跨系统传输时在字段名里写死单位created_at_ms)比自动推断更可靠。

时区:UTC、GMT 与偏移

时间戳是 UTC 绝对时刻。当我们在页面上看到「2023-11-15 06:13:20」,那是把 UTC 时刻按当前时区渲染后的结果。

const ts = 1700000000          // 秒级
const d = new Date(ts * 1000)  // 注意 ×1000 转毫秒
console.log(d.toString())      // 本地时区:Wed Nov 15 2023 06:13:20 GMT+0800
console.log(d.toISOString())   // UTC:2023-11-14T22:13:20.000Z

三个常被混用的概念:

概念 含义 用法
UTC 协调世界时,原子钟基准 存储与计算的唯一基准
GMT 格林尼治平均时,天文基准 日常口语中与 UTC 混用,技术文档应用 UTC
偏移(offset) 与 UTC 的小时差,如 +08:00 只描述某一时刻的差值,不是时区

偏移 ≠ 时区,这是最隐蔽的错误来源。+08:00 只说明此刻比 UTC 快 8 小时;中国全年是 +08:00,但伦敦冬天 +00:00、夏天 +01:00。要正确表达「用户所在地」,应该用 IANA 时区标识符Asia/ShanghaiEurope/London),而不是偏移量——IANA 时区数据库(tzdata)里带了历年规则变更的历史,偏移没有。

需要按地区换算时用 时区转换器,它按 IANA 数据库处理,能正确处理夏令时切换。

永远用 UTC 存储与传输,只在展示层按用户时区格式化。这是跨时区系统不踩坑的唯一铁律。

夏令时(DST)陷阱

实行夏令时的地区,一年中有两天会出现「不存在的小时」或「重复的小时」:

场景 发生时刻 现象 后果
春季跳变 本地 02:00 → 03:00 02:00–02:59 不存在 定时任务在这一小时静默跳过
秋季回拨 本地 03:00 → 02:00 02:00–02:59 出现两次 同一本地时间对应两个时刻,日志、去重、聚合都会错

基于本地日历的计算(如「加 24 小时」「明天同一时刻」)在 DST 切换日会偏差 1 小时。正确做法是:在时间戳(UTC 整数)层面做算术,再格式化展示,绝不在本地墙上时间上直接加减。

需要算两个日期之间隔了多少天、或加上 N 个工作日时,用 日期计算器 比手算安全;要写定时规则则用 Cron 表达式解析 确认实际触发时刻——注意 Cron 走的是服务器本地时区,容器里默认是 UTC。

闰秒

Unix 时间按固定每天 86400 秒线性递增,刻意不感知闰秒。闰秒由操作系统/NTP 在底层做「闰秒补偿」(通常是 smear,把 1 秒摊到前后若干小时),对绝大多数业务透明。只有金融撮合、卫星授时等极端精度场景需要单独处理。

存储与传输怎么选

方案 优点 缺点 适用
整数时间戳(秒/毫秒) 跨语言一致、比较快、无时区歧义 人不可读、数据库难按日期聚合 日志、排序、跨系统传输
ISO 8601 带偏移 可读、可字典序排序、自解释 字符串较长、索引体积大 API 响应、配置文件
数据库原生时间类型 可按日期分组、索引友好、有函数支持 各库语义不同,需确认是否带时区 需要时间维度聚合的业务表
本地时间字符串 无时区、不可比较、不可排序 ❌ 任何场景都不该用

各数据库的差异(容易踩)

数据库 类型 是否带时区 注意
PostgreSQL timestamptz ✅ 存 UTC,按会话时区展示 首选;timestamp 不带时区,别用错
MySQL DATETIME ❌ 按字面存 适合存「本地墙上时间」;跨时区需自己约定为 UTC
MySQL TIMESTAMP ✅ 存 UTC,按会话时区展示 范围只到 2038-01-19,且有 1970 下限
SQLite 无原生类型 通常存整数时间戳或 ISO 8601 文本
MongoDB Date ✅ 内部是 UTC 毫秒 驱动层自动转换

MySQL 的 TIMESTAMP 撞上 2038 上限是真实事故来源——新表建议用 DATETIME 并存 UTC,或直接用 BIGINT

各语言的正确转换

JavaScript(毫秒 → 可读,注意单位):

const sec = 1700000000
const d = new Date(sec * 1000)           // 秒→毫秒
const iso = d.toISOString()              // UTC 字符串,末尾 Z
const local = d.toLocaleString('zh-CN')  // 本地时区字符串

// 按指定时区渲染(服务端渲染、多时区报表必备)
new Intl.DateTimeFormat('zh-CN', {
  timeZone: 'America/New_York',
  dateStyle: 'full', timeStyle: 'long',
}).format(d)

Python

from datetime import datetime, timezone
ts = 1700000000
print(datetime.fromtimestamp(ts, tz=timezone.utc))  # 明确指定 UTC,否则按系统本地时区
print(datetime.fromtimestamp(ts, tz=timezone.utc).isoformat())

Python 的 datetime.fromtimestamp(ts) 不传 tz 会按系统本地时区解释——本地开发机和 UTC 服务器结果不同,这是最常见的上线后才发现的时间 bug。

Go

import "time"
t := time.Unix(1700000000, 0)        // 秒级
// 毫秒用 time.UnixMilli(ms),微秒 time.UnixMicro,纳秒 time.Unix(0, ns)
loc, _ := time.LoadLocation("Asia/Shanghai")
fmt.Println(t.In(loc).Format(time.RFC3339))

Java

Instant i = Instant.ofEpochSecond(1700000000L);   // 秒
Instant j = Instant.ofEpochMilli(1700000000000L); // 毫秒
System.out.println(i.atZone(ZoneId.of("Asia/Shanghai")));
// 用 java.time(Java 8+),不要再碰 java.util.Date / Calendar

PHP

$dt = (new DateTimeImmutable('@1700000000'))      // @ 前缀表示时间戳,默认 UTC
    ->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo $dt->format(DateTimeInterface::RFC3339);

Rust

use chrono::{DateTime, Utc, TimeZone};
let dt: DateTime<Utc> = Utc.timestamp_opt(1700000000, 0).unwrap();
println!("{}", dt.to_rfc3339());

症状 → 原因 → 修复

症状 原因 修复
时间显示成 1970-01-01 传了 0 或 null,或字符串解析失败 校验输入;解析失败要显式报错而不是静默取 0
时间跳到 5 万年后 毫秒当秒存(差 1000 倍) 统一精度;字段名写死单位
本地正确、服务器差 8 小时 服务器时区 UTC,代码按本地时区解析 全程 UTC,只在展示层转
用户看到的日期差一天 展示用了 UTC 而用户按本地预期 展示层传用户时区
定时任务少跑一次 DST 春季跳变,目标时刻不存在 Cron 用 UTC,或改用「每隔 N 秒」而非「每天 HH:MM」
订单在同一秒重复 时间戳精度不够(秒级撞车) 改用毫秒,或加单调序号
2038 之后时间变负数 32 位整数溢出 换 64 位或 ISO 8601
跨库迁移后差几小时 一边存本地、一边存 UTC 统一为 UTC 后重算历史数据

最佳实践

  1. 统一用 UTC 存储:数据库字段用 timestamptz/DATETIME(UTC) 或整数时间戳,别用「本地时间」列。
  2. 接口传整数时间戳或 ISO 8601 带偏移:避免传「2023-11-15 06:13:20」这种无时区字符串。
  3. 展示层按用户时区格式化:用 Intl.DateTimeFormat(前端)或带 ZoneId 的格式化器(后端),别手写拼接。
  4. 单位要显式约定:前后端文档写清是秒还是毫秒,字段名带 _ms 后缀,转换工具自动识别兜底。
  5. 别在墙上时间做算术:差值、定时、排序都在时间戳整数层面算。
  6. 时区用 IANA 标识符存:数据库里存 Asia/Shanghai,不要存 +08:00
  7. 边界要测:1970 之前(负数)、2038-01-19(32 位溢出)、DST 切换日、闰年 2 月 29 日。

改完代码别忘了实测。把可疑值丢进 时间戳转换工具,它会同时给出 UTC 与各主要时区的渲染结果,一眼就能看出是单位错了还是时区错了。

常见问题

10 位和 13 位时间戳有什么区别?

10 位是秒级(Unix epoch 秒),13 位是毫秒级。同一时刻 13 位值 ≈ 10 位值 × 1000。JavaScript 的 Date.now() 返回毫秒,很多后端接口用秒,混用会差 1000 倍。

为什么同一时间戳在不同时区显示不同?

时间戳本身是 UTC 绝对时刻,与时区无关;显示出的日期时间是按你所在时区渲染的结果。转换工具应允许选择目标时区再展示。

前端和后端的时区要对齐吗?

不需要在「时区」上对齐,而要在「使用 UTC 存储、本地展示」上对齐。数据库和接口统一传 UTC(或时间戳),展示层按用户时区格式化,避免服务器时区漂移导致错乱。

闰秒会影响时间戳吗?

Unix 时间戳按固定 86400 秒/天线性递增,不感知闰秒;闰秒由 NTP/操作系统在底层平滑处理。业务系统一般无需特殊处理,但金融高频场景要知悉此差异。

2038 年问题还存在吗?

32 位有符号整数存秒级时间戳会在 2038-01-19 溢出。现代语言(64 位)已规避,但老系统、嵌入式设备、部分 C 库仍可能踩坑,跨系统传时间建议用 64 位或 ISO 8601 字符串。

数据库该存整数时间戳还是 DATETIME?

看你要不要靠数据库做时间运算。需要按日期分组、按月聚合、用 BETWEEN 查区间时,数据库原生时间类型(PostgreSQL 的 `timestamptz`、MySQL 的 `DATETIME`)可读性和索引表现都更好,且**必须带时区或用 UTC 语义**。只做排序和差值、且要跨语言跨存储一致时,整数时间戳更省心。最不该做的是存「本地时间字符串」——它既无时区又无法比较。

JSON 里的时间该怎么写?

JSON 本身没有日期类型,所以必须约定一种字符串或数字。推荐 ISO 8601 带时区偏移(`2026-08-30T02:13:20+08:00`)或 UTC 的 `Z` 形式(`2026-08-30T18:13:20Z`)——可读、可排序、有明确时区。整数时间戳也合法,但调试时看不出是哪一天。千万别用 `2026-08-30 02:13:20` 这种无时区的本地串:解析器会按运行环境时区解释,跨时区必然错。

← 返回博客首页