1970-01-01 08:00:00这个时间凡是写过代码的人迟早都会碰上。你可能在数据库里看到日期字段全是这个默认值可能在某个嵌入式设备上电后发现系统时间“穿越回原点”更常见的是排查日志时看到一整页 1970 开头的脏数据。它的本质就是 Unix 时间戳的零点是所有 Unix/Linux 系统计算时间的起始坐标。为什么偏偏是 1970 年而不是别的时间时间戳到底怎么算、怎么用、有哪些隐藏的坑这篇文章一次性讲透顺便把开发中最高频的一批时间戳转换需求和故障排查经验整理成一份能直接抄作业的清单。1. 1970-01-01到底是哪来的Unix时间戳的起源与定义1.1 时间戳是个什么概念Unix 时间戳英文叫 Unix timestamp定义非常简单从某一个固定的起始时刻开始到当前时刻所经过的秒数。这个固定起始时刻就是 1970 年 1 月 1 日 00:00:00 UTC协调世界时。也就是说我写下这句话的这一刻距离那个原点已经过去了五十多年的秒数这个秒数用一个整数表示就是时间戳。它和“某年某月某日”这种人类可读的日期格式完全不同是一串纯数字。比如1700000000、1577836800一眼扫过去没有任何直观意义但计算机非常喜欢这种表示法——存储方便、比较大小方便、计算两个时间点之间的间隔更是直接做个减法就行。国内很多小伙伴第一次接触这个概念都是在日志或者数据库里看到1970-01-01 08:00:00心里嘀咕明明定义里写的是 00:00:00为什么显示成了 08:00:00答案很简单因为中国在东八区UTC 零点的时刻放到东八区来看就是早上八点。这个细节后面我会专门展开讲因为它就是很多人时间戳转换出错的第一道坎。1.2 为什么偏偏是1970年1月1日这个问题几乎每个学计算机的人都问过但市面上的解释往往含糊其辞。我把能查到的历史脉络整理一下。事情要追溯到上世纪 60 年代末、70 年代初贝尔实验室的 Ken Thompson 和 Dennis Ritchie 等人正在开发 Unix 操作系统。操作系统内核里必须有一套统一的时间表示方式不可能每个人各写各的。当时他们面临一个选择给时间定一个“原点”epoch所有时间都用从这个原点开始的秒数来表示。那为什么选 1970 年 1 月 1 日最直接的原因是Unix 早期版本的系统开发工作正是在 1969-1970 年前后完成的。1970 年被写进 Unix 的早期实现里作为一个相对“新”的、当时看来毫无历史包袱的起点。后来 POSIX 标准在 1988 年左右正式把1970-01-01 00:00:00 UTC定为系统时间原点所有遵循 POSIX 的系统Linux、macOS、BSD 以及 Windows 上的很多模拟层都沿用这个约定慢慢就成了行业事实标准。如果你去翻系统源码会看到很多地方的注释直接写着00:00:00 Jan 1, 1970。它没有太多玄妙的数学道理就是一个被历史选中、被标准固化的“坐标原点”。可以类比为地图上的经纬度零点或者中国古代的“纪元”——总要定一个起点大家都从这儿开始数才好统一口径。这里还有一个补充信息在 Unix 更早期的原型里其实尝试过用 1971 年、甚至用 1/60 秒作为计时单位后来因为 32 位整数溢出风险、计算复杂度等原因最终统一成“秒”和“1970-01-01 00:00:00 UTC”这个组合。换句话说1970 不是什么神秘的“宇宙大爆炸时刻”而是工程师在特定历史条件下做出的最优取舍。1.3 时间戳的“基地”UTC与时区要理解时间戳必须先理解 UTC。UTC 是协调世界时的缩写你可以把它理解成一种“绝对时间基准”不随国家、季节变化也不存在夏令时这种人为调整。北京时间的标准说法是 UTC8也就是比 UTC 快 8 个小时纽约时间则是 UTC-5冬令时或 UTC-4夏令时。关键点来了Unix 时间戳本身不含任何时区信息。它是一个绝对时刻的秒数表示全球任何地方同一个物理时刻的 Unix 时间戳数值都是完全一样的。比如北京时间 2024 年 1 月 1 日早上 8 点和 UTC 时间 2024 年 1 月 1 日凌晨 0 点是同一个时刻它们的 Unix 时间戳都是1704067200。那为什么你在 Linux 里执行date -d 1704067200在中国机器上会显示2024-01-01 08:00:00 CST而在美国机器上会显示2023-12-31 19:00:00 EST因为date命令在输出“人能看懂的日期”时会自动把绝对时间戳转换成当前系统的本地时区显示。时间戳数值没有变变的只是“展示方式”。理解了这一点很多诡异现象就有了解释为什么数据库里存了一个 0查出来却是 1970-01-01 08:00:00因为存储层把 0 当作 Unix 时间戳展示层又按东八区转成了本地时间于是 0 就变成了“1970 年 1 月 1 日早上 8 点”。这一点相当重要后面排查问题的时候会反复用到。2. 时间戳是怎么计算的从秒到日期再算到2038年2.1 一个数字怎么变成日期手工把一个时间戳换算成日期其实不复杂。以 Unix 时间戳86400为例一天有 24 × 3600 86400 秒所以它代表从 1970-01-01 00:00:00 UTC 往后推了一天即 1970-01-02 00:00:00 UTC。向东八区显示就是 1970-01-02 08:00:00。通用的手动换算步骤是把时间戳数值 N 除以 86400取整数部分得到天数 D余数部分是当天已经过去的秒数 S。从 1970-01-01 开始往后退 D 天得到日期。再用 S 换算成具体的小时、分钟、秒。如果想显示成某个时区的本地时间UT% 时间基础上加上该时区与 UTC 的小时偏移。比如1577836800除以 86400 得到 18262 天整除没有余数。1970 年 1 月 1 日加 18262 天正好是 2020 年 1 月 1 日UTC 零时东八区显示为 08:00:00。这就是为什么1577836800被很多开发者直接记作“2020 年的标准开年时间戳”。如果你不想手工算Linux 下一行命令搞定# 查看时间戳对应的 UTC 时间 date -u -d 1577836800 # 查看本地时区时间 date -d 1577836800 # 获得当前时间戳 date %sdate -d 秒数是排查问题时最常用的命令务必记牢。2.2 时区不同显示就不同前面说过时间戳是绝对的时区是相对的。再举一个具体例子来加深理解。执行带时区参数的命令TZUTC date -d 0 TZAsia/Shanghai date -d 0在 UTC 时区下0 显示为1970-01-01 00:00:00在 Asia/Shanghai东八区下显示为1970-01-01 08:00:00。同一个 0两种显示都对因为它们是同一个时刻。这个知识点在日志分析里尤其重要。很多服务器日志会混杂存储时间戳和本地时间如果不确认日志记录的时区很容易把同一时刻的事件误判成相隔 8 小时的两件事。我处理过的案例里就有同事因为把东八区的时间硬塞进 UTC 的字段导致监控告警整整偏移了 8 个小时查了半天才发现是时区处理不一致。2.3 2038年问题与64位时间戳聊到时间戳必须提“2038 年问题”。时间戳最初是用 32 位有符号整数存储的32 位有符号整数能表示的最大值是 2^31 - 1 2147483647。这个秒数对应的 UTC 时间是 2038 年 1 月 19 日 03:14:07。再往后 1 秒32 位有符号整数就会溢出翻转成负数系统时间会瞬间“回退”到 1901 年 12 月 13 日 20:45:52 UTC。这在年长一些的程序员圈子里是教科书写烂了的经典问题也是当年 Y2K千年虫问题之外的另一个定时炸弹。我们现在的操作系统基本都用 64 位时间戳了64 位有符号整数能表示到约 2920 亿年之后完全够用。但现代系统里仍藏着很多 32 位时间戳的角落老式嵌入式设备、旧路由器固件、某些工业控制器、还有不少 C 语言老项目里直接用int存时间戳。如果你在维护这类系统建议尽早把存储时间戳的字段改成long long或 64 位类型。这里给一个非常实用的建议代码里存时间戳永远不要用int用long、long long或语言对应的 64 位整型。你永远不知道你的代码会被部署到什么平台上而这个习惯能帮你避免一大批潜在溢出问题。3. 开发场景里最常用到的时间戳操作3.1 JavaScript里取时间戳与转换前端开发里时间戳的坑我见得太多了。JavaScript 的Date对象返回的时间戳单位是毫秒不是秒。这一点是新手最容易踩的坑。// 获取当前时间戳单位是毫秒 console.log(Date.now()); // 例如 1735689600000 // 秒级时间戳需要手动除以 1000 console.log(Math.floor(Date.now() / 1000)); // 字符串转时间戳 let ts new Date(2024-01-01T00:00:00Z).getTime(); console.log(ts); // 1704067200000毫秒 // 时间戳转日期注意补上 *1000 let date new Date(1704067200000); console.log(date.toISOString());最常见的错误是后端接口返回的是秒级时间戳前端直接new Date(1704067200)结果日期变成1970-01-20左右时间还完全对不上。因为Date构造函数期望毫秒你把一个秒级数值传进去相当于少了 1000 倍的精度。我个人的习惯是后端接口如果返回时间戳统一约定用秒还是毫秒写在接口文档里前端拿到后第一件事就是确认单位再决定要不要* 1000。3.2 Java与fastjson中时间转时间戳的坑后端 Java 开发中fastjson 是一个出镜率很高的 JSON 处理库但它处理日期字段时有一些默认行为会让不少人头疼。默认情况下JSON.toJSONString(new Date())输出的是一个数字时间戳不是格式化字符串。如果接口的一个字段是Date序列化后前端拿到的可能是一串毫秒值前端如果没有统一处理直接展示就会变成不可读的 1234567890123。解决方案有三种在字段上用JSONField(format yyyy-MM-dd HH:mm:ss)让 fastjson 序列化时输出格式化字符串。public class OrderVO { JSONField(format yyyy-MM-dd HH:mm:ss) private Date createTime; }在全局配置里处理JSON.toJSONString(obj, SerializerFeature.WriteDateUseDateFormat);遇到LocalDateTime时先转换成Instant再取毫秒/秒LocalDateTime now LocalDateTime.now(); long epochSecond now.toInstant(ZoneOffset.ofHours(8)).getEpochSecond(); long epochMilli now.toInstant(ZoneOffset.ofHours(8)).toEpochMilli();这里要特别提醒LocalDateTime本身不携带时区信息toInstant时必须显式指定一个ZoneOffset否则程序不知道把这个“本地日期时间”放进哪个绝对时刻。很多人就是把ZoneOffset.ofHours(8)写成了ZoneOffset.UTC导致时间戳凭空少了 8 小时。3.3 Excel时间戳转时间Excel日期序列号与Unix时间戳Excel 里处理 Unix 时间戳是个经典需求但 Excel 自带的日期系统跟 Unix 时间戳根本不是一回事。Excel 把日期存成一个序列号起点是 1900 年 1 月 1 日Windows 版本而 Unix 时间戳的起点是 1970 年 1 月 1 日。两者之间差了大约 25569 天。如果你的 Excel 单元格 A1 放的是一个 Unix 秒级时间戳想把它转成可读日期公式可以这样写(A1/86400)DATE(1970,1,1)8/24最后的8/24是东八区的时区偏移。如果 UTC 显示去掉8/24。如果想直接格式化成字符串可以再套一层TEXTTEXT((A2/86400)DATE(1970,1,1)8/24,yyyy-mm-dd hh:mm:ss)注意如果数据源给的是毫秒级时间戳要先除以 1000((A2/1000)/86400)DATE(1970,1,1)8/24这里还有一个大坑Windows 版 Excel 有个著名的 1900 年闰年 bug——它会错误地把 1900 年当作闰年处理导致序列号 60 对应的日期是虚拟的 1900-02-29。虽然我们在算 Unix 时间戳转换时DATE(1970,1,1)已经自动规避了大部分问题但在处理 1970 年以前的日期时还是要额外小心微软这个历史包袱。3.4 日志工具里的时间戳SecureCRT、MobaXterm怎么配置排查线上问题日志里的时间戳是救命稻草。很多人用 SecureCRT 登录设备抓日志默认抓下来的文件是没有时间信息的一个问题发生几分钟后你根本看不出哪条输出对应的是哪个时刻。SecureCRT 里给日志加时间戳的方法菜单栏Options - Session Options - Log File在 Log Filename 里可以加变量比如%Y-%m-%d_%H-%M-%S_session.log在 On Connect 和 On Disconnect 相关选项里勾选记录时间戳的选项把%H:%M:%S变量加到每行日志前缀里。MobaXterm 也类似Settings - Terminal或者 Session 的 Terminal settings 里可以开启 Terminal timestamp 显示让每行命令输出前带当前时间。还有一个更省事的方案直接在 shell 里配置PROMPT_COMMAND让每个命令执行前自动打印一条时间记录export PROMPT_COMMANDecho [$(date %Y-%m-%d %H:%M:%S)]这样就算终端工具不带时间戳功能也能在日志里留下时间印记。注意这类方案在交互式终端里会有额外输出如果只是临时抓取问题现场把它加进脚本里可能更干净。4. 常见故障排查与实用技巧实录4.1 系统时间突然变成1970-01-01的原因与修复这是运维和嵌入开发里非常高频的一个问题。系统时间突然变成 1970 年通常有几种原因主板 CMOS 电池没电了。服务器或工控机断电重启后硬件时钟无法保持系统在启动时读不到有效的硬件时间于是从 1970 这个默认原点开始走。系统没配置 NTP又恰好经历过一次异常断电或重启RTC实时时钟数据没有被正确写入。嵌入式设备没有 RTC 芯片每次上电时间都会回到编译内核时的时间很多就是 1970-01-01。文件系统在拷贝时老旧的 FAT 格式或者网络文件系统不支持某些时间戳精度导致文件时间被重置为 epoch 值。排查命令可以按这个顺序来date # 查看当前系统时间 hwclock -r # 查看硬件时间 timedatectl # 查看系统时间、RTC、NTP状态 ntpq -p # 查看NTP同步情况修复手段也很直接如果 CMOS 电池没电换电池如果只是硬件时间丢了手动或通过 NTP 重新设置# 将系统时间写入硬件时钟 hwclock --systohc # 启用 NTP 自动同步 timedatectl set-ntp true这里给一个排障小经验当你看到业务库里某个时间字段突然变成 1970先不要急着改数据去登录那台应用服务器看一眼系统时间。很多“数据库时间不对”的告警根因是应用服务器的时间跳变而不是数据库写入逻辑出错。4.2 MySQL启动报错与日志时间戳定位热词里有一条 MySQL 相关错误说得很具体mysqld_safe directory /var/run/mysqld for unix socket file dont exists。这是 MySQL 启动时很经典的一个问题。原因是 MySQL 需要通过 Unix socket 文件进行本地连接而 socket 文件默认放在/var/run/mysqld/目录下如果系统重启后这个目录没有被创建或者目录属主不对mysqld_safe 就会报错退出。解决方式mkdir -p /var/run/mysqld chown mysql:mysql /var/run/mysqld systemctl start mysqld更想让目录重启后自动创建并设置权限的话在 systemd 的 unit 文件里加RuntimeDirectorymysqld或直接在/etc/tmpfiles.d/里写一条配置。那这条错误跟时间戳有什么关系关系在于排查这个问题的时候你的思维不能只盯着错误文本。MySQL 的错误日志/var/log/mysql/error.log里会带有时间戳如果系统时间已经跳变回 1970日志时间就和真实时间对不上原本早上 10 点发生的崩溃日志里可能显示成 1970-01-01 08:02。所以我接到这类报障第一件事永远是date看时间对不对再顺着时间戳看日志范围避免在一堆错误的时间序列里找错了方向。另外服务器时间跳变还会引发 binlog 时间戳、主从复制延迟异常等一系列连锁反应属于牵一发而动全身的问题。4.3 Windows崩溃日志里explorer.exe的时间戳怎么读Windows 事件查看器里经常能看到这种错误记录错误应用程序名称: explorer.exe版本: 6.1.7601.23537时间戳: 0x57c44efe 错误模块名称: shell32.dll版本: 6.1.7601.23451时间戳: 0x56b0697a很多人看到“时间戳”三个字就以为是崩溃发生的时刻其实这里的“时间戳”是 PE 文件头里的一个字段表示这个 exe/dll 文件是哪个时候编译链接出来的单位是 Unix 秒从 1970 年开始算但显示为十六进制。0x57c44efe换算成十进制是多少我用计算逻辑拆一下57C44EFE十六进制 1472056062十进制。再用 Unix 时间戳换算工具或者date -d 1472056062得到大约是2016-08-24。再配合版本号6.1.7601.23537能看出这是 Windows 7 SP1 在 2016 年某个补丁更新之后的 explorer.exe 版本。这个信息在排查系统崩溃时有实际价值如果崩溃的模块是某个第三方 shell 插件时间戳往往对应插件发布版本可以直接去版本库比对这个时间段改了什么代码如果是系统模块时间戳能帮你确认有没有被第三方工具覆盖过系统文件——正常情况下同一个版本号的文件时间戳应该是一致的如果时间戳异常就要考虑 DLL 劫持或文件损坏。查看 Windows 文件时间戳PowerShell 可以这样写(Get-Item C:\Windows\explorer.exe).VersionInfo或者用 Visual Studio 自带的dumpbin /headers查看 PE 头的 TimeDateStamp 字段。4.4 Unix/Linux查看IP地址和时间相关命令速查热词里还出现了一条“unix查看ip地址命令”顺手一起整理掉。排查环境时我们经常要同时确认机器的 IP 和时间这两者都是确定“我是谁、我在哪、现在几点”的基础信息。# 查看 IP 地址 ip addr show hostname -I ifconfig # 老命令部分系统需要额外安装 net-tools # 查看当前时间戳和日期 date %s date # 查看硬件时钟 hwclock -r # 时间戳与日期互转 date -d 1704067200 TZUTC date -d 1704067200hostname -I是我个人最推荐的一条输出干净不带多余的回环地址和链路层信息适合脚本里去取本机 IP。4.5 时间戳转换常见问题速查表把我这些年遇到的高频问题统一整理成表方便直接对号入座现象根本原因处理建议显示 1970-01-01 08:00:00时间戳为 0 或字段未赋值检查写入逻辑确认初始化是否遗漏显示 1970-01-01 00:00:00按 UTC 展示了零值时间戳确认展示层时区设置显示 1901-12-1332 位有符号时间戳溢出变负数改用 64 位存储排查溢出点JS 日期显示 1970 且时间不对秒级时间戳被当毫秒传给 Date确认单位秒级需乘 1000Java 序列化 Date 输出一串数字没配日期格式加 JSONField(formatyyyy-MM-dd HH:mm:ss)Excel 里时间戳是一串 10 位数字没有转换公式用 (A1/86400)DATE(1970,1,1)时区偏移日志没有时间戳终端工具未启用时间记录配置 SecureCRT/MobaXterm 时间戳变量系统时间重启就变 1970CMOS 电池没电或缺少 NTP换电池并启用 NTP 自动同步本地时间凭空少了 8 小时LocalDateTime 转换时区指定错误toInstant 时明确使用东八区偏移最后再分享一个我个人的小习惯在代码库里常备一个“时间戳调试工具类”其实不需要只要记住几个关键锚点就行。0 对应 1970-01-01 08:00:00东八区2147483647 对应 2038-01-19 11:14:07东八区1577836800 对应 2020-01-01 08:00:00东八区。开发调试时用date -d 0快速验证当前环境时区再配合date %s看当前时间戳大部分时间问题都能在五分钟内定位。写代码时统一用 UTC 存储、展示时再转本地时区日志里坚持带时间和时区这三条做到了你的时间戳相关 Bug 至少能少一半。