【曹工雜談】Mysql-Connector-Java時區問題的一點理解--寫入資料庫的時間總是晚13小時問題

三國夢迴發表於2022-02-21

背景

去年寫了一篇“【曹工雜談】Mysql客戶端上,時間為啥和本地差了整整13個小時,就離譜 ”,結果最近還真就用上了。

不是我用上,是組內一位同事,他也是這樣:有個服務往資料庫insert記錄,記錄裡有時間,比如時間A。然後寫進資料庫後,資料庫裡的時間是A-13,晚了13小時。然後就改了這麼個地方:

寫進去的資料,就是正確的時間了。

後邊,他還有一個查詢服務,要去查寫進去那條記錄,比如記錄有個建立時間欄位,欄位值是2022-02-19 00:00:00. 然後假設我查的時候,就根據這個時間來查,傳個2022-02-19 00:00:00。結果發現,查不到。為啥呢,因為引數裡的時間也被減了13個小時,導致和伺服器端記錄的時間匹配不上了。

其實,兩個問題,是同一個問題,最終的解決辦法也是一樣的。

這個問題,抽象一下,就是,在mysql-connector-java 8.0.x版本下,我們傳送給伺服器的時間,為啥會少了13個小時。

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.16</version>
    </dependency>

關於mysql-connector-java

主要版本

現在主流的版本,有兩個,5.1.x系列和8.0.x系列,5.1.x系列最新的一個版本是5.1.49.

大家看下圖,有紅色字樣的 "1 vulnerability",表示有漏洞,這也是為什麼我們同事為啥要升級或者是被安全組逼著升級到8.0.x版本的原因。

8.0.x的最新版本是8.0.28,可以看到,沒有漏洞字樣:

版本差異

  1. 先給一份官方的:

    其實可以看出來,5.1和8.0的相容性都不錯,都支援mysql server端:5.6/5.7/8/0,差異無非是對jre和jdk的版本不一樣。

    這裡多說一句,mysql-connector-java是jdbc規範的一個實現,jdbc規範相關介面(java.sql和javax.sql裡的就是,比如java.sql.Driver),跟隨jdk一起釋出。

    jdbc規範版本 jdk
    4.0 jdk 6
    4.1 jdk 7
    4.2 jdk 8
    4.3 jdk 9及以後

    可參考:https://docs.oracle.com/en/java/javase/11/docs/api/java.sql/java/sql/package-summary.html

  2. connection property發生了變化,什麼是connection property,舉例:

    jdbc:mysql://1.1.1.1:3306/test?useSSL=false&serverTimezone=Asia/Shanghai
    

    上面的useSSL、serverTimezone就是connection property。

    具體變化:https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-properties-changed.html

  3. mysql driver的類名也發生了變化,5.1.x版本是叫 "com.mysql.jdbc.Driver",8.0.x裡面是 "com.mysql.cj.jdbc.Driver",而且,8.0版本不需要我們自己再去寫這種程式碼:

    // 註冊 JDBC 驅動
    String JDBC_DRIVER = "com.mysql.jdbc.Driver";
    Class.forName(JDBC_DRIVER);
    

    當然了,8.0版本對5.1版本做了相容,你即使載入5.1的driver,也沒影響。

  4. 還有些大家不用感知的,比如一些介面的包名發生變化,一些異常類被刪除了,因為我們一般不會直接用mysql-connector-java去程式設計,我們都是用jdbc介面嘛,實現類再怎麼變,也沒什麼影響

    https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-exceptions-changes.html

錯誤的時間,是客戶端傳送前就錯了,還是服務端錯了

界定問題範圍

問一下自己這個問題,主要是界定問題發生的地方。這個也容易界定,最理想的方式就是網路抓包,wireshark或者tcpdump自己選吧。

這裡先看下我的測試程式要做的事:

資料庫有下面這一條記錄,我要做的,就是根據時間引數,把記錄查出來。

程式如下:

我如果實際執行這個demo,是查不出結果的,為啥呢,我網路抓包的截圖給大家看看:

至於這個錯誤的時間,是怎麼來的,那可能確實需要慢慢去debug。

debug過程

看看我們前面的程式碼,設定時間引數主要是下面這一行:

Timestamp timestamp = new Timestamp(simpleDateFormat.parse("2022-02-17 22:49:27").getTime());
preparedStatement.setTimestamp(1, timestamp);

那我們直接一點,就在這行打上斷點,開始除錯:

這裡看得出來,是給this.query這個物件,設定相關的繫結引數。我們繼續跟進:

此時,時間依然還是正確的。我們傳了4個引數到setTimestamp方法,注意,第三個引數targetCalendar為null,這個引數會影響內部的分支。

看上圖,這裡因為targetCalendar為null,所以會去獲取當前這個mysql會話中的時區欄位。

