Unix 时间戳完整指南

从基础概念到进阶应用,全面了解 Unix Timestamp 的运作原理与实务技巧

目录

什么是 Unix 时间戳?

Unix 时间戳(Unix Timestamp),又称为 POSIX 时间或 Epoch 时间,是一种用来追踪时间的系统。它的定义非常简单:从 1970 年 1 月 1 日 00:00:00 UTC 到指定时间所经过的秒数

这个起始时间点——1970 年 1 月 1 日 00:00:00 UTC——被称为 Unix Epoch(纪元)。之所以选择这个时间,是因为 Unix 操作系统最初在 1970 年代开发,开发者需要一个简单且统一的时间参考点。

运作原理

Unix 时间戳本质上就是一个不断递增的整数计数器。每过一秒,这个数字就加 1。例如:

日期时间 (UTC) Unix 时间戳
1970-01-01 00:00:00 0
1970-01-01 00:01:00 60
1970-01-01 01:00:00 3600
1970-01-02 00:00:00 86400
2000-01-01 00:00:00 946684800
2024-01-01 00:00:00 1704067200
2025-01-01 00:00:00 1735689600

这种计时方式的巧妙之处在于它完全不涉及时区、夏令时、闰年等复杂因素。无论你身在北京、纽约还是伦敦,同一个瞬间的 Unix 时间戳都是同一个数字。

需要注意的是,Unix 时间戳不计算闰秒。UTC 偶尔会插入闰秒来修正原子钟与地球自转的差异,但 Unix 时间戳会假设每天都是精确的 86400 秒。这意味着 Unix 时间戳和真正的 UTC 之间可能存在几十秒的微小差异,但对于绝大多数应用来说,这个差异可以忽略不计。

时间戳的表示方式

虽然「Unix 时间戳」最初定义为秒级精度,但随着应用场景的多样化,时间戳现在有多种精度等级:

秒级时间戳(10 位数)

这是最经典的 Unix 时间戳格式,以秒为单位,通常是一个 10 位的整数。

1700000000  →  2023-11-14 22:13:20 UTC
1735689600  →  2025-01-01 00:00:00 UTC

大多数服务器端编程语言(如 PHP、Python、Go)默认使用秒级时间戳。

毫秒级时间戳(13 位数)

以毫秒为单位(1 秒 = 1000 毫秒),通常是一个 13 位的整数。

1700000000000  →  2023-11-14 22:13:20.000 UTC
1735689600123  →  2025-01-01 00:00:00.123 UTC

JavaScript 的 Date.now() 和 Java 的 System.currentTimeMillis() 都返回毫秒级时间戳。这是前端开发中最常遇到的格式。

微秒级时间戳(16 位数)

以微秒为单位(1 秒 = 1,000,000 微秒),通常是一个 16 位的整数。

1700000000000000  →  2023-11-14 22:13:20.000000 UTC

Python 的 time.time_ns() // 1000、PostgreSQL 的内部时间表示都使用微秒级精度。适用于需要更高精度的场景,如性能测量。

纳秒级时间戳(19 位数)

以纳秒为单位(1 秒 = 1,000,000,000 纳秒),通常是一个 19 位的整数。

1700000000000000000  →  2023-11-14 22:13:20.000000000 UTC

Go 语言的 time.Now().UnixNano() 返回纳秒级时间戳。InfluxDB、Prometheus 等时序数据库也常使用纳秒精度。

快速辨别时间戳精度

精度 位数 示例 常见来源
10 位 1700000000 PHP, Python, Go, Unix shell
毫秒 13 位 1700000000000 JavaScript, Java, Dart
微秒 16 位 1700000000000000 PostgreSQL, Python (time_ns)
纳秒 19 位 1700000000000000000 Go, InfluxDB, Prometheus

拿到一个时间戳时,最简单的判断方式就是看位数:10 位是秒、13 位是毫秒、16 位是微秒、19 位是纳秒。

为什么使用 Unix 时间戳?

在软件开发中,Unix 时间戳被广泛采用有以下几个重要原因:

1. 与时区无关

