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 是协调世界时的缩写,你可以把它理解成一种“绝对时间基准”,不随国家、季节变化,也不存在夏令时这种人为调整。北京时间的标准说法是 UTC+8,也就是比 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 时区不同显示就不同
前面说过,时间戳是绝对的,时区是相对的。再举一个具体例子来加深理解。
执行带时区参数的命令:
TZ=UTC date -d @0 TZ=Asia/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。如果想直接格式化成字符串,可以再套一层TEXT:
=TEXT((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_COMMAND='echo "[$(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 don't 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 文件里加RuntimeDirectory=mysqld或直接在/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 TZ=UTC date -d @1704067200hostname -I是我个人最推荐的一条:输出干净,不带多余的回环地址和链路层信息,适合脚本里去取本机 IP。
4.5 时间戳转换常见问题速查表
把我这些年遇到的高频问题统一整理成表,方便直接对号入座:
| 现象 | 根本原因 | 处理建议 |
|---|---|---|
| 显示 1970-01-01 08:00:00 | 时间戳为 0 或字段未赋值 | 检查写入逻辑,确认初始化是否遗漏 |
| 显示 1970-01-01 00:00:00 | 按 UTC 展示了零值时间戳 | 确认展示层时区设置 |
| 显示 1901-12-13 | 32 位有符号时间戳溢出变负数 | 改用 64 位存储,排查溢出点 |
| JS 日期显示 1970 且时间不对 | 秒级时间戳被当毫秒传给 Date | 确认单位,秒级需乘 1000 |
| Java 序列化 Date 输出一串数字 | 没配日期格式 | 加 @JSONField(format="yyyy-MM-dd HH:mm:ss") |
| Excel 里时间戳是一串 10 位数字 | 没有转换公式 | 用 (A1/86400)+DATE(1970,1,1)+时区偏移 |
| 日志没有时间戳 | 终端工具未启用时间记录 | 配置 SecureCRT/MobaXterm 时间戳变量 |
| 系统时间重启就变 1970 | CMOS 电池没电或缺少 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 至少能少一半。