這個時區是啥呢,就是CST。

也就是說,2022-02-17 22:49:27 這個時間,在CST時區下,就是 2022-02-17 08:49:27。

這裡CST說是有好幾個時區都是這個縮寫,比如:

  • Central Standard Time, North America's Central Time Zone: UTC−06:00,這個時間基本就是北美中部時間,北美中部包括了:美國、加拿大、墨西哥的中部地區
  • China Standard Time: UTC+08:00,這個就是中國的北京時間了,但感覺CST一般還是指:北美中部時間
  • Cuba Standard Time: UTC−04:00,這個其實點連結,會跳轉進入美洲東部時間的wiki,因為古巴也是在北美東部位置,包括了:美國、加拿大、墨西哥東南、巴拿馬、哥倫比亞、厄瓜多、祕魯等(這裡也有中美洲的一些地區)

可能國際上來說,看到CST,首先是任務是美國中部時區Central Standard Time(USA)UTC-06:00。一般不是是另外兩個時區,中國那肯定就是Asia/Shanghai,古巴這種小國,存在感也較弱

這個時區,是零時區 - 6(美國冬令時,從11月7日3月11日)或者是零時區 - 5(夏令時,從“3月11日”至“11月7日”),因為現在是美國的冬令時,所以這裡差14小時(我們是東八區嘛,8 + 6)。

ok,言歸正傳,反正問題就是出現在:會話的時區不對,為啥是CST啊,能不能改?

會話中的時區變數,怎麼是CST,什麼時候設定的

第一次設定(初始化)

targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone()

這裡面其實是獲取了:

com.mysql.cj.protocol.a.NativeServerSession#getDefaultTimeZone    

private TimeZone defaultTimeZone = TimeZone.getDefault();    
public TimeZone getDefaultTimeZone() {
    return this.defaultTimeZone;
}

我們可以在這個欄位上打個斷點,看看這個值什麼時候被設定:

然後重新debug整個程式,看看什麼時候進入該field斷點。我們會發現,第一次進入,就是在new這個類的物件時,

可以看看這個堆疊,基本就是獲取connection的時候,相當於就是建立一個會話,所以這裡會去new一個會話出來。

我看了下,在我機器上,初始化後,是東八區。

在第一次設定和第二次設定之間

這之間發生了一次重要的網路請求,

客戶端向服務端請求各種服務端的variable,也就是服務端的配置。上面有兩個時區相關的,system_time_zone和time_zone。

第二次設定

接下來,執行到了com.mysql.cj.protocol.a.NativeProtocol#configureTimezone,開始了第二次設定。

這個方法比較長,我分兩三段來截圖。

上圖比較清楚,就是:

  1. 獲取服務端的"time_zone"配置,如果“time_zone”為“system”,則獲取“system_time_zone”的配置

    我這邊資料庫吧,反正預設裝好就是這樣的,正好就是cst和system,也沒動過,所以這也是為啥國內大家很多人遇到這個問題的原因。

  2. 獲取客戶端自身建立連線時候的配置,通俗來說,就是dbUrl裡面那些connection property

  3. 如果客戶端沒配,則以服務端的為準

再接下來,就是以CST來設定成本次會話的預設時區。下面最後一行紅框的,也就是這第二次設定。

解決問題的思路

通過上面,我們知道了,如果客戶端沒設定時區,就會用服務端的。所以,兩種改法:

  1. 把服務端配置的system_time_zone和time_zone改成正確的,網上也有些教程,就是這樣。但是我們這邊公司大,資料庫很多業務在用,這麼改,怕影響到別人

  2. 客戶端連線url中,指定時區

    也就是這樣指定serverTimezone:

    jdbc:mysql://1.1.1.1:3306/test_ckl?useSSL=false&serverTimezone=Asia/Shanghai
    

我們改了客戶端,再看看。

跑完程式,正常查詢到資料:

id: 8; name:yyyy; time:22:49:27 

擴充套件資訊

這個整個互動中,一共有如下幾次網路請求。

  1. tcp三次握手
  2. 登入請求,帶著使用者名稱、密碼去登入
  3. 接下來,就是那次查詢服務端各種配置引數的請求,包括time_zone等全域性variable
  4. show warnings,這次請求應該就是看看服務端有沒有什麼警告資訊
  5. 客戶端發起的,"set names latin1"
  6. 客戶端發起:“SET character_set_results = NULL”
  7. 客戶端發起:SET autocommit=1
  8. 我們的業務查詢請求
  9. 結束會話
  10. 4次揮手

具體可以看下面的紅框部分:

總結

這個引數在服務端的配置我還沒來得及去看,不過對客戶端的影響,基本大致瞭解了。如果對大家也有些幫助,榮幸之至,謝謝大家。

相關文章