Unix 时间戳是一个绝对值,不依赖于任何特定时区。无论系统设定为哪个时区,同一瞬间的时间戳永远相同。这避免了在跨时区系统中最常见的时间处理错误。例如,一个在北京(UTC+8)产生的事件,和在纽约(UTC-5)读取的事件,使用时间戳就不会有混淆。

2. 易于比较和计算

因为时间戳就是一个整数,比较两个时间的先后只需要比较数字大小。计算时间差只需要做减法:

时间差(秒)= 时间戳B - 时间戳A
时间差(天)= (时间戳B - 时间戳A) / 86400

不需要考虑不同月份的天数、闰年等复杂逻辑。

3. 存储空间小

一个 32 位整数只占 4 个字节,64 位也只占 8 个字节。相比之下,ISO 8601 格式的字符串(如 2025-01-01T00:00:00+08:00)需要 25 个字节以上。在大量数据存储的场景下,时间戳可以显著节省空间。

4. 通用标准

几乎所有编程语言、操作系统和数据库都支持 Unix 时间戳。它是系统之间交换时间信息最可靠的格式之一。API 设计中使用时间戳可以避免不同系统对日期字符串格式解析不一致的问题。

5. 不受夏令时影响

夏令时(DST)会导致一年中有一天是 23 小时、另一天是 25 小时,这使得基于本地时间的计算变得极其复杂。Unix 时间戳完全不受 DST 影响,因为它基于 UTC。

何时不适合用时间戳?

尽管时间戳有很多优点,但在某些场景下,直接使用日期时间格式可能更合适:

各编程语言中的时间戳

以下是各种主流编程语言和数据库中获取与转换时间戳的代码示例。

JavaScript

// 获取当前时间戳(毫秒)
const msTimestamp = Date.now();           // 1700000000000
const msTimestamp2 = new Date().getTime(); // 同上

// 获取当前时间戳(秒)
const secTimestamp = Math.floor(Date.now() / 1000); // 1700000000

// 时间戳转日期
const date = new Date(1700000000 * 1000); // 注意:要乘以 1000
console.log(date.toISOString());          // "2023-11-14T22:13:20.000Z"
console.log(date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }));
// "2023/11/15 上午6:13:20"

// 日期转时间戳
const ts = new Date('2025-01-01T00:00:00Z').getTime() / 1000;
// 1735689600

注意:JavaScript 原生使用毫秒,与 Unix 标准的秒之间需要乘除 1000。这是最常见的错误来源之一。

Python

import time
from datetime import datetime, timezone

# 获取当前时间戳(秒,浮点数)
timestamp = time.time()              # 1700000000.123456

# 获取当前时间戳(秒,整数)
timestamp_int = int(time.time())     # 1700000000

# 时间戳转日期(UTC)
dt_utc = datetime.fromtimestamp(1700000000, tz=timezone.utc)
print(dt_utc)  # 2023-11-14 22:13:20+00:00

# 时间戳转日期(本地时区)
dt_local = datetime.fromtimestamp(1700000000)
print(dt_local)  # 2023-11-15 06:13:20(北京时间 UTC+8)

# 日期转时间戳
dt = datetime(2025, 1, 1, tzinfo=timezone.utc)
ts = int(dt.timestamp())             # 1735689600

PHP

<?php
// 获取当前时间戳(秒)
$timestamp = time();                  // 1700000000

// 获取当前时间戳(毫秒)
$msTimestamp = round(microtime(true) * 1000); // 1700000000000

// 时间戳转日期
echo date('Y-m-d H:i:s', 1700000000);
// "2023-11-15 06:13:20"(服务器时区,假设 Asia/Shanghai)

// 指定时区转换
$dt = new DateTime('@1700000000');
$dt->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo $dt->format('Y-m-d H:i:s');     // "2023-11-15 06:13:20"

// 日期转时间戳
$ts = strtotime('2025-01-01 00:00:00 UTC'); // 1735689600
?>

Go

package main

import (
    "fmt"
    "time"
)

