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


背景

去年寫了一篇“【曹工雜談】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次揮手

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

總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM