Java 時間戳

轉載自:https://lotabout.me/2019/Timestamp-revealed/

敘述

海上生明月,天涯共此時。在計算機的世界裏,怎麼才能“共此時”呢?

分析

時間如何存儲

存儲的首先需要有唯一性。

你朋友說要在 22:00 給你打電話,結果 21:00 電話就來了,心裏咒罵了一陣,纔想起來朋友在日本,日本 22:00 時北京正好是 21:00。雖然平時可能不太注意得到,但是如果想讓時間唯一,是需要加上時區的。用 時間+時區 來存儲時間似乎是一個好選擇。

存儲的數據最好能方便比較。

你可能很難一眼看出 10:00 CST 和 11:00 IOT 哪個時間更早。但如果統一換算成協調世界時(UTC)或是其它什麼時區,就很容易比較了。也就是說存儲的基準最好一致。

再着嘛,最好節省空間。

直接的想法是記錄年月日時,但是一個標準的時間字符串 (如 2019-10-15 20:48:19.128) 就佔用了 23 個字節,比較浪費。所以計算機中也使用一種稱爲 epoch time 的存儲方式,存儲的是當前時間(轉換爲 UTC) 距離 Unix epoch (1970-01-01 00:00:00) 的毫秒數,例如上例可表示爲 1571172499000。這樣要表示日常生活中的時間,通常只需要 4 個字節(32位) 或是 8 個字節(64位) 即可。當然,存儲節省了,能表示的時間範圍也小了,例如 32 位的 epoch time 最多隻能表示到 2038-01-19

下面是列舉了一些系統的時間表示方式:

  • MySQL 中的 TIMESTAMP 類型以 YYYY-MM-DD hh:mm:ss 表示當前時間對應的UTC時間[1], 佔 19 個字節。
    • 5.6.4 之後的版本可通過 TIMESTAMP(n) 指定保留 n 位毫秒數[2]
  • Java 中的 Date 類型內部以 long 型(64位)存儲當前時間(UTC)距 epoch time 的毫秒數。
  • 大數據格式 Parquet 以 int96 的類型存儲當前時間(UTC)距 epoch time 的納秒數。

當然後面我們會看到,爲了更準確處理各種情形,也會直接用 年月日時分秒+時區 的方式存儲。

時間如何解析

假設我們以 epoch time 作爲存儲格式,現在拿到 2019-10-15 20:48:19 這樣一個時間,要如何轉換成相應的 epoch time 呢?注意,這個時間字符串是不帶時區的!

原始時區信息缺失是時間處理不一致的重要根源之一,不同的系統/工具應對的方式不同。

例如 java.sql.Timestamp.valueOf 會認爲解析的字符串就是 UTC 時間。Java 創建 Timestamp類型的初衷是對標 MySQL 的 TIMESTAMP 類型,兩者在解析時都認爲輸入是 UTC 時間也就不足爲奇了。

String timeStr = "2019-10-15 10:10:10.001";
Timestamp timestamp = java.sql.Timestamp.valueOf(timeStr);
System.out.println(timestamp);
System.out.println(timestamp.getTime());

// 2019-10-15 10:10:10.001 # 北京時間下運行
// 1571105410001 # (2019-10-15 10:10:10.001 in GMT)

// 2019-10-15 10:10:10.001 # 東京時間下運行
// 1571101810001 # (2019-10-15 10:10:10.001 in GMT)

上例中將系統調成北京時間(CST)還是東京時間(JST),輸出的內容不變。

而 java.util.Date 以及對應的 java.text.DateFormat 都允許指定時區,默認選取系統的時區進行解析。下面以 SimpleDateFormat 爲例 [3]

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String timeStr = "2019-10-15 10:10:10.001";
java.util.Date date = format.parse(timeStr);
System.out.println(date);
System.out.println(date.getTime());

// Tue Oct 15 10:10:10 CST 2019 # 北京時間下運行
// 1571105410001 # (2019-10-15 2:10:10.001 in GMT)

// Tue Oct 15 10:10:10 JST 2019 # 東京時間下運行
// 1571101810001 # (2019-10-15 1:10:10.001 in GMT)

由於使用了系統當前所在的時區,上面的代碼在北京時間(CST)和東京時間(JST)下執行,得到的毫秒數是不同的。

時間如何展示

展示的終極問題:要以當前的時區展示?還是以原時區展示?這是與業務相關的。

  • 如果要判斷一筆交易在幾點進行,則可能按發生地時區展示/計算更合理(例如認爲凌晨發生的交易是欺詐的可能性高,則境外的交易就需要按境外的時區算幾點)
  • 如果要展示一篇博客何時發佈,以讀者所在的時區展示可能更理想

正因爲這個決定跟業務相關,系統的實現者只能爲兩種需求都提供對應的機制。顯然以 epoch time 存儲是不行的,因爲它不帶原始時區。MySQL 的存儲也同樣不行,雖然以字符串存儲,但依舊不包含原始時區[4]。這裏我們簡單記錄 Java 提供的處理機制。

Java 8 的 java.time 包中提供了許多時間處理的類,讓我們按需自取。如 LocalDateLocalTime 和 LocalDateTime 內部以年月日、時分秒的形式保存了日期和時間,不包含任何時區的信息。而 ZonedDateTime 則是 DateTime 加上時區,用於處理與時區相關的所有操作,包括時區間的時間轉換。

Java 8 中的 ZonedDateTime 人如其名,內部提供了額外的字段保留時區:

String timeStr = "2019-10-15T10:10:10.001+02:00[Europe/Paris]";
ZonedDateTime datetime = ZonedDateTime.parse(timeStr);
System.out.println(datetime);
System.out.println(datetime.toInstant().getEpochSecond());

// 2019-10-15T10:10:10.001+02:00[Europe/Paris] # 北京時間下運行
// 1571127010 # 2019-10-15 08:10:10 GMT

可以看到,儘管在北京時間下運行,輸出裏仍然保留了原始輸入的時區:巴黎時間。

而如果希望將巴黎時間展示爲當前的時區,則可以如下操作:

String timeStr = "2019-10-15T10:10:10.001+02:00[Europe/Paris]";
ZonedDateTime parisTime = ZonedDateTime.parse(timeStr);
ZonedDateTime shanghaiTime = parisTime.withZoneSameInstant(ZoneId.systemDefault());
System.out.println(shanghaiTime);
System.out.println(parisTime.toInstant().getEpochSecond());

// 2019-10-15T16:10:10.001+08:00[Asia/Shanghai]
// 1571127010

可以看到,當前時區是上海,巴黎 10:10 時,上海是 16:10

小結

時間處理,尤其是在不同系統中傳遞時間信息,一般會涉及三個問題:

  1. 數據解析,時間數據如何解析成內部格式?如何補全時區信息?
  2. 數據存儲,存儲帶不帶原始時間的時區?
  3. 數據展示,要展示原始時區?當前時區?還是其它時區?

考慮一下,Java 中的 LocalDateTime 是不帶時區的,但是如果將對應數據存入 MySQL,則需要轉換成 epoch time,那麼如何補全時區信息呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章