func main() {
    // 获取当前时间戳
    now := time.Now()
    secTs := now.Unix()      // 秒级:1700000000
    msTs := now.UnixMilli()  // 毫秒级:1700000000000
    nsTs := now.UnixNano()   // 纳秒级:1700000000000000000

    // 时间戳转日期
    t := time.Unix(1700000000, 0)
    fmt.Println(t.UTC())
    // 2023-11-14 22:13:20 +0000 UTC

    // 转换至上海时区
    loc, _ := time.LoadLocation("Asia/Shanghai")
    fmt.Println(t.In(loc))
    // 2023-11-15 06:13:20 +0800 CST

    // 日期转时间戳
    dt := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
    fmt.Println(dt.Unix()) // 1735689600
}

Java

import java.time.*;

public class TimestampExample {
    public static void main(String[] args) {
        // 获取当前时间戳(毫秒)
        long msTs = System.currentTimeMillis();       // 1700000000000

        // 获取当前时间戳(秒)
        long secTs = Instant.now().getEpochSecond();  // 1700000000

        // 时间戳转日期(UTC)
        Instant instant = Instant.ofEpochSecond(1700000000L);
        System.out.println(instant);
        // 2023-11-14T22:13:20Z

        // 转换至上海时区
        ZonedDateTime shanghai = instant.atZone(ZoneId.of("Asia/Shanghai"));
        System.out.println(shanghai);
        // 2023-11-15T06:13:20+08:00[Asia/Shanghai]

        // 日期转时间戳
        ZonedDateTime dt = ZonedDateTime.of(
            2025, 1, 1, 0, 0, 0, 0, ZoneId.of("UTC")
        );
        long ts = dt.toEpochSecond(); // 1735689600
    }
}

MySQL

-- 获取当前时间戳(秒)
SELECT UNIX_TIMESTAMP();                  -- 1700000000

-- 时间戳转日期
SELECT FROM_UNIXTIME(1700000000);         -- '2023-11-15 06:13:20'(服务器时区)

-- 带格式的转换
SELECT FROM_UNIXTIME(1700000000, '%Y-%m-%d %H:%i:%s');

-- 日期转时间戳
SELECT UNIX_TIMESTAMP('2025-01-01 00:00:00');  -- 1735660800(依服务器时区)

-- 查询特定时间范围的数据
SELECT * FROM events
WHERE created_at >= UNIX_TIMESTAMP('2025-01-01')
  AND created_at <  UNIX_TIMESTAMP('2025-02-01');

PostgreSQL

-- 获取当前时间戳(秒)
SELECT EXTRACT(EPOCH FROM NOW());          -- 1700000000.123456

-- 时间戳转日期
SELECT TO_TIMESTAMP(1700000000);           -- 2023-11-15 06:13:20+08

-- 带时区的转换
SELECT TO_TIMESTAMP(1700000000) AT TIME ZONE 'UTC';
-- 2023-11-14 22:13:20

SELECT TO_TIMESTAMP(1700000000) AT TIME ZONE 'Asia/Shanghai';
-- 2023-11-15 06:13:20

-- 日期转时间戳
SELECT EXTRACT(EPOCH FROM TIMESTAMP WITH TIME ZONE '2025-01-01 00:00:00 UTC');
-- 1735689600

常见问题与陷阱

在实际开发中,时间戳处理有不少容易踩到的坑。以下列出最常见的问题及其解决方案。

1. 时区混淆(8 小时偏差)

这是中国开发者最常遇到的问题。由于北京时间(Asia/Shanghai)是 UTC+8,如果将本地时间误当作 UTC 来转换,或反过来,就会出现刚好 8 小时的偏差。

// 错误示范:忽略时区
new Date('2025-01-01 00:00:00')
// 浏览器可能解读为本地时间,也可能解读为 UTC,行为不一致!

// 正确做法:明确指定时区
new Date('2025-01-01T00:00:00+08:00')  // 北京时间
new Date('2025-01-01T00:00:00Z')       // UTC 时间

解决方案:在处理时间时,永远明确指定时区。存储时使用 UTC,显示时再转换为用户的本地时区。

2. 秒与毫秒混用

