ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unix时间戳全解析:从1970-01-01到2038年问题

Unix时间戳全解析:从1970-01-01到2038年问题 做运维和开发这些年最常把我和同事看愣的一个日期就是1970-01-01。日志里刷出一片1970-01-01 08:00:00接口返回一个负数时间戳或者前端把秒级数字当成毫秒去new Date()出来的日期全在 1970 年 1 月。这种场景我碰过太多次每次排查到最后都会回到同一个核心概念Unix 时间戳。简单说1970-01-01 00:00:00 UTC是 Unix 时间戳的起点也就是常说的 Unix epoch。计算机里并没有一个专门的黑洞日期所有跑到这个日期上的问题本质都是时间戳换算、存储或显示出了偏差。这篇文章就围绕这个起点展开把1970-01-01的来龙去脉讲清楚顺便把日常开发运维里容易踩的时间戳坑都摊开说一遍。1. 1970-01-01 到底怎么来的Unix 时间戳的起点1.1 时间戳本质上就是一把“秒尺”Unix 时间戳的定义非常直白从1970年1月1日 00:00:00 UTC到目标时刻经过的总秒数。它像一把从固定零点开始数数的尺子每过一秒就加一。所以时间戳0代表1970-01-01 00:00:00 UTC时间戳86400代表1970-01-02 00:00:00 UTC因为一天正好 86400 秒时间戳是负数代表 1970 年之前的时间时间戳再大也就是单纯把这个数字继续往上叠。很多人第一次看到1970-01-01 08:00:00会奇怪为什么不是00:00:00因为这不是 UTC 时间而是 UTC8 的北京当地显示。同一把“秒尺”用不同时区去读读出来的钟面时刻自然不一样。这也是后面要反复提到的一点时间戳是绝对标尺时区只影响显示不影响计数。理解了这个定义再回头看那些1970-01-01的日志基本就能猜出概率最高的原因某个时间字段传了0或者换算时把单位搞错了。1.2 为什么偏偏选了 1970 年Unix 诞生那点事要解释为什么是 1970得把时间拨回上世纪六七十年代。Unix 系统诞生于贝尔实验室Ken Thompson 和 Dennis Ritchie 这批人最早在 PDP-7、PDP-11 这类小型机上做实验。而 Unix 时间戳的零点最后就定在了1970-01-01 00:00:00 UTC。这个选择在今天被 POSIX 标准固定下来所以不只是 UnixLinux、macOS、Java、Go、Python 等一堆主流系统都用同一套秒计数体系。具体为什么选这一天业界普遍接受的解释是Unix 系统在 1969 年到 1970 年间逐渐成型当时需要一个足够近期、好记、便于整数计算的起点1970 年 1 月 1 日便是最自然的选择。再往前翻历史早期 Unix 的时间单位甚至有1/60 秒这种精细度后来才统一改成秒配合 C 语言库函数一起被标准化。所以1970-01-01不是什么玄学日期它就是 Unix 设计者在一开始定下的一把“秒尺”零点最后整个生态都沿用了这套规则。1.3 不是所有系统都用 1970各家的“纪元”对比虽然 Unix 系和 Java 系都默认1970-01-01但不少平台有自己独立的时间零点混用的时候最容易出 bug。系统/场景时间起点单位Unix / Linux 时间戳1970-01-01 00:00:00 UTC秒JavaSystem.currentTimeMillis()1970-01-01 00:00:00 UTC毫秒WindowsFILETIME1601-01-01 00:00:00 UTC100 纳秒Windows 注册表/API 常用文件时间1601-01-01 00:00:00 UTC100 纳秒Excel 日期序列号1900-01-01实际从 1900-01-00 起算天.NETDateTimeTicks0001-01-01 00:00:00100 纳秒Windows 选 1601 年是因为格里高利历 400 年一个周期1601 年正好是周期起点。Excel 沿用 Lotus 1-2-3 的日期系统起点是 1900 年还有个著名的 1900 闰年 bug日期序列号从虚幻的1900-01-00开始处理早于 1900 年 3 月 1 日的日期时要特别小心。各家起点不同日常跨系统对接时经常出现“我用 1970 算出来一个数你按 Windows 时间一解释直接差了几百年”的情况。理解这些起点是排查跨平台时间问题的第一步。2. 时间戳工作的真实细节与三个坑2.1 那些“1970-01-01”是怎么冒出来的排查日志时看到 1970第一反应不应该是“系统穿越了”而要从下面几个方向找原因。最常见的是全 0 时间戳。数据库字段默认值为 0接口没传值正则解析失败后 fallback 成 0都会把它直接还原成1970-01-01 00:00:00 UTC。北京时区显示成08:00:00伦敦时区显示成00:00:00仅此而已。其次是毫秒和秒的换算错误。最常见的是用 Java 或 JS 时把秒级时间戳直接当成毫秒使用。比如服务端返回1700000000前端new Date(1700000000)JavaScript 会把它当作毫秒换算出来就是1970-01-20 16:13:20 UTC。这个日期离 1970 特别近非常有迷惑性一眼就能看出单位错了。再是格式化框架的时区默认值。一些 JSON 序列化工具如果没配置时区可能用 UTC 输出也可能用服务器默认时区输出。同一时间戳在不同环境显示差 8 小时通常不是时间戳变了而是时区配置不一致。所以看到 1970不要急着改数据先确认三件事这个字段存的是秒还是毫秒、初始值是不是 0、显示时用的是哪个时区。2.2 2038 年问题32 位整数不是无限大只要聊 Unix 时间戳2038 年问题就绕不开。32 位有符号整数的最大值是2,147,483,647也就是 2038 年 1 月 19 日 03:14:07 UTC。再往后加一秒32 位有符号数溢出时间戳会突然变负日期直接跳回 1901 年 12 月 13 日左右。这本质上和当初 Y2K 问题类似是个整数上限引起的溢出。现在主流服务器基本都是 64 位系统time_t已经是 64 位64 位秒级时间戳能覆盖到宇宙热寂都不止基本不用担心。但风险集中在三类地方还在跑的 32 位嵌入式设备、老旧路由器、单片机固件内部仍然用int存储时间戳的业务代码跨语言对接时一方用 64 位一方用 32 位强转后溢出。我建议在新项目里所有时间戳存储、数据库字段、接口字段都统一用int64/BIGINT别再用int兜底。现在看着 2038 年很远但存量系统改造周期极长越早动手越省事。2.3 时区、夏令时与显示为什么同一时间戳显示不同时间戳本身不携带时区信息它只表示“从 1970 年到现在一共过了多少秒”。但同样的秒数在不同时区显示出的钟面时间一定不同。比如1700000000UTC 显示2023-11-14 22:13:20北京时间UTC8显示2023-11-15 06:13:20纽约冬令时UTC-5显示2023-11-14 17:13:20。很多线上事故的根因是日志打印用的服务器本地时区而数据库连接串写的时区参数是另一个。尤其是容器化部署以后TZ环境变量没设置时容器默认使用 UTC宿主机却是北京时间日志和监控时间对不上排查起来特别痛苦。夏令时更是噩梦。某些国家一年两次切换部分地区还会跳过一小时或重复一小时如果你所在业务涉及跨夏令时区域存储本地时间而不是 UTC 时间早晚要出 bug。铁律很简单存储统一用 UTC展示层再做本地化转换。3. 热搜里那些时间戳场景逐个拆给你看3.1 fastjson日期和时间戳互相转换的经典误区最近很多人在搜 “fastjson 时间转时间戳”说明这部分确实容易踩坑。fastjson 处理 Date 时序列化和反序列化的默认行为不一致最容易出问题。先说序列化。fastjson 默认把java.util.Date序列化成yyyy-MM-dd HH:mm:ss字符串但如果配置了序列化格式或你直接输出getTime()可能又变成了一串数字。前后端一旦对字段类型约定不一致返回给前端的就是裸时间戳。反序列化更考验人。fastjson 拿到一个 JSON 数字如果目标字段是Date它默认会按毫秒去解析。假设上游接口给的是秒级时间戳1700000000fastjson 会当成 1700000000 毫秒也就是1970-01-20 16:13:20。肉眼一看就发现不对秒级数据被当成毫秒用了。改造思路很明确跨系统传输时间最好直接用标准字符串比如2024-03-01T08:00:00Z或者明确约定字段是毫秒并统一内存和接口类型。Java 代码里也建议多用java.time包// 秒 - Date Date date new Date(seconds * 1000L); // Date - 秒 long seconds date.getTime() / 1000; // LocalDateTime - 秒先明确时区 long epochSecond localDateTime.toInstant(ZoneOffset.ofHours(8)).getEpochSecond(); // 秒 - LocalDateTime LocalDateTime ldt LocalDateTime.ofEpochSecond(epochSecond, 0, ZoneOffset.ofHours(8));这类代码里最容易忽略的就是时区toInstant()没指定偏移量时会直接抛异常或默认用系统时区。3.2 JavaScript秒还是毫秒差出一个时代前端处理时间戳几乎每个人都犯过“忘乘 1000”的错。JS 里Date.now()返回的是毫秒getTime()也是毫秒但后端接口经常返回秒级时间戳。如果你直接const ts 1700000000; // 秒 const d new Date(ts);得到的日期会是1970-01-20T16:13:20.000Z而不是真正的 2023 年。正确做法是显式判断单位const seconds 1700000000; const date new Date(seconds * 1000);反过来要向后端传秒级时间戳时记得除以 1000const nowSeconds Math.floor(Date.now() / 1000);还有一点Date在解析 ISO 字符串时带Z是 UTC不带Z是本地时间两者处理结果也可能差 8 小时。所以接口文档里最好把格式写死避免前端和服务端各猜各的。3.3 MySQL 与 Unix 套接字启动报错里的时间“周边”热搜词里有一条相当典型的运维报错mysqld_safe directory /var/run/mysqld for unix socket file dont exists.这个报错本身和时间戳没有直接关系但它发生在排查 MySQL 启动日志时和“Unix”体系关系很深。MySQL 在 Linux 上创建 Unix socket 需要/var/run/mysqld目录存在并且权限属于mysql用户。目录缺失或者被清理掉时服务启动就会报这个错。解决方式也简单mkdir -p /var/run/mysqld chown mysql:mysql /var/run/mysqld操作完再重启 MySQL 就能正常创建/var/run/mysqld/mysqld.sock。顺便说一句/var/run 在很多系统上是个临时目录重启后会被清空所以最好把上面的 mkdir 命令写进系统服务启动脚本里。MySQL 本身经常处理时间戳。常用函数有两个-- 当前时间戳秒 SELECT UNIX_TIMESTAMP(); -- 将时间戳转成可读时间 SELECT FROM_UNIXTIME(1700000000, %Y-%m-%d %H:%i:%s);数据库里存时间戳建议字段直接用BIGINT不要用INT避免 2038 年问题。如果业务只关心日期时间直接用DATETIME也无妨但应用层必须统一规范存储时区。3.4 Excel、SecureCRT、MobaXterm桌面工具的时间戳转换不少非专业运维的人搜“Excel 时间戳转时间”大多是从系统里导出了 10 位或 13 位数字想变成可读日期。Excel 里处理 Unix 秒级时间戳公式是(A1/86400)DATE(1970,1,1)如果你是北京时间还需要加 8 小时(A1/86400)DATE(1970,1,1)8/24比如在 A1 单元格输入1700000000B1 输入上面公式再把 B1 格式设置成yyyy-mm-dd hh:mm:ss就能看到2023-11-15 06:13:20。注意 Excel 的日期系统和 Unix 时区换算有差异直接套公式不加时区会差 8 小时。SecureCRT 和 MobaXterm 这类终端工具很多人也在搜“日志时间戳”。SecureCRT 的 Session Options 里Log File 名称可以写成%H-%M-%S.log表示按小时-分钟-秒命名日志文件同时在 Logging 选项里打开 “Start log on connect” 和 “Append to file”。MobaXterm 则可以直接在设置里开启时间戳显示会记录每条命令的本地时间。这些工具记录的时间戳能帮我们定位操作顺序但如果服务器和客户端时区不一致看日志时还是要统一换算。3.5 Windows 错误模块explorer.exe 崩溃里的十六进制时间戳热搜词里有条很具体的 Windows 日志错误应用程序名称: explorer.exe 版本: 6.1.7601.23537 时间戳: 0x57c44efe这里的时间戳不是“崩溃发生时间”而是PE 文件头里的 TimeDateStamp也就是 explorer.exe 这个程序文件的构建时间以 Unix 时间戳的十六进制形式存在。把0x57c44efe转成十进制printf %d\n 0x57c44efe结果是1472483070。再用前面提到的转换思路date -u -d 1472483070会得到2016-08-29 15:00:48 UTC对应的北京时间是2016-08-29 23:00:48。所以这个文件大概是 2016 年 8 月底编译的版本。遇到 Windows 报错里的时间戳别当成问题发生时间。它更像一个版本指纹用来确认你运行的具体程序文件是哪个编译批次。排查 DLL 冲突、补丁版本时这类时间戳比对还挺有用。4. 时间戳问题的排查工具箱与好习惯4.1 命令行快速换算三秒钟得到答案Linux 上最常用的时间戳换算就是date命令。# 当前时间戳 date %s # 时间戳转 UTC 时间 date -u -d 1700000000 # 时间戳转本地时间 date -d 1700000000macOS 上把-d换成-rdate -r 1700000000如果想把当前时间转成时间戳date %sWindows 的 PowerShell 也可以处理 Unix 时间戳[DateTimeOffset]::FromUnixTimeSeconds(1700000000).ToLocalTime()这套组合拳基本覆盖了日常排查日志时间戳的最高频场景。4.2 日志里批量转换时间戳的脚本线上日志如果打的是裸时间戳人眼看会崩溃排查故障非常痛苦。一个很实用的小技巧是直接用 awk 把时间戳字段替换成可读时间。假设日志每行开头是时间戳1700000000 request_idabc 1700000030 request_iddef用 awk 加 system 函数转换awk { cmddate -u -d $1 \047%Y-%m-%d %H:%M:%S\047; cmd | getline t; close(cmd); $1t; print } app.log实际生产环境里我更喜欢用 Perl 或 Python 写一次性脚本来处理逻辑清晰也方便处理毫秒、时区等问题。比如把 13 位毫秒时间戳批量转成北京时间python3 -c import sys, datetime for line in sys.stdin: ts int(line.strip()) dt datetime.datetime.fromtimestamp(ts / 1000, tzdatetime.timezone(datetime.timedelta(hours8))) print(dt.strftime(%Y-%m-%d %H:%M:%S)) timestamps.txt关键是先确认日志里到底是秒还是毫秒再决定要不要除以 1000不然转出来的时间会离奇地近 1970。4.3 主流语言解析时间戳的注意点不同语言对时间戳的单位定义不同这是跨系统对接时最容易埋雷的地方。Javalong ts 1700000000; Instant instant Instant.ofEpochSecond(ts); ZonedDateTime beijing instant.atZone(ZoneId.of(Asia/Shanghai)); System.out.println(beijing);Got : time.Unix(1700000000, 0) fmt.Println(t.In(time.FixedZone(CST, 8*3600)))Pythonfrom datetime import datetime, timezone print(datetime.fromtimestamp(1700000000, tztimezone.utc))所有语言解析时都要先确认入参是秒还是毫秒。比如 Java 的Instant.ofEpochMilli和Instant.ofEpochSecond差 1000 倍混用一次时间直接回到 1970 年。4.4 我建议长期遵守的 4 条时间处理铁律这些年排查过太多时间戳事故最后总结出几条特别实用的原则写代码和运维时都应该遵守。第一存储和传输永远用 UTC。数据库字段如果必须存时间戳建议直接存 UTC 秒或毫秒展示层再做本地化。不要在数据库里存“北京时间”这种带时区含义的字符串后续一换服务器就全乱。第二明确单位并在代码里写清楚。参数名、字段名、接口文档里标注释比如timestamp_ms表示毫秒timestamp_s表示秒。别裸写一个timestamp就完事过两个月自己都会忘。第三传输优先用 ISO 8601 字符串。比如2024-03-01T08:00:0008:00它自带时区信息比裸整数更容易排查问题。只在性能敏感的接口里才退而使用毫秒时间戳。第四别手写日期运算。日期加减、时区转换、闰秒处理都交给语言标准库或成熟的时间库。手写闰年判断、天数和秒数换算常规场景可能没事遇到边界就是大事故。5. 最后分享两个实操小技巧如果看到系统里有大量1970-01-01的脏数据先别急着清理。确认这些数据是“该更新但没更新”的正常值还是“解析失败默认成 0”的坏数据。前者可能是业务上真的没有发生时间后者才是 bug。再一个日志打印时间戳时最好统一打印成yyyy-MM-dd HH:mm:ss.SSS和 UTC 两个版本。调试时直接看可读时间做数据对齐时用时间戳避免楼道里来回喊“这到底是几点钟的日志”。1970 年这个起点说穿了就是一把被全世界 Unix 系接受的“尺子”。搞懂它排查时间问题就有了一个稳定的锚点。下一次再看到1970-01-01先算算时间戳是不是 0再查查单位是不是搞反了问题往往马上就有答案。
返回列表