MySQL Connect/J 8.0時區陷阱

image

最近公司正在升級Spring Boot版本(從1.5升級到2.1),其間踩到一個很是隱晦的MySQL時區陷阱,具體來講,就是數據庫讀出的歷史數據的時間和實際時間差了14個小時,而新寫入的數據又都正常。若是你以前也是使用默認的MySQL時區配置,那麼大機率會碰到這個問題,深究其背後的緣由又涉及到不少技術細節,故整理出來分享給你們。html

首先來看一下緣由。升級到Boot 2.1以後,MySQL Connect/J版本也隨之升級到8.0,會優先使用鏈接參數(serverTimezone)中指定的時區,若是沒有指定,則再使用數據庫配置的時區,參考下面的官宣(對應的源代碼是com.mysql.cj.protocol.a.NativeProtocol#configureTimezone())。因爲咱們以前數據庫鏈接參數沒有指定時區,而且數據庫配置的是默認的CST時區(美國中部時區,即-6:00),因此讀取出來的時間出現誤差。java

Connector/J 8.0 always performs time offset adjustments on date-time values, and the adjustments require one of the following to be true:mysql

  • The MySQL server is configured with a canonical time zone that is recognizable by Java (for example, Europe/Paris, Etc/GMT-5, UTC, etc.)
  • The server's time zone is overridden by setting the Connector/J connection property serverTimezone (for example, serverTimezone=Europe/Paris).

找到緣由以後,解決辦法就比較直白了,sql

方法一:數據庫的鏈接參數添加serverTimezone=Asia/Shanghai或者serverTimezone=GMT%2B8。Boot 1.5下不須要添加此參數,但添加了也無妨。數據庫

方法二:修改MySQL數據庫的time_zone配置,改成+8:00(默認是SYSTEM)。採用此方法,則不須要修改數據庫鏈接參數。ui

方法二顯然更優,一次修改,終生受益。但要注意,對於升級到Boot 2.1以後新生成的那批數據,若是包含時間類型的字段而且該字段值是應用指定的而不是數據庫生成的(例如DEFAULT CURRENT_TIMESTAMP),那麼須要手動修復(加上誤差的小時數)。this

兩個解決辦法都很簡單,有同窗立刻會問,爲何Boot 1.5下沒有這個問題?爲何Boot 2.0下讀取歷史數據存在14個小時的誤差,而新生成的數據又是好的?要回答這兩個問題,看官宣就不夠了,須要讀一下MySQL Connect/J的源代碼。url

謎題一,爲何Boot 1.5下沒有這個問題?答案隱藏在com.mysql.jdbc.ResultSetImplcom.mysql.jdbc.ConnectionImpl兩個類的源代碼中。spa

// 源代碼:com.mysql.jdbc.ResultSetImpl
private TimeZone getDefaultTimeZone() {
        // useLegacyDatetimeCode默認爲true,所以使用connection的默認時區
        return this.useLegacyDatetimeCode ? this.connection.getDefaultTimeZone() : this.serverTimeZoneTz;
    }
// 源代碼:com.mysql.jdbc.ConnectionImpl
public ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
        // connection的默認時區使用的是JVM的默認時區,通常爲操做系統的時區
        // We store this per-connection, due to static synchronization issues in Java's built-in TimeZone class...
        this.defaultTimeZone = TimeUtil.getDefaultTimeZone(getCacheDefaultTimezone());
}

Boot 1.5下,MySQL Connect/J默認使用操做系統的時區(Asia/Shanghai,即+8:00),而忽略鏈接參數或者數據庫指定的時區,所以無論是讀數據仍是寫數據都是使用統一的時區,所以不存在時間誤差。操作系統

謎題二,爲何Boot 2.0下讀取歷史數據存在14個小時的誤差,而新生成的數據又是好的?升級到Boot 2.0以後,MySQL Connect/J改成使用數據庫配置的CST時區,而歷史數據是在Boot 1.5下的Asia/Shanghai時區生成的,所以讀出來存在14(-6:00和+8:00之間)個小時的誤差。對於新生成的數據,因爲同處在CST時區下,所以沒有誤差。

解完這兩個謎題,你可能還有些疑惑。那麼接下來,結合數據流轉的順序,咱們再來分析一下數據流轉過程當中時區的變化。

image

設定Application-1爲數據生產方,Application-2爲數據消費方,TZ-IN1爲Application-1所處的時區,TZ-IN2爲Application-1寫入數據庫的時區,TZ-OUT1爲Application-2讀出數據庫的時區,TZ-OUT2爲Application-2所處的時區。如前所述,TZ-IN2和TZ-OUT1由鏈接參數或者數據庫配置決定。

整個數據流轉過程,會涉及3次顯式的時區轉換和1次隱式的時區轉換。

  • 轉換①(顯式):TZ-IN1轉TZ-IN2,這個轉換由MySQL Connect/J完成(參考com.mysql.cj.ClientPreparedQueryBindings#setTimestamp(),限於篇幅,此處再也不展開分析)。
  • 轉換②(隱式):TZ-IN2轉無時區,MySQL內部存儲時間類型的字段時或者忽略時區(DateTime類型)或者使用UTC(Timestamp類型),參考MySQL官宣的時間類型部分。
  • 轉換③(顯式):無時區轉TZ-OUT1,將MySQL讀出的無時區時間置爲TZ-OUT1時區(參考com.mysql.cj.result.SqlTimestampValueFactory#localCreateFromTimestamp())。
  • 轉換④(顯式):TZ-OUT1轉TZ-OUT2,這個轉換由Application-2負責,通常在DAO層完成。

仔細分析這4次時區轉換,其中①、②、③都是由MySQL完成,正確性不用懷疑,但因爲TZ-IN2和TZ-OUT1都是由應用指定,若是二者值不相同,那麼最後結果就會出現誤差(咱們踩到的就是這個坑)。至於④,那麼就得靠應用來保證正確性了,通常也不會出錯。說句題外話,無論是時區轉換,仍是其餘類型的數據轉換(好比字符集轉換),咱們能夠發現,正確轉換的關鍵在於數據接收方必須使用和數據發送方相同的格式。這看上去像是一句廢話,倒是解決此類問題的底層心法。

至此,這個MySQL Connect/J 8.0的時區陷阱就算被填平了,但願你從中有所收穫。

相關文章
相關標籤/搜索