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

📅 2026/8/6 4:52:29
被一个 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就这么简单。一个数字对应一个时间点。各语言获取当前时间戳的方式语言代码输出精度JavaScriptDate.now()13 位毫秒Pythonint(time.time())10 位秒JavaSystem.currentTimeMillis()13 位毫秒Gotime.Now().Unix()10 位秒Ruststd::time::SystemTime::now()需要手动转Shelldate %s10 位秒PHPtime()10 位秒⚠️注意JavaScript 默认是毫秒Python/Go/PHP 默认是秒。这是最容易混的地方。各语言互相转换的方法秒级 → 毫秒级# Pythonimporttime ts_secint(time.time())# 1719820800ts_msts_sec*1000# 1719820800000毫秒级 → 秒级// JavaScriptconsttsMsDate.now();// 1719820800000consttsSecMath.floor(tsMs/1000);// 1719820800时间戳 → 可读时间# Pythonfromdatetimeimportdatetime,timezone dtdatetime.fromtimestamp(1719820800,tztimezone.utc)print(dt.strftime(%Y-%m-%d %H:%M:%S))# 2024-07-01 12:00:00// JavaScriptconstdtnewDate(1719820800*1000);console.log(dt.toISOString());// 2024-07-01T12:00:00.000Z// Got:time.Unix(1719820800,0)fmt.Println(t.UTC().Format(2006-01-02 15:04:05))// 2024-07-01 12:00:00可读时间 → 时间戳# Pythonfromdatetimeimportdatetime dtdatetime.strptime(2024-07-01 20:00:00,%Y-%m-%d %H:%M:%S)tsint(dt.timestamp())# 注意这里用的是本地时区⚠️这里的坑Python 的datetime.timestamp()默认用本地时区。如果你在北京UTC82024-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 UTC8 → 2024-07-01 20:00:00 北京时间 UTC-5 → 2024-07-01 07:00:00 美东时间 UTC9 → 2024-07-01 21:00:00 东京时间实际踩坑案例我们有个数据报表系统每天凌晨 0 点拉取前一天的数据。代码里用的是# 获取昨天的起止时间戳todaydatetime.now().date()yesterday_startdatetime.combine(today-timedelta(days1),time(0,0,0))yesterday_enddatetime.combine(today,time(0,0,0))看起来没问题。但服务器部署在美国UTC-5而业务数据的一天是按北京时间UTC8划分的。结果每次拉取的数据都差了 13 个小时。修复方案永远显式指定时区。fromdatetimeimportdatetime,timezone,timedelta bj_tztimezone(timedelta(hours8))todaydatetime.now(bj_tz).date()yesterday_startdatetime.combine(today-timedelta(days1),time(0,0,0),tzinfobj_tz)yesterday_enddatetime.combine(today,time(0,0,0),tzinfobj_tz)日期字符串解析各语言的坑JavaScript 的 Date 解析newDate(2024-07-01)// ✅ UTC 时间 → 2024-07-01T00:00:00.000ZnewDate(2024/07/01)// ⚠️ 本地时间 → 取决于你的时区newDate(2024-07-01 12:00:00)// ❌ 不同浏览器行为不一致newDate(July 1, 2024)// ⚠️ 本地时间规则很简单带-的 ISO 格式YYYY-MM-DD按 UTC 解析其他格式按本地时间解析。但不同浏览器特别是 Safari的实现有差异。iOS 上的经典坑// Chrome 上正常newDate(2024-07-01 12:00:00)// Mon Jul 01 2024 12:00:00// iOS Safari 上newDate(2024-07-01 12:00:00)// Invalid Date ❌// 修复用 / 替代 -newDate(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实用场景速查场景一快速查看一个时间戳对应的真实时间开发调试时经常需要把日志里的时间戳转成可读时间。几个常用方法# macOSdate-r1719820800# Linuxdate-d1719820800# 在线工具随时打开浏览器就能用# 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 层面做校验defvalidate_timestamp(ts):校验时间戳格式和范围ifisinstance(ts,str):tsint(ts)# 判断是秒级还是毫秒级ifts1e12:# 13 位以上大概率是毫秒ts_sects/1000else:ts_sects# 校验合理范围2000-2100 年ifnot(946684800ts_sec4102444800):raiseValueError(f时间戳超出合理范围:{ts})returnts场景三定时任务中的时间计算fromdatetimeimportdatetime,timezone,timedelta bj_tztimezone(timedelta(hours8))defget_today_range():获取今天北京时间的时间戳范围nowdatetime.now(bj_tz)start_of_daynow.replace(hour0,minute0,second0,microsecond0)end_of_daystart_of_daytimedelta(days1)returnint(start_of_day.timestamp()),int(end_of_day.timestamp())defget_last_n_days_range(n):获取最近 n 天的时间戳范围nowdatetime.now(bj_tz)startnow-timedelta(daysn)start_of_startstart.replace(hour0,minute0,second0,microsecond0)returnint(start_of_start.timestamp()),int(now.timestamp())场景四日志时间对齐分布式系统中不同服务的日志时间可能不在同一个时区。排查问题时用时间戳对齐# 从日志中提取时间戳并排序grepERRORservice-a.log|seds/.*\[\(.*\)\].*/\1/|sort# 或者直接对比时间戳echoService A:$(date-r1719820800)echoService B:$(date-r1719820803)# 差 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 UTC002000-01-01 00:00:00 UTC9466848009466848000002020-01-01 00:00:00 UTC157783680015778368000002024-01-01 00:00:00 UTC170406720017040672000002025-01-01 00:00:00 UTC173568960017356896000002030-01-01 00:00:00 UTC189345600018934560000002038-01-19 03:14:07 UTC21474836472147483647000快速判断位数10 位数 → 2001 年到 2286 年之间的秒级时间戳13 位数 → 毫秒级时间戳9 位数 → 1970 年到 2001 年之间的秒级时间戳各语言常用时间格式符速查Python strftime/strptime格式符含义示例%Y4 位年份2024%m2 位月份07%d2 位日期01%H24 小时制14%M分钟30%S秒00%f微秒123456%z时区偏移0800%A星期全称MondayJavaScript Intl.DateTimeFormatnewIntl.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(newDate(1719820800000));// 2024/07/01 20:00:00常见问题Q怎么快速判断一个时间戳是秒级还是毫秒级A看位数。10 位是秒级13 位是毫秒级。或者看数值大小大于1e12一万亿的基本上是毫秒级。Q时间戳有最大值吗A取决于存储类型。32 位有符号整数最大到21474836472038 年。64 位整数理论上可以用到 2920 亿年后。JavaScript 的Number.MAX_SAFE_INTEGER是9007199254740991对应大约 28.5 万年后的毫秒时间戳。Q为什么我的时间戳转换后差了 8 小时A时区问题。北京时间是 UTC8如果你的代码用 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