時間類型和時間戳

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

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

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

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

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

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

比較特殊的是 UTC 時區,能夠表示爲 2006-01-02T15:04:05+00:00,但一般簡化爲 2006-01-02T15:04:05Zsql

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

日期和時間的解析

不一樣的數據來源極可能使用不一樣的時間表示方法。根據是否可讀分紅兩類:數據庫

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

數字類型就不詳細說明。函數

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

  • 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 驅動使用的是第三種思路。

相關文章
相關標籤/搜索