時間類型和時間戳

Unix 時間戳以及日期表示方法

Unix 時間戳表示的是從世界標準時間(UTC,Coordinated Universal Time)的 1970 年 1 月 1 日 0 時 0 分 0 秒開始的偏移量。

全球共有 24 個時區,分爲東西各 12 時區。所有地區在使用同一個時間戳的基礎上,根據當地時區調整時間的表示。

現在比較常見的日期和時間的表示標準是 ISO8601,或者在其基礎上更加標準化的 RFC3339。

舉個例子,北京時間 2021 年 1 月 28 日 0 時 0 分 0 秒用 RFC3339 表示爲:2021-01-28T00:00:00+08:00

+08:00 表示東 8 區,2021-01-28T00:00:00 表示這個時區的人所看到的時間。加號如果改爲減號,則表示西時區。

比較特殊的是 UTC 時區,可以表示爲 2006-01-02T15:04:05+00:00,但通常簡化爲 2006-01-02T15:04:05Z

在使用的時候,應當根據時區調整時間的展示。例如 1611792000 可以表示爲 2021-01-28T00:00:00Z 或者 2021-01-28T08:00:00+08:00

日期和時間的解析

不同的數據來源很可能使用不同的時間表示方法。根據是否可讀分成兩類:

  • 用數字表示的時間戳
  • 用字符串表示的年月日時分秒

數字類型就不詳細說明。

字符串又根據是否有時區分爲兩類:

  • 2021-01-28 00:00:00 沒有包含時區信息
  • 2021-01-28T08:00:00+08:00 包含了時區信息

在解析沒有包含時區信息的字符串時,通常要由程序員指定時區,否則默認爲 UTC 時區。如果附帶時區,那就可以不用另外指定。

例如 Golang 的時間庫,就有兩個方法:

  • Parse(layout, value string)
  • ParseInLocation(layout, value string, loc *Location)

在解析的時候,會先根據年月日時分秒計算出一個整數。接着看 value 是否包含時區信息。

如果 value 包含時區,那麼就會給解析後的整數加一個偏移量,這個偏移量由時區與 UTC 時區之間的位置關係決定。

如果 value 不包含時區信息,Parse 會將其設置爲 UTC 時區,ParseInLocation 會根據傳入的時區調整解析出來的整數,並將時區設置爲傳入的時區。

日期和時間的存儲

和解析時一樣,保存日期和時間的方式有多種。

例如 Golang 的 Time :

type Time struct {
	wall uint64
	ext  int64
	loc *Location  // 位置。用於調整時間的表示。
}

Golang 存儲的不是 Unix 時間戳,但是會根據情況將其轉換爲時間戳。對於 loc 的修改不會對 Unix 時間戳產生影響,只會影響時間的展示形式。

MongoDB 使用的 bson.Date 使用 int64 存儲從 1970 年 1 月 1 日以來的毫秒數。

MySQL 使用 DATETIME 類型存儲不包含時區的年月日時分秒,查詢時以 YYYY-MM-DD HH:MM:SS 的形式展示。也可以用四個字節的 TIMESTAMP 類型存儲 Unix 時間戳。

時間戳的問題

以前在保存時間戳的時候,通常都使用四個字節,也就是 32 位的有符號整數存儲。

把二進制的 01111111 11111111 11111111 11111111 轉化爲十進制後得到 2147483647,再轉化爲北京時間得到 2038-01-19 11:14:07

這就表示 32 位整數最多隻能存儲到 2038 年的時間,因此被稱爲 “2038 年問題”。

比較新的一些項目會通過各種方式解決這個問題,通常是使用 64 位整數來存儲時間戳。但使用方式各有不同。

例如 Golang 使用了兩個 64 位整數來存儲。其中無法符號整數 wall,第一位表示是否有單調時間。

  • 如果爲 1,則表示有單調時間。
    wall 的 2~34 位存儲自 1885 年 1 月 1 日 0 時 0 分 0 秒以來的秒數,35~64 位存儲納秒數。
    有符號的 64 位整數 ext 存儲從進程啓動以來的納秒數(單調時間)。
  • 如果爲 0,則表示沒有單調時間。
    wall 的 2~64 不存儲時間。
    有符號的 64 位整數 ext 存儲從 0001 年 1 月 1 日 0 時 0 分 0 秒以來的秒數。

MongoDB 則是使用 int64 存儲從 1970 年 1 月 1 日以來的 UTC 毫秒數。

MySQL 沒有解決 TIMESTAMP 類型的問題,它始終是四個字節。因此如果要解決這個問題,最好使用 DATETIME。但是 DATETIME 也有問題,它沒法存儲時區。不過大多數應用都無需考慮時區問題,無需擔心。

