上个月,凌晨两点被 oncall 电话叫醒。线上告警,某个下游服务全部报"签名过期"。
排查了半小时,最后发现原因离谱到想哭:上游传过来的时间戳是 13 位的毫秒级,下游期望的是 10 位的秒级。 差了三个数量级,下游把 1719820800000 当成了公元 56438 年的时间,签名校验直接失败。
这种坑,我踩过不止一次。
时间戳为什么这么容易踩坑?
时间戳看起来就是一个数字,但它可能是:
| 10 位整数 | 1719820800 | 秒级时间戳(Unix 标准) |
| 13 位整数 | 1719820800000 | 毫秒级时间戳(JavaScript 常用) |
| 16 位整数 | 1719820800000000 | 微秒级时间戳(某些高精度场景) |
| 带小数点的浮点数 | 1719820800.123 | 秒 + 毫秒(Python time.time()) |
最常见的坑就是 10 位和 13 位混用。 不同语言、不同系统默认的时间戳精度不一样,对接的时候稍不注意就会出问题。
我曾经统计过自己踩过的时间戳相关 Bug:
| 10 位/13 位混用 | 7 次 | 签名失败、数据查询为空 |
| 时区没对齐(UTC vs 本地时间) | 4 次 | 日志时间对不上、定时任务偏移 |
| 日期字符串解析不一致 | 3 次 | iOS 上 new Date("2024-07-01") 返回 NaN |
| 闰秒/夏令时 | 1 次 | 跨时区报表差 1 小时 |
时间戳的基础知识:一篇讲透
Unix 时间戳到底是什么?
Unix 时间戳(也叫 POSIX time、Epoch time)是一个数字,表示 从 1970 年 1 月 1 日 00:00:00 UTC 到某个时刻经过的秒数。
0 → 1970-01-01 00:00:00 UTC
1719820800 → 2024-07-01 12:00:00 UTC
就这么简单。一个数字,对应一个时间点。
各语言获取当前时间戳的方式
| JavaScript | Date.now() | 13 位(毫秒) |
| Python | int(time.time()) | 10 位(秒) |
| Java | System.currentTimeMillis() | 13 位(毫秒) |
| Go | time.Now().Unix() | 10 位(秒) |
| Rust | std::time::SystemTime::now() | 需要手动转 |
| Shell | date +%s | 10 位(秒) |
| PHP | time() | 10 位(秒) |
⚠️ 注意: JavaScript 默认是毫秒,Python/Go/PHP 默认是秒。这是最容易混的地方。
各语言互相转换的方法
秒级 → 毫秒级:
# Python
import time
ts_sec = int(time.time()) # 1719820800
ts_ms = ts_sec * 1000 # 1719820800000
毫秒级 → 秒级:
// JavaScript
const tsMs = Date.now(); // 1719820800000
const tsSec = Math.floor(tsMs / 1000); // 1719820800
时间戳 → 可读时间:
# Python
from datetime import datetime, timezone
dt = datetime.fromtimestamp(1719820800, tz=timezone.utc)
print(dt.strftime("%Y-%m-%d %H:%M:%S")) # 2024-07-01 12:00:00
// JavaScript
const dt = new Date(1719820800 * 1000);
console.log(dt.toISOString()); // 2024-07-01T12:00:00.000Z
// Go
t := time.Unix(1719820800, 0)
fmt.Println(t.UTC().Format("2006-01-02 15:04:05")) // 2024-07-01 12:00:00
可读时间 → 时间戳:
# Python
from datetime import datetime
dt = datetime.strptime("2024-07-01 20:00:00", "%Y-%m-%d %H:%M:%S")
ts = int(dt.timestamp()) # 注意:这里用的是本地时区
⚠️ 这里的坑: Python 的 datetime.timestamp() 默认用本地时区。如果你在北京(UTC+8),"2024-07-01 20:00:00" 会被当作 2024-07-01 12:00:00 UTC,得到的是 1719835200 而不是 1719864000。一定要明确你传入的时间是什么时区。
时区:时间戳踩坑的第二大来源
时间戳本身是 与时区无关的。1719820800 永远代表 2024-07-01 12:00:00 UTC。
但当你把时间戳转成可读时间时,时区就来了:
1719820800 在不同时区的显示:
UTC → 2024-07-01 12:00:00
UTC+8 → 2024-07-01 20:00:00 (北京时间)
UTC-5 → 2024-07-01 07:00:00 (美东时间)
UTC+9 → 2024-07-01 21:00:00 (东京时间)
实际踩坑案例:
我们有个数据报表系统,每天凌晨 0 点拉取前一天的数据。代码里用的是:
# 获取"昨天"的起止时间戳
today = datetime.now().date()
yesterday_start = datetime.combine(today – timedelta(days=1), time(0, 0, 0))
yesterday_end = datetime.combine(today, time(0, 0, 0))
看起来没问题。但服务器部署在美国(UTC-5),而业务数据的"一天"是按北京时间(UTC+8)划分的。结果每次拉取的数据都差了 13 个小时。
修复方案:永远显式指定时区。
from datetime import datetime, timezone, timedelta
bj_tz = timezone(timedelta(hours=8))
today = datetime.now(bj_tz).date()
yesterday_start = datetime.combine(today – timedelta(days=1), time(0, 0, 0), tzinfo=bj_tz)
yesterday_end = datetime.combine(today, time(0, 0, 0), tzinfo=bj_tz)
日期字符串解析:各语言的坑
JavaScript 的 Date 解析
new Date("2024-07-01") // ✅ UTC 时间 → 2024-07-01T00:00:00.000Z
new Date("2024/07/01") // ⚠️ 本地时间 → 取决于你的时区
new Date("2024-07-01 12:00:00") // ❌ 不同浏览器行为不一致
new Date("July 1, 2024") // ⚠️ 本地时间
规则很简单:带 – 的 ISO 格式(YYYY-MM-DD)按 UTC 解析,其他格式按本地时间解析。 但不同浏览器(特别是 Safari)的实现有差异。
iOS 上的经典坑
// Chrome 上正常
new Date("2024-07-01 12:00:00") // Mon Jul 01 2024 12:00:00
// iOS Safari 上
new Date("2024-07-01 12:00:00") // Invalid Date ❌
// 修复:用 "/" 替代 "-"
new Date("2024/07/01 12:00:00") // ✅ 所有浏览器都支持
Python 的 strptime
# 正确
datetime.strptime("2024-07-01 12:00:00", "%Y-%m-%d %H:%M:%S")
# 容易搞混的格式符
# %m = 月份 (01-12)
# %M = 分钟 (00-59)
# %H = 24小时制 (00-23)
# %I = 12小时制 (01-12)
# %p = AM/PM
实用场景速查
场景一:快速查看一个时间戳对应的真实时间
开发调试时经常需要把日志里的时间戳转成可读时间。几个常用方法:
# macOS
date -r 1719820800
# Linux
date -d @1719820800
# 在线工具(随时打开浏览器就能用)
# https://leowh.com/tools/timestamp/index.html
日常开发中,遇到需要快速确认某个时间戳是什么时间、或者把某个时间转成时间戳的场景特别多。查日志、对接口、验签名的时候,手边有一个随时可用的转换工具比翻文档快得多。我自己用的是 leowh.com/tools/timestamp,支持秒级/毫秒级互转,也能直接输入日期时间转时间戳,打开即用,不需要装任何东西。
场景二:API 接口对接时统一时间戳精度
团队规范建议:
## 时间戳规范
1. 所有 API 接口的时间参数统一使用 13 位毫秒级时间戳
2. 数据库中存储的时间统一使用 UTC 时间戳(10 位秒级)
3. 前端展示时转换为本地时间
4. 日志中统一使用 ISO 8601 格式:2024-07-01T12:00:00Z
在 API 层面做校验:
def validate_timestamp(ts):
"""校验时间戳格式和范围"""
if isinstance(ts, str):
ts = int(ts)
# 判断是秒级还是毫秒级
if ts > 1e12: # 13 位以上,大概率是毫秒
ts_sec = ts / 1000
else:
ts_sec = ts
# 校验合理范围(2000-2100 年)
if not (946684800 <= ts_sec <= 4102444800):
raise ValueError(f"时间戳超出合理范围: {ts}")
return ts
场景三:定时任务中的时间计算
from datetime import datetime, timezone, timedelta
bj_tz = timezone(timedelta(hours=8))
def get_today_range():
"""获取今天(北京时间)的时间戳范围"""
now = datetime.now(bj_tz)
start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
end_of_day = start_of_day + timedelta(days=1)
return int(start_of_day.timestamp()), int(end_of_day.timestamp())
def get_last_n_days_range(n):
"""获取最近 n 天的时间戳范围"""
now = datetime.now(bj_tz)
start = now – timedelta(days=n)
start_of_start = start.replace(hour=0, minute=0, second=0, microsecond=0)
return int(start_of_start.timestamp()), int(now.timestamp())
场景四:日志时间对齐
分布式系统中,不同服务的日志时间可能不在同一个时区。排查问题时用时间戳对齐:
# 从日志中提取时间戳并排序
grep "ERROR" service-a.log | sed 's/.*\\[\\(.*\\)\\].*/\\1/' | sort
# 或者直接对比时间戳
echo "Service A: $(date -r 1719820800)"
echo "Service B: $(date -r 1719820803)"
# 差 3 秒,可能是网络延迟
时间戳的边界情况
2038 年问题
32 位有符号整数的最大值是 2147483647,对应 2038-01-19 03:14:07 UTC。超过这个时间,32 位系统的时间戳会溢出变成负数。
2147483647 → 2038-01-19 03:14:07 UTC ← 32 位上限
2147483648 → 1901-12-13 20:45:52 UTC ← 溢出后的值
影响范围:
- 🔴 嵌入式系统、IoT 设备(大量使用 32 位芯片)
- 🔴 老旧的 C/C++ 程序(使用 time_t 类型)
- 🟢 现代 64 位系统不受影响(上限约 2920 亿年后)
闰秒
UTC 时间偶尔会插入闰秒(23:59:60),但 Unix 时间戳不记录闰秒。这意味着:
- 包含闰秒的那一天有 86401 秒而不是 86400 秒
- 时间戳在闰秒时刻会"暂停"一秒
- 大部分场景可以忽略,但金融交易系统需要特别注意
负数时间戳
1970 年之前的时间用负数表示:
-86400 → 1969-12-31 00:00:00 UTC
-62135596800 → 0001-01-01 00:00:00 UTC
有些库不支持负数时间戳,处理历史日期时要注意。
速查表:常用时间戳对照
| 1970-01-01 00:00:00 UTC | 0 | 0 |
| 2000-01-01 00:00:00 UTC | 946684800 | 946684800000 |
| 2020-01-01 00:00:00 UTC | 1577836800 | 1577836800000 |
| 2024-01-01 00:00:00 UTC | 1704067200 | 1704067200000 |
| 2025-01-01 00:00:00 UTC | 1735689600 | 1735689600000 |
| 2030-01-01 00:00:00 UTC | 1893456000 | 1893456000000 |
| 2038-01-19 03:14:07 UTC | 2147483647 | 2147483647000 |
快速判断位数:
- 10 位数 → 2001 年到 2286 年之间的秒级时间戳
- 13 位数 → 毫秒级时间戳
- 9 位数 → 1970 年到 2001 年之间的秒级时间戳
各语言常用时间格式符速查
Python strftime/strptime
| %Y | 4 位年份 | 2024 |
| %m | 2 位月份 | 07 |
| %d | 2 位日期 | 01 |
| %H | 24 小时制 | 14 |
| %M | 分钟 | 30 |
| %S | 秒 | 00 |
| %f | 微秒 | 123456 |
| %z | 时区偏移 | +0800 |
| %A | 星期全称 | Monday |
JavaScript Intl.DateTimeFormat
new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: 'Asia/Shanghai',
hour12: false
}).format(new Date(1719820800000));
// "2024/07/01 20:00:00"
常见问题
Q:怎么快速判断一个时间戳是秒级还是毫秒级?
A:看位数。10 位是秒级,13 位是毫秒级。或者看数值大小:大于 1e12(一万亿)的基本上是毫秒级。
Q:时间戳有最大值吗?
A:取决于存储类型。32 位有符号整数最大到 2147483647(2038 年)。64 位整数理论上可以用到 2920 亿年后。JavaScript 的 Number.MAX_SAFE_INTEGER 是 9007199254740991,对应大约 28.5 万年后的毫秒时间戳。
Q:为什么我的时间戳转换后差了 8 小时?
A:时区问题。北京时间是 UTC+8,如果你的代码用 UTC 解析但显示时用本地时间(或反过来),就会差 8 小时。永远显式指定时区,不要依赖系统默认时区。
Q:数据库应该存时间戳还是日期字符串?
A:推荐存 UTC 时间戳(整数类型),原因:
- 查询效率高(整数比较比字符串快)
- 没有时区歧义
- 占用空间小
- 排序天然正确
如果一定要存字符串,用 ISO 8601 格式:2024-07-01T12:00:00Z。
Q:前后端对接时时间格式怎么统一?
A:建议后端统一返回 13 位毫秒级时间戳(和 JavaScript 的 Date.now() 一致),前端根据用户时区转换显示。避免传 "2024-07-01 12:00:00" 这种字符串——不同语言解析行为不一致。
Q:微秒/纳秒级时间戳在什么场景下用?
A:高频交易、性能 profiling、分布式链路追踪。普通业务场景用毫秒级就够了。
总结
时间戳看起来简单,但细节不少。最容易踩的三个坑:
记住这几条就够了:
✅ 团队内统一时间戳精度规范(建议 13 位毫秒) ✅ 所有时间处理代码显式指定时区 ✅ 跨语言传递用时间戳,不用日期字符串 ✅ 数据库存 UTC 时间戳,展示时转本地时间 ✅ 手边备一个转换工具,调试时随时验证:leowh.com/tools/timestamp






