雜記 › 技術筆記 › Unix 時間戳對不上日期?10 位秒級、13 位毫秒級與時區差一次講清

Unix 時間戳對不上日期?10 位秒級、13 位毫秒級與時區差一次講清

凌晨一點二十,群組訊息把我叫醒:營運貼了張後台截圖,三月那批訂單的「建立時間」整欄變成 1970-01-21 00:13:20。值班的前端爬起來看程式碼,五分鐘就定位到了——介面回傳的 create_time 是 10 位秒數,而他寫的 new Date(create_time) 把這個數字當成毫秒。同一個 1700000000,按秒讀落在 2023 年 11 月,按毫秒讀就掉回 1970 年 1 月。那次之後,我們組把時間戳相關的約定寫成一張卡片釘在 wiki 首頁,新人接手排查基本上不用再問人。

一、位數就是精度:先學會一眼判斷單位

時間戳本身只是一個整數,含義完全取決於它被按什麼單位產生出來。我的第一判斷依據永遠是數位數,而不是猜後端用了哪門語言:

位數單位典型來源與秒級的關係
10 位秒PHP time()、Go Unix()、Linux date +%s基準
13 位毫秒JavaScript Date.now()、Java currentTimeMillis()秒 × 1000
16 位微秒Go UnixMicro()、Python time_ns()//1000秒 × 1000000
19 位奈秒Python time_ns()、Go UnixNano()秒 × 10⁹

再補一條值域檢查:2020-01-01 到 2030-01-01 之間,秒級落在 1577836800 到 1893456000,毫秒級是 1577836800000 到 1893456000000。把 13 位當 10 位解析,日期會飛到公元五萬年附近;反過來把 10 位當 13 位,就掉回 1970 年。我遇到的「時間顯示異常」基本上都落在這兩種。

二、差的 8 小時不在時間戳裡,在讀它的那一層

時間戳是從 1970-01-01 00:00:00 UTC 起算的絕對量,不帶任何時區資訊。同一個數字在倫敦還是上海都是同一個值,分歧只發生在「把整數翻譯成人能讀的日期」這一步。以 1700000000 為例,它的 UTC 時刻是 2023-11-14 22:13:20,換成台北時間是 2023-11-15 06:13:20,正好差 8 小時。

實際專案裡造成 8 小時錯覺的四個常見位置,值得挨個排除:

  • 容器預設時區是 UTC、宿主機是 Asia/Shanghai,日誌牆鐘時間和業務看到的差 8 小時。TZ=Asia/Shanghai date -d @1700000000 和不帶 TZ 各跑一次,就能確認錯在哪一層。
  • MySQL 的 FROM_UNIXTIME() 按工作階段時區輸出。先 SELECT @@session.time_zone,再和 UNIX_TIMESTAMP() 雙向核對,欄位本身往往沒錯。
  • PHP 沒配 date_default_timezone 時按 UTC 走,date() 輸出整體平移。
  • 前端 new Date(ts).toISOString() 永遠回傳 UTC 且末尾帶 Z,toLocaleString() 按系統時區渲染。同一份資料在兩個函式裡「不一致」,是除錯裡最常見的自我懷疑。

三、我現在的排查順序,以及在哪一步用上工具

  1. 先數位數定單位,寫下「這批數字按秒還是按毫秒」的結論,避免後面兩個人各按一種理解算。
  2. 再定基準:我們最後把規矩壓成一句——資料庫裡只存 10 位 UTC 秒,介面欄位名後綴寫清單位(create_time_ms),呈現層負責轉本地時區。
  3. 雙向驗一次:拿一個已知時刻(比如「昨晚 23:07」)轉成時間戳,再轉回來,看是否落回同一個值。這一步能同時暴露單位和時區兩處錯。
  4. 批次核對時,我把可疑數字貼進時間戳轉換,它會自己分辨秒級還是毫秒,同時給出台北時間、UTC、ISO 8601 和「幾天前」這類相對描述,省掉我心算時區偏移;日期反推時間戳也在同一頁做,頁面上還備了幾種語言取時間戳的寫法,換語言時直接抄那一段。
  5. 要算兩個時間點的間隔,或推「往後 90 天是哪天」,用時間加減計算,結果連星期幾和時間戳一起給出,比手動除以 86400 可靠。

四、容易忽略的邊界情況

這幾條踩過之後才會主動檢查,寫在這裡當清單用:

邊界具體表現處理方式
32 位溢出有號 32 位存秒級,到 2038-01-19 03:14:07 UTC 翻負;MySQL TIMESTAMP 欄位上限也是這個時刻欄位改 BIGINT,遷移前先用 SELECT MAX() 看餘量
負數時間戳1970 年之前是負值,部分資料庫函式和圖表庫顯示成空或直接報錯歷史檔案用日期字串存,時間戳只留給近幾十年業務
閏秒時間戳不表達閏秒,對齊 UTC 秒的排程器在閏秒當天可能看到重複的一秒關鍵計時走單調時鐘(CLOCK_MONOTONIC),別拿牆上時間做倒數
日光節約時間美國 2024-03-10 02:00 直接跳到 03:00,這一小時的本地時間不存在;11-03 回撥又重複一小時存 IANA 時區名(如 America/New_York),別只存 UTC+4 這種固定偏移
同秒並發拿時間戳當檔名或快取 Key,同一秒內的請求會互相覆蓋毫秒級再拼隨機後綴,或用 UUID

跨國排會議是另一類需求:真正要的往往不是時間戳,而是「對方現在算不算工作時間」。這種我會先看世界時間裡兩城的目前時刻和時差,定下來再把時刻轉成時間戳寫進行程介面。

五、常見疑問

Q:轉換出來差 8 小時,是時間戳錯了嗎?

幾乎不是。先確認解析用的時區,再確認位數單位;時間戳數值本身沒變,變的只是渲染它的這一層。

Q:JavaScript 裡到底該乘 1000 還是除 1000?

看輸入位數。JS 的 Date 只認毫秒,拿到 10 位要乘 1000;反過來把 Date.now() 發給一個按秒解析的介面,就要除 1000 並取整。最穩的辦法是在欄位名裡寫清單位。

Q:怎麼快速判斷一批混亂的日誌時間是秒還是毫秒?

取最大值數位數,再用值域對照:2020 到 2030 年之間,10 位是秒、13 位是毫秒。同一份日誌裡兩種混用並不罕見,先分桶再統一轉換。

Q:2038 問題離我們遠嗎?

只要還有 32 位整數欄位在跑老系統就不算遠。成本主要在改欄位型別和重建索引,越晚動越麻煩,最好在下一輪資料表結構調整裡順手做掉。

回頭看那次故障,真正的修復不是那一行 new Date(),而是「誰產生數字誰標註單位、誰呈現時間誰負責時區」這條約定終於落進了介面文件。以後再看到 1970 或者公元五萬年,先數位數,再看時區,兩件事按順序查完,絕大多數時間錯亂都到不了第二步。

📎 相關文章

PNG 壓了三遍只減 426 位元組:無損壓縮的真實邊界 2026-10-22 WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住 2026-10-22 截圖縮到四分之一,小字糊成一團的三種原因 2026-10-22 同樣裁 300×300,頭像為什麼總偏右下角半格 2026-10-22 按每月 1.7 倍預估 18 個月,腳本和表格為什麼給出兩個數 2026-10-22