这是第二常见的错误。把秒级时间戳当作毫秒来用,或者反过来,会得到完全错误的结果:

// 错误:把秒级时间戳直接传给 JavaScript Date
new Date(1700000000)
// 1970-01-20T16:13:20.000Z(错误!差了 1000 倍)

// 正确:秒级时间戳需乘以 1000
new Date(1700000000 * 1000)
// 2023-11-14T22:13:20.000Z(正确)

// 同样地,不要把毫秒级时间戳传给期望秒的函数
// Python: datetime.fromtimestamp(1700000000000) → OverflowError!

解决方案:始终确认 API 或函数期望的是秒还是毫秒。拿到时间戳时先看位数:10 位是秒,13 位是毫秒。

3. 2038 年问题

使用 32 位有符号整数存储的 Unix 时间戳将在 2038 年 1 月 19 日溢出。详见下一节

4. 负数时间戳(1970 年之前)

1970 年之前的时间以负数时间戳表示。大多数现代语言和系统支持负数时间戳,但某些旧版 API 或数据库可能不支持:

// 1969-12-31 00:00:00 UTC
Unix 时间戳: -86400

// 1900-01-01 00:00:00 UTC
Unix 时间戳: -2208988800

// PHP 的 strtotime() 在某些旧版本中不支持 1970 年以前的日期
// MySQL 的 TIMESTAMP 类型范围是 1970-2038,不支持负数
// 但 DATETIME 类型可以存储 1000-01-01 到 9999-12-31

5. 浮点数精度问题

某些语言(如 Python 的 time.time())返回浮点数时间戳。IEEE 754 双精度浮点数有约 15-16 位有效数字,而毫秒级时间戳已经有 13 位,因此在微秒精度下可能会出现精度丢失:

# Python 浮点数精度问题
import time
t = time.time()  # 1700000000.1234567(最后几位可能不精确)

# 如需高精度,使用 time.time_ns()(Python 3.7+)
t_ns = time.time_ns()  # 1700000000123456789(整数,无精度丢失)

6. 日期字符串解析的不一致性

不同语言和浏览器对日期字符串的解析行为不一致,这也是使用时间戳(而非日期字符串)进行数据交换的重要原因之一:

// JavaScript 中的陷阱
new Date('2025-01-01')   // 被解析为 UTC 00:00:00
new Date('2025/01/01')   // 被解析为本地时间 00:00:00(差 8 小时!)
new Date('Jan 1, 2025')  // 也是本地时间

// 最安全的做法:使用时间戳或 ISO 8601 格式
new Date(1735689600000)              // 明确无歧义
new Date('2025-01-01T00:00:00Z')     // ISO 8601,明确 UTC

2038 年问题

2038 年问题(又称 Y2K38 或 Epochalypse)是计算机科学中一个已知的时间表示问题,与 2000 年的千年虫问题(Y2K)性质类似。

问题根源

许多早期的系统使用 32 位有符号整数(signed 32-bit integer)来存储 Unix 时间戳。32 位有符号整数的最大值是:

2^31 - 1 = 2,147,483,647

这个数字对应的时间是:

2038 年 1 月 19 日 03:14:07 UTC(星期二)

当时间超过这一刻,32 位整数会发生整数溢出(integer overflow)。在二进制补码表示法中,2,147,483,647 加 1 会变成 -2,147,483,648,对应的时间会「回到」:

1901 年 12 月 13 日 20:45:52 UTC(星期五)

这可能导致依赖时间戳的系统出现各种异常行为,包括数据排序错误、过期判断失效、文件系统损坏等。

受影响的系统

以下类型的系统可能受到 2038 年问题的影响:

解决方案:迁移到 64 位

64 位有符号整数的最大值是:

2^63 - 1 = 9,223,372,036,854,775,807

这对应的时间大约是公元 2920 亿年后,远超过太阳系的预期寿命,因此可以认为是「永远不会溢出」。

目前的迁移现状:

