欢迎光临
我们一直在努力

被一个 13 位数字搞崩了线上服务,我才彻底搞懂时间戳这件事

上个月,凌晨两点被 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

有些库不支持负数时间戳,处理历史日期时要注意。


速查表:常用时间戳对照

时间10 位时间戳(秒)13 位时间戳(毫秒)
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、分布式链路追踪。普通业务场景用毫秒级就够了。


总结

时间戳看起来简单,但细节不少。最容易踩的三个坑:

  • 10 位和 13 位混用 — 对接前确认精度,代码里做校验
  • 时区不明确 — 永远显式指定时区,不要靠默认行为
  • 日期字符串解析不一致 — 跨语言传递用时间戳,不要用字符串
  • 记住这几条就够了:

    ✅ 团队内统一时间戳精度规范(建议 13 位毫秒) ✅ 所有时间处理代码显式指定时区 ✅ 跨语言传递用时间戳,不用日期字符串 ✅ 数据库存 UTC 时间戳,展示时转本地时间 ✅ 手边备一个转换工具,调试时随时验证:leowh.com/tools/timestamp


    赞(0)
    未经允许不得转载:171主机测评 » 被一个 13 位数字搞崩了线上服务,我才彻底搞懂时间戳这件事
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址