Unix 时间戳是从 1970-01-01 00:00:00 UTC 起算的秒数,不含闰秒。这一句里有两个容易被忽略的点:它是相对 UTC 定义的,和你所在时区无关;它计的是"日历秒",闰秒不参与计数,所以不能拿两个时间戳之差去算物理时间。
这是日常最常遇到的坑。一眼判别的经验规则:
| 位数 | 单位 | 覆盖范围 | 常见来源 |
|---|---|---|---|
| 10 位 | 秒 | 约 2001-09-09 至 2286-11-20 | C、Go、Python、MySQL UNIX_TIMESTAMP() |
| 13 位 | 毫秒 | 同上,精度到毫秒 | JavaScript Date.now()、Java System.currentTimeMillis() |
1700000000 是秒,对应 2023-11-14 22:13:20 UTC;1700000000000 是毫秒,对应同一时刻。把毫秒当秒传给后端,会得到一个公元五万多年的日期;把秒当毫秒喂给 JS 的 new Date(),会得到 1970-01-20。
const sec = Math.floor(Date.now() / 1000); // 当前秒级时间戳
const ms = Date.now(); // 毫秒
new Date(sec * 1000).toISOString(); // '2023-11-14T22:13:20.000Z'
Date.parse('2023-11-14T22:13:20Z'); // 1700000000000(毫秒)
new Date() 只接受毫秒。另外 Date.parse 对不带时区的 '2023-11-14 22:13:20' 在不同引擎里可能按本地时区解释,写死 Z 或完整偏移量最安全。
import datetime
now = int(datetime.datetime.now(datetime.timezone.utc).timestamp()) # 秒
datetime.datetime.fromtimestamp(1700000000, datetime.timezone.utc)
# datetime.datetime(2023, 11, 14, 22, 13, 20, tzinfo=datetime.timezone.utc)
Python 的 fromtimestamp() 不传第二个参数时会按本地时区解释,同一份代码在不同机器上结果不同,务必显式传 tz。
now := time.Now().Unix() // 秒
ms := time.Now().UnixMilli() // 毫秒(Go 1.17+)
t := time.Unix(1700000000, 0).UTC()
fmt.Println(t.Format(time.RFC3339)) // 2023-11-14T22:13:20Z
-- MySQL
SELECT UNIX_TIMESTAMP(); -- 当前秒
SELECT FROM_UNIXTIME(1700000000); -- 按会话时区显示
-- PostgreSQL
SELECT extract(epoch FROM now())::bigint;
SELECT to_timestamp(1700000000) AT TIME ZONE 'UTC';
-- SQLite
SELECT strftime('%s', 'now');
SELECT datetime(1700000000, 'unixepoch'); -- 结果恒为 UTC
32 位有符号整数能表示的最大秒数是 2147483647,对应 2038-01-19 03:14:07 UTC,再加一秒就溢出成负数(回到 1901 年)。受影响的是把时间戳存成 32 位 INT 的老系统和老嵌入式设备。
MySQL 的 TIMESTAMP 类型上限就是 2038-01-19,要存更远的日期必须改用 DATETIME。PostgreSQL 的 timestamptz、SQLite、以及所有用 64 位整数的语言都没有这个问题。
优先用数据库原生的带时区类型(PostgreSQL 的 timestamptz、MySQL 的 DATETIME)。理由是:直接可读、能直接用日期函数做按月聚合、不会被误读单位。只有在跨系统交换时(API 响应、日志行)才用整数时间戳,并且在字段名里写清单位,例如 created_at_ms。
如果一定要存整数,用 BIGINT 而不是 INT。
时间戳本身没有时区,时区只在"渲染成人类可读形式"和"按自然日聚合"时才出现。规则:
Asia/Shanghai 切,日活数字会不一样。2023-11-14T22:13:20+08:00),比裸数字更不容易被误读。"加一天"不等于"加 86400 秒"。夏令时切换的那天只有 23 小时或 25 小时,中国没有夏令时,但你的用户可能在美国或欧洲。正确做法是用库提供的日期运算:Python 的 timedelta、Go 的 AddDate(0, 0, 1)、JS 里先转 Date 再用 setDate(),或者交给数据库的 DATE_ADD / INTERVAL '1 day'。
各语言和系统的默认单位不一样,混一次就是十亿倍的误差:
| 单位 | 1 秒 = | 常见来源 |
|---|---|---|
| 秒 | 1 | Go Unix()、C time()、MySQL UNIX_TIMESTAMP() |
| 毫秒 | 10³ | JS Date.now()、Java currentTimeMillis() |
| 微秒 | 10⁶ | Python datetime 的内部精度、MySQL DATETIME(6) |
| 纳秒 | 10⁹ | Go time.Time 的内部精度、Linux CLOCK_MONOTONIC |
跨服务传值时最有效的做法,是把单位写进字段名(expires_at_ms、created_at_us),比在接口文档里补一句"单位是毫秒"可靠得多——文档会过时,字段名不会。
Python 的 time.time() 和 datetime.timestamp() 返回浮点数。时间戳到 1.7e9 量级时,float64 能表示的最小间隔大约是 0.2 微秒,再往下的位数不可信。所以不要用浮点时间戳做相等判断,也不要依赖它的小数末位;需要精确比较就用整数秒或 datetime 对象。
这是性能问题不是正确性问题,但在大表上差别巨大:
-- 不推荐:对索引列做运算,索引失效
SELECT * FROM orders WHERE FROM_UNIXTIME(created_at) >= '2024-01-01';
-- 推荐:把运算放到常量一侧,索引可用
SELECT * FROM orders WHERE created_at >= UNIX_TIMESTAMP('2024-01-01');
规则是让索引列保持裸露,把转换放在常量那一侧。PostgreSQL 里同理:写 created_at >= to_timestamp(1700000000),而不是对列做 extract(epoch FROM created_at) >= 1700000000。