系统/语言 状态 说明
64 位 Linux 已解决 time_t 已是 64 位
32 位 Linux (5.6+) 已解决 内核已支持 64 位时间
Windows 已解决 64 位版本使用 64 位 time_t
macOS 已解决 自 10.6 起使用 64 位 time_t
MySQL 8.0+ 部分解决 DATETIME 类型不受影响;TIMESTAMP 仍有限制
PostgreSQL 已解决 内部使用 64 位微秒
Java / Go / Python 3 已解决 原生使用 64 位或大整数
JavaScript 已解决 使用 64 位浮点数(安全到 285616 年)
嵌入式设备 风险中 许多 32 位设备可能无法更新

开发者该怎么做?

常见问答 (FAQ)

Unix 时间戳从什么时候开始?

Unix 时间戳从 1970 年 1 月 1 日 00:00:00 UTC 开始计算,这个时间点被称为「Unix Epoch」。选择这个日期是因为 Unix 操作系统在 1970 年代初期开发,当时的工程师需要一个方便的起始点。最初 Unix 系统使用 32 位计数器,每秒递增 60 次(即 1/60 秒精度),后来改为每秒递增 1 次(秒精度)。Epoch 时间点的时间戳值为 0。

时间戳可以是负数吗?

可以。负数的 Unix 时间戳表示 1970 年 1 月 1 日之前的时间。例如,-86400 代表 1969 年 12 月 31 日 00:00:00 UTC(Epoch 前一天),-2208988800 代表 1900 年 1 月 1 日 00:00:00 UTC。大多数现代编程语言(Python、Java、Go、JavaScript)都能正确处理负数时间戳。但要注意,某些数据库类型(如 MySQL 的 TIMESTAMP)不支持 1970 年之前的日期。

为什么我的转换结果和预期差了几个小时?

这几乎一定是时区问题。Unix 时间戳本身基于 UTC,但在转换为日期时间字符串时,系统会套用默认时区。如果你在中国(UTC+8),转换结果比预期「早了 8 小时」,可能是你预期的是 UTC 时间,但系统显示了北京时间;反之,如果「晚了 8 小时」,可能是系统以 UTC 显示,但你预期的是北京时间。解决方法是在转换时明确指定时区,例如 JavaScript 中使用 toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })

JavaScript 和 Unix 时间戳有什么不同?

最关键的差异在于精度单位。标准 Unix 时间戳以「秒」为单位(10 位数),而 JavaScript 的 Date.now()new Date().getTime() 返回的是「毫秒」(13 位数)。这意味着:

  • JavaScript 毫秒 ÷ 1000 = Unix 秒
  • Unix 秒 × 1000 = JavaScript 毫秒
  • new Date() 构造函数接受的是毫秒,所以要把秒级时间戳乘以 1000

此外,JavaScript 的 Date 对象内部使用 64 位浮点数,可安全表示到公元 275760 年左右,不受 2038 年问题影响。

32 位和 64 位时间戳有什么区别?

区别在于可表示的时间范围:

  • 32 位有符号整数:范围是 -2,147,483,648 到 2,147,483,647,对应 1901-12-13 到 2038-01-19。超过此范围会溢出,这就是「2038 年问题」。
  • 64 位有符号整数:范围是约 -9.2 × 10^18 到 9.2 × 10^18,可表示的时间范围远超宇宙的年龄,实际上不会溢出。

现代 64 位系统、主流编程语言和数据库大多已采用 64 位时间。开发者需要注意的主要是旧系统的兼容性和数据库字段类型的选择。

如何在数据库中存储时间戳?

有两种主要策略,各有优缺点:

策略一:使用原生时间类型(推荐)

  • MySQL:使用 DATETIME(范围 1000-9999 年)而非 TIMESTAMP(受 2038 限制)
  • PostgreSQL:使用 TIMESTAMPTZ(带时区)或 TIMESTAMP
  • 优点:可读性好、支持原生时间函数、排序比较方便

策略二:使用整数时间戳

  • 使用 BIGINT(64 位)存储秒或毫秒级时间戳
  • 优点:跨数据库移植性好、不受时区设定影响、索引效率高
  • 缺点:不可直接阅读、查询时需要转换

无论哪种方式,都建议统一使用 UTC 存储时间,在应用层再转换为用户的本地时区。