每个开发者都发布过的 7 个 Unix 时间戳 bug
这些是引发生产事故的时间戳错误:悄无声息的单位不匹配、时区假设、被遗忘的 ×1000、字符串存储、有歧义的解析,以及夏令时边界错误。每个 bug 都附带症状、根因和实用修复。
Bug 1:JavaScript 中漏了 × 1000
new Date(1700000000) 产生的是接近 1970 年 1 月的日期,而非 2023 年 11 月。JavaScript 的 Date 构造函数期望毫秒,但大多数服务器 API 和数据库返回 Unix 秒。这个 bug 常先出现在管理后台,因为后端负载是有效的 JSON,而界面只是渲染出了错误的年份。
- Wrong: new Date(response.created_at) — if created_at is Unix seconds
- Right: new Date(response.created_at * 1000)
- Detection: if your date shows a year near 1970, the timestamp is in seconds not milliseconds
- Safe wrapper: const toDate = ts => new Date(ts < 1e11 ? ts * 1000 : ts)
Bug 2:系统边界处单位混用
JavaScript 返回毫秒。Python、Go 和大多数服务器端 API 返回秒。当 JavaScript 未经转换就把时间戳发送给 Python 后端时,后端会把毫秒值解释为秒,把事件放到 55792 年。反方向同样糟糕:界面收到秒,直接传给 new Date(),显示 1970 年 1 月。
- JavaScript sends: Date.now() → 1700000000000 (ms)
- Python receives: datetime.fromtimestamp(1700000000000) → year 55792
- Fix: divide by 1000 before sending to a non-JavaScript system
- Better fix: use ISO 8601 strings at API boundaries — they are unambiguous
Bug 3:依赖服务器的本地时区
使用 Python 的 datetime.fromtimestamp() 而不指定 UTC 的代码,会因服务器配置的时区不同而产生不同结果。它在开发中能用,部署到不同区域时在生产中失效。当代码不带显式 timeZone 选项格式化日期、随后在测试中快照该结果时,Node 中也会出现同类 bug。
- Wrong: datetime.fromtimestamp(ts) — uses server's local timezone
- Right: datetime.fromtimestamp(ts, tz=datetime.timezone.utc)
- Wrong in JS: date.toLocaleDateString() — uses the Node process timezone
- Right in JS: date.toISOString() or Intl.DateTimeFormat with an explicit timeZone option
Bug 4:把时间戳存为字符串
把时间戳作为格式化字符串存入 VARCHAR 列看似无害,直到你需要按时间排序、查询范围或做算术。日期的字符串比较只对严格的、零填充的 ISO 8601 字段有效。像 Nov 15, 2023 或 1/2/24 这样易读的字符串是显示格式,而非存储格式。
- Wrong: INSERT INTO events (created) VALUES ('Nov 15, 2023') — not sortable
- Wrong: INSERT INTO events (created) VALUES ('2023-11-15') — loses time component
- Right: use a native DATETIME/TIMESTAMPTZ column or BIGINT for Unix milliseconds
- If a string is unavoidable: use ISO 8601 with full precision: '2023-11-15T06:13:20Z'
Bug 5:解析有歧义的日期字符串
new Date('2024-01-01') 和 new Date('2024/01/01') 看起来等价,行为却不同。带连字符的 ISO 8601 格式被解析为 UTC 午夜;带斜杠的格式由实现决定,多数浏览器解析为本地午夜。日期选择器、API 负载和数据库行可能显示同一日历日期却表示不同的瞬间。
- new Date('2024-01-01') → January 1, 2024 00:00:00 UTC (correct, explicit)
- new Date('2024/01/01') → January 1, 2024 00:00:00 local time (varies by runtime)
- new Date('January 1, 2024') → local time, may be rejected by strict parsers
- Safe rule: always use ISO 8601 with explicit timezone: new Date('2024-01-01T00:00:00Z')
Bug 6:加 24 小时来表示明天
加 86,400,000 毫秒看似是获取明天的干净方法,但本地日历日并不总是 24 小时。在夏令时切换期间,本地一天可能是 23 或 25 小时。如果产品逻辑指的是下一个本地日历日,请在目标时区做日历算术,而非在 UTC 毫秒中做时长算术。
- 对日历日是错的:next = new Date(date.getTime() + 86400000)
- 在本地 Date 代码中更好:复制 Date,然后调用 setDate(d.getDate() + 1)
- 对服务器代码:使用支持时区的库或数据库的日期算术
- 对重复排程:存储 local_date、local_time 和 timezone_id,再计算每次发生
- 为目标时区的夏令时开始日和结束日添加测试
Bug 7:包含末尾的当日过滤
常见的分析查询使用 created_at >= start 和 created_at <= endOfDay。看似合理,但当数据库存储毫秒、微秒或纳秒时会产生精度 bug。更安全的模式是半开区间:大于等于起点,且严格小于下一周期的起点。
- Risky: WHERE created_at <= '2026-05-19 23:59:59'
- Safer: WHERE created_at >= '2026-05-19' AND created_at < '2026-05-20'
- Half-open ranges work for seconds, milliseconds, microseconds, and nanoseconds
- The same pattern works for months: created_at >= monthStart AND created_at < nextMonthStart
生产环境预防清单
大多数时间戳事故都可以通过命名、测试和显式边界来预防。目标不是让每一行日期代码都很巧妙;而是让单位、时区和范围语义显而易见到乏味。
- 用带单位的名称命名数值字段:createdAtMs、expiresAtSeconds、imported_at_ms
- 存储用 UTC,显示用显式的 IANA 时区
- 在人工检查的 API 边界处优先使用带 Z 或偏移的 ISO 8601 字符串
- 日期过滤使用半开区间
- 在 1 月 1 日、闰日、夏令时切换和 2038 年边界附近做测试
Unix 时间戳 bug 常见问题
- 最常见的 Unix 时间戳 bug 是什么?
- 在期望毫秒的地方传入秒,或反过来。在 JavaScript 中,new Date(unixSeconds) 会落在 1970 年,因为构造函数期望毫秒;先把 10 位的值乘以 1000。
- 如何预防秒对毫秒的 bug?
- 用带单位的名称命名字段(createdAtMs、expiresAtSeconds),在系统边界处转换,并在 API 契约中优先使用 ISO 8601 字符串,使值能自我描述。
- 为什么加 86,400,000 毫秒会破坏「明天」?
- 本地日历日在夏令时切换期间为 23 或 25 小时,所以加固定的 24 小时可能落到错误的日期。请在目标时区做日历算术。