時間的展示

數據庫都默認使用 UTC。如果不加以處理,存儲到數據庫的時間就會展示爲與本地實際展示的時間不一致的形式。

例如 MongoDB 存儲的是從 1970 年 1 月 1 日以來的 UTC 毫秒數,像 Navicat 這種工具,會用 UTC 的形式展示時間。這樣其他時區的人看起來就會不習慣。

而 MySQL 就更難處理了,DATETIME 不帶時區。

解決這個問題有三種思路:

  1. 修改數據庫配置,改成本地時區
    MongoDB 這樣設置不會有影響,仍然存儲的是毫秒數。只是在展示的時候會使用配置的時區格式化字符串。
    MySQL 這樣設置後,會對 NOW() 這種函數的結果產生影響。不會對 SQL 語句中直接寫 0000-00-00 00:00:00 的情況產生影響。
  2. 查詢的時候將其重新轉換爲本地時區
    有三種:
    • 爲數據庫連接會話設置時區。同上,只是在會話級別產生影響。
      MySQL 會有影響,如果不同地方的會話設置不同時區,又使用了 NOW(),得到的結果不一致。
    • 在代碼上做一層包裝,用於調整時區。
      MongoDB 沒啥影響,畢竟存儲的是毫秒數。只是展示的時候做個調整。
      MySQL 可以始終存儲爲 UTC 形式,然後要展示的時候,用代碼把時間格式化爲本地時區的形式。
    • 爲數據庫表創建 view,在 view 裏面處理時區
      例如 MongoDB:
      db.createView("view_name","collection_name",[
          {
              $addFields: {
                  date: {
                      $dateToString: {
                          date: "$date",
                          format: "%Y-%m-%dT%H:%M:%S+08:00",
                          timezone: "+08:00"
                      }
                  }
              }
          }
      ]);
      
      addFields 會覆蓋同名的字段。上面的語句會將原先的 date 字段的值以新的格式展示。
  3. 存儲的時候創建一個年月日時分秒和本地展示時間一致的 UTC 時間
    這會改變數據庫存儲的時間戳,使得時間戳與實際時間戳不一致。對 MongoDB 會產生影響。
    不過 MySQL 的 DATETIME 不是用時間戳,所以只要格式化到 SQL 語句的時間形式是本地時區的就行了。只是如果出現跨時區的用戶、數據、開發人員,處理起來就比較麻煩。

具體實例

Golang MongoDB 庫

MongoDB 的官方庫在存儲的時候,會使用 UTC 的時間戳。但在查詢的時候,會判斷是否設置了使用本地時間展示。如果沒有設置按本地時間展示,則會將 Time 設置爲 UTC 時區。

if !tc.UseLocalTimeZone {
    timeVal = timeVal.UTC()
}

如何事先配置好?

builder := bsoncodec.NewRegistryBuilder()

// 註冊默認的編碼和解碼器
bsoncodec.DefaultValueEncoders{}.RegisterDefaultEncoders(builder)
bsoncodec.DefaultValueDecoders{}.RegisterDefaultDecoders(builder)

// 註冊時間解碼器
tTime := reflect.TypeOf(time.Time{})
tCodec := bsoncodec.NewTimeCodec(bsonoptions.TimeCodec().SetUseLocalTimeZone(true))
registry := builder.RegisterTypeDecoder(tTime, tCodec).Build()

client, err := mongo.NewClient(options.Client().ApplyURI(uri), options.Client().SetRegistry(registry))

MongoDB 使用的 bson.Date 使用 int64 存儲 1970 年 1 月 1 日以來的毫秒數。從 MongoDB 查出來的也是這個數據。

如果 decode 的時候指定了存儲結果的結構體的時間字段的類型,如 time.Time。則會將 int64 轉化爲 time.Time。如果不指定,則返回 int64。

可見 MongoDB 官方庫使用的是第二種思路。

Golang MySQL 驅動的實例

https://github.com/go-sql-driver/mysql#loc

需要在連接的時候設置。dsn 裏面帶上 loc 參數。

在解析查詢結果中的 DateTime 類型的時候,會將字節轉換爲字符串形式。這個字符串形式最長的情況是 0000-00-00 00:00:00.0000000。驅動會根據實際長度解析。

MySQL 驅動的做法是,如果 dsn 有帶 loc 參數,那麼在解析年月日時分秒和毫秒後,以這些數據和時區創建 time.Time。即 time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc)

而在 insert 操作時,會將 time.Time 設置爲指定的時區。v.In(mc.cfg.Loc).AppendFormat(b, timeFormat),這裏的 v 就是我們 Insert 的類型爲 time.Time 的值。

可見 MySQL 驅動使用的是第三種思路。

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