背景
去年寫了一篇“【曹工雜談】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,可以看到,沒有漏洞字樣:
版本差異
-
先給一份官方的:
其實可以看出來,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
-
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
-
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,也沒影響。
-
還有些大家不用感知的,比如一些接口的包名發生變化,一些異常類被刪除了,因為我們一般不會直接用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
,開始了第二次設置。
這個方法比較長,我分兩三段來截圖。
上圖比較清楚,就是:
-
獲取服務端的"time_zone"配置,如果“time_zone”為“system”,則獲取“system_time_zone”的配置
我這邊數據庫吧,反正默認裝好就是這樣的,正好就是cst和system,也沒動過,所以這也是為啥國內大家很多人遇到這個問題的原因。
-
獲取客戶端自身建立連接時候的配置,通俗來說,就是dbUrl里面那些connection property
-
如果客戶端沒配,則以服務端的為准
再接下來,就是以CST來設置成本次會話的默認時區。下面最后一行紅框的,也就是這第二次設置。
解決問題的思路
通過上面,我們知道了,如果客戶端沒設置時區,就會用服務端的。所以,兩種改法:
-
把服務端配置的system_time_zone和time_zone改成正確的,網上也有些教程,就是這樣。但是我們這邊公司大,數據庫很多業務在用,這么改,怕影響到別人
-
客戶端連接url中,指定時區
也就是這樣指定serverTimezone:
jdbc:mysql://1.1.1.1:3306/test_ckl?useSSL=false&serverTimezone=Asia/Shanghai
我們改了客戶端,再看看。
跑完程序,正常查詢到數據:
id: 8; name:yyyy; time:22:49:27
擴展信息
這個整個交互中,一共有如下幾次網絡請求。
- tcp三次握手
- 登錄請求,帶着用戶名、密碼去登錄
- 接下來,就是那次查詢服務端各種配置參數的請求,包括time_zone等全局variable
- show warnings,這次請求應該就是看看服務端有沒有什么警告信息
- 客戶端發起的,"set names latin1"
- 客戶端發起:“SET character_set_results = NULL”
- 客戶端發起:SET autocommit=1
- 我們的業務查詢請求
- 結束會話
- 4次揮手
具體可以看下面的紅框部分:
總結
這個參數在服務端的配置我還沒來得及去看,不過對客戶端的影響,基本大致了解了。如果對大家也有些幫助,榮幸之至,謝謝大家。