如果把日期比作一张写着“上午八点”的便签,那么时间戳更像一条精确的坐标:它不关心你站在哪个时区,只记录那个瞬间距离一个共同起点有多远。理解这一点,后面的换算、时区和排错都会顺很多。
01 什么是 Unix 时间戳
Unix 时间戳(Unix timestamp)表示从 1970 年 1 月 1 日 00:00:00 UTC 起,到某个时间点经过的秒数。这个起点被称为 Unix 纪元(Epoch)。它不包含时区信息,也不包含“上午”或“下午”这类本地化描述,因此适合在不同系统之间传递。
Unix 时间戳通常不考虑闰秒。也就是说,它把每一天视为 86,400 秒,而不是在闰秒发生时额外加一秒。对绝大多数业务系统来说,这个差异可以忽略;但在高精度天文计算或某些特殊计时场景里,需要另行处理。
时间戳可以是负数,表示 1970 年之前的时间点。比如 -1 对应 1969-12-31 23:59:59 UTC。负数并不神秘,它只是坐标轴向左延伸的部分。
一个容易被忽略的事实:时间戳描述的是“哪一刻”,而不是“哪一地”。
1767225600在全球任何地方都指向同一个瞬间,只是当地人看到的钟表读数不同。
02 秒、毫秒与位数
时间戳本身没有规定单位。不同系统可能用秒、毫秒、微秒或纳秒。最常见的两种是:
- 秒级(s):当前时间通常是 10 位,例如
1767225600。Unix 命令、很多后端接口和数据库函数使用这一种。 - 毫秒级(ms):当前时间通常是 13 位,例如
1767225600000。JavaScript 的Date.now()、移动端日志和部分 API 使用这一种。
更精确的系统还会使用 16 位微秒或 19 位纳秒。它们的数值更大,但最终显示到日期时,浏览器通常只能保留到毫秒精度。把纳秒值直接塞进 JavaScript 的 Number 也可能损失精度,超过安全整数范围时应该用字符串或 BigInt 传递。
| 位数 | 单位 | 示例 | 典型场景 |
|---|---|---|---|
| 10 位 | 秒 | 1767225600 | Unix 命令、后端 API |
| 13 位 | 毫秒 | 1767225600000 | JavaScript、日志 |
| 16 位 | 微秒 | 1767225600000000 | 高精度数据库、SDK |
| 19 位 | 纳秒 | 1767225600000000000 | 系统调用、性能计时 |
位数只是线索,不是法律。更稳妥的判断方式是结合数量级、来源字段和业务日期一起验证。本站的在线转换工具会按数量级自动识别,也允许你手动锁定单位。
03 时间戳转日期:一条除法链
以 1767225600 为例。先把秒数除以 86,400,得到从纪元起经过的完整天数:
1767225600 ÷ 86400 = 20454 天,余 0 秒
起点:1970-01-01 00:00:00 UTC
结果:2026-01-01 00:00:00 UTC
换算到北京时间:2026-01-01 08:00:00实际计算不需要真的数 20,454 个格子,编程语言和浏览器都有现成的日期库。手算的意义在于理解:时间戳转日期,本质上是“从 UTC 起点开始累加”,再把结果格式化到某个时区。
格式化时要特别小心。很多语言的默认输出使用运行环境的本地时区,服务器时区、容器时区和用户浏览器时区可能各不相同。看到时间差了 8 小时,先检查输出时区,而不是急着修改时间戳。
04 日期转时间戳:先确定时区,再做减法
反向转换多了一步:输入的“2026-01-01 08:00:00”到底属于哪个时区?如果它是北京时间,那么它对应的 UTC 时间是 2026-01-01 00:00:00,时间戳是 1767225600;如果它其实是伦敦时间,时间戳会完全不同。
所以,日期转时间戳的正确顺序是:
- 读取年月日时分秒,确认它们属于哪个时区;
- 用该时区的规则还原成 UTC 时间点;
- 计算该 UTC 时间点与 1970-01-01 00:00:00 UTC 的秒数差;
- 根据接口要求决定输出秒还是毫秒。
如果输入字符串自带偏移,例如 2026-01-01T08:00:00+08:00,就可以直接按偏移计算,不必再猜时区。这也是为什么跨系统传输时,带偏移的 ISO 8601 比裸的本地时间更可靠。
05 时区与那 8 小时
北京时间使用 UTC+8,所以同一时间戳在 UTC 下显示为 00:00 时,在北京时间下就是 08:00。两者相差 8 小时,指的都是同一个绝对时间点。这个关系全年成立,因为中国目前不实行夏令时。
但并不是所有地方都这么稳定。纽约冬令时是 UTC−5,夏令时是 UTC−4;伦敦冬令时是 UTC+0,夏令时是 UTC+1。它们与北京的时差会在一年中变化。用固定的“差 12 小时”去算,换季时就会错一小时。
还有一个常见误区是时区缩写。CST 可能指中国标准时间,也可能指美国中部标准时间;EST 也不总是等于 UTC−5。程序里应使用 IANA 时区名,例如 Asia/Shanghai、America/New_York,而不是缩写或固定的 GMT+8。
想直观看到多个城市的时间,可以使用时区换算器;如果已经遇到差 8 小时的具体问题,这篇排查笔记给出了从接口到数据库的检查顺序。
06 闰年、闰秒、夏令时,分别影响什么
闰年影响日历天数。2 月 29 日存在时,一年有 366 天,但时间戳仍然只是连续累加的秒数。把时间戳转成日期时,日期库会自动处理 2 月 29 日,不需要你手工判断。
闰秒是让 UTC 与地球自转保持接近的调整。Unix 时间戳通常忽略闰秒,把每天当作 86,400 秒。因此,在闰秒发生的瞬间,时间戳与某些高精度时钟之间可能出现 1 秒差异。普通业务不需要处理,但涉及天文、卫星或高精度计时的系统需要专门设计。
夏令时影响本地钟表读数。时间戳本身不受影响,但从“本地时间”转成时间戳时,必须知道该时区当天是否处于夏令时。更麻烦的是,夏令时切换会造成“重复的一小时”或“跳过的一小时”:某些钟表时间会出现两次,某些钟表时间根本不存在。浏览器和 IANA 数据库会按既定规则选择或调整,但业务上仍应避免把会议时间放在切换的缝隙里。
07 2038 年问题:32 位整数的边界
32 位有符号整数能表示的最大值是 2,147,483,647。如果某个系统用它来存储秒级 Unix 时间戳,这个值对应:
2147483647 → 2038-01-19 03:14:07 UTC
2147483648 → 2038-01-19 03:14:08 UTC(超出 32 位有符号上限)在北京时间下,这两个时刻分别是 2038-01-19 11:14:07 和 11:14:08。使用 64 位整数存储秒级时间戳后,可表示范围会扩展到数亿年,日常系统不需要再担心这个边界。真正需要排查的是旧系统、嵌入式设备、老版本数据库字段、文件格式和协议字段。
排查时可以从四个问题开始:字段类型是不是 32 位?协议里有没有固定 4 字节?数据库列是不是 INT?测试数据有没有只测到 2037 年?把测试用例推进到 2038 年之后,往往能提前暴露问题。
08 常见语言里的写法
下面只给出最短可用示例。实际项目里,请优先使用带时区的时间库,并明确单位。
JavaScript
const seconds = Math.floor(Date.now() / 1000);
const milliseconds = Date.now();
const date = new Date(1767225600 * 1000);
console.log(date.toISOString()); // 2026-01-01T00:00:00.000Z
const beijing = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
dateStyle: 'medium',
timeStyle: 'medium',
hourCycle: 'h23'
}).format(date);Python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
dt = datetime.fromtimestamp(1767225600, tz=timezone.utc)
print(dt.isoformat()) # 2026-01-01T00:00:00+00:00
print(dt.astimezone(ZoneInfo("Asia/Shanghai")).isoformat())
# 当前时间戳
print(int(datetime.now(timezone.utc).timestamp()))Java
long seconds = Instant.now().getEpochSecond();
long millis = Instant.now().toEpochMilli();
Instant instant = Instant.ofEpochSecond(1767225600L);
System.out.println(instant); // 2026-01-01T00:00:00Z
ZonedDateTime beijing = instant.atZone(ZoneId.of("Asia/Shanghai"));PHP
$seconds = time();
$dt = new DateTimeImmutable('@1767225600');
$beijing = $dt->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo $beijing->format('Y-m-d H:i:s');Go
now := time.Now().Unix()
t := time.Unix(1767225600, 0).UTC()
fmt.Println(t.Format(time.RFC3339))
loc, _ := time.LoadLocation("Asia/Shanghai")
fmt.Println(t.In(loc).Format("2006-01-02 15:04:05"))SQL
SELECT UNIX_TIMESTAMP(NOW()) AS seconds;
SELECT FROM_UNIXTIME(1767225600) AS local_time;
-- 显式查看当前会话时区,避免误判
SELECT @@session.time_zone, @@global.time_zone;09 数据库与接口里的时间字段
比“会不会转换”更容易出问题的是“存什么、传什么、显示什么”。一套省心的约定通常包含三层:
- 存储层:使用带时区的时间类型,或统一存 UTC;如果用整数,使用
BIGINT而不是INT,并在字段名里写清单位,例如created_at_ms。 - 传输层:优先使用 RFC 3339 / ISO 8601 字符串,或明确单位的整数。不要在 JSON 里传一个没有说明的“时间”字段。
- 展示层:按用户所在时区格式化,并标注时区。管理后台如果需要固定时区,直接写“北京时间”,不要只显示一个裸时间。
数据库排查时,至少同时记录三个值:原始时间戳、数据库会话时区、应用服务器时区。很多“差 8 小时”并不是时间戳算错了,而是这三个值里有两个不一致。
10 一份可执行的排查清单
- 确认输入是秒还是毫秒,并和接口文档、数据库字段类型对上。
- 确认转换方向:是“数字 → 日期”,还是“日期 → 数字”。
- 确认日期输入所绑定的时区,而不是仅凭服务器默认值。
- 把结果同时输出为 UTC 与本地时间,对比是否只差时区偏移。
- 检查是否跨过夏令时切换、月末、闰年或 2038 边界。
- 检查超过 13 位的整数是否被 JSON Number 或前端解析损失精度。
- 在日志里保留原始值与单位,别只记录格式化后的日期。
- 用第二个独立工具或服务端代码复算一次,避免同源错误。
如果你只想快速完成一次核对,可以直接打开时间戳转换工作台;如果手上有整列数据,用批量转换更合适;涉及跨城会议时,再到时区换算器对齐两端的钟表时间。