背景
去年寫了一篇“【曹工雜談】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次揮手
具體可以看下面的紅框部分:
總結
這個引數在服務端的配置我還沒來得及去看,不過對客戶端的影響,基本大致瞭解了。如果對大家也有些幫助,榮幸之至,謝謝大家。