MySQL Connect/J 8.0時區(qū)陷阱

image

最近公司正在升級Spring Boot版本(從1.5升級到2.1)阎抒,其間踩到一個非常隱晦的MySQL時區(qū)陷阱坯门,具體來說返敬,就是數(shù)據(jù)庫讀出的歷史數(shù)據(jù)的時間和實際時間差了14個小時椅挣,而新寫入的數(shù)據(jù)又都正常头岔。如果你之前也是使用默認的MySQL時區(qū)配置,那么大概率會碰到這個問題鼠证,深究其背后的原因又涉及到很多技術(shù)細節(jié)峡竣,故整理出來分享給大家。

首先來看一下原因量九。升級到Boot 2.1之后适掰,MySQL Connect/J版本也隨之升級到8.0颂碧,會優(yōu)先使用連接參數(shù)(serverTimezone)中指定的時區(qū),如果沒有指定类浪,則再使用數(shù)據(jù)庫配置的時區(qū)载城,參考下面的官宣(對應的源代碼是com.mysql.cj.protocol.a.NativeProtocol#configureTimezone())。由于我們之前數(shù)據(jù)庫連接參數(shù)沒有指定時區(qū)费就,并且數(shù)據(jù)庫配置的是默認的CST時區(qū)(美國中部時區(qū)诉瓦,即-6:00),所以讀取出來的時間出現(xiàn)偏差力细。

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

  • 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).

找到原因之后睬澡,解決辦法就比較直白了,

方法一:數(shù)據(jù)庫的連接參數(shù)添加serverTimezone=Asia/Shanghai或者serverTimezone=GMT%2B8眠蚂。Boot 1.5下不需要添加此參數(shù)煞聪,但添加了也無妨。

方法二:修改MySQL數(shù)據(jù)庫的time_zone配置逝慧,改為+8:00(默認是SYSTEM)昔脯。采用此方法,則不需要修改數(shù)據(jù)庫連接參數(shù)馋艺。

方法二顯然更優(yōu)栅干,一次修改,終生受益捐祠。但要注意碱鳞,對于升級到Boot 2.1之后新生成的那批數(shù)據(jù),如果包含時間類型的字段并且該字段值是應用指定的而不是數(shù)據(jù)庫生成的(例如DEFAULT CURRENT_TIMESTAMP)踱蛀,那么需要手動修復(加上偏差的小時數(shù))窿给。

兩個解決辦法都很簡單,有同學馬上會問率拒,為什么Boot 1.5下沒有這個問題崩泡?為什么Boot 2.0下讀取歷史數(shù)據(jù)存在14個小時的偏差,而新生成的數(shù)據(jù)又是好的猬膨?要回答這兩個問題角撞,看官宣就不夠了,需要讀一下MySQL Connect/J的源代碼勃痴。

謎題一谒所,為什么Boot 1.5下沒有這個問題?答案隱藏在com.mysql.jdbc.ResultSetImplcom.mysql.jdbc.ConnectionImpl兩個類的源代碼中沛申。

// 源代碼:com.mysql.jdbc.ResultSetImpl
private TimeZone getDefaultTimeZone() {
        // useLegacyDatetimeCode默認為true劣领,因此使用connection的默認時區(qū)
        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的默認時區(qū)使用的是JVM的默認時區(qū),一般為操作系統(tǒng)的時區(qū)
        // 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默認使用操作系統(tǒng)的時區(qū)(Asia/Shanghai尖淘,即+8:00)奕锌,而忽略連接參數(shù)或者數(shù)據(jù)庫指定的時區(qū),因此不管是讀數(shù)據(jù)還是寫數(shù)據(jù)都是使用統(tǒng)一的時區(qū)村生,因此不存在時間偏差惊暴。

謎題二,為什么Boot 2.0下讀取歷史數(shù)據(jù)存在14個小時的偏差梆造,而新生成的數(shù)據(jù)又是好的缴守?升級到Boot 2.0之后,MySQL Connect/J改為使用數(shù)據(jù)庫配置的CST時區(qū)镇辉,而歷史數(shù)據(jù)是在Boot 1.5下的Asia/Shanghai時區(qū)生成的屡穗,因此讀出來存在14(-6:00和+8:00之間)個小時的偏差。對于新生成的數(shù)據(jù)忽肛,由于同處在CST時區(qū)下村砂,因此沒有偏差。

解完這兩個謎題屹逛,你可能還有些疑惑础废。那么接下來,結(jié)合數(shù)據(jù)流轉(zhuǎn)的順序罕模,我們再來分析一下數(shù)據(jù)流轉(zhuǎn)過程中時區(qū)的變化评腺。

image

設定Application-1為數(shù)據(jù)生產(chǎn)方,Application-2為數(shù)據(jù)消費方淑掌,TZ-IN1為Application-1所處的時區(qū)蒿讥,TZ-IN2為Application-1寫入數(shù)據(jù)庫的時區(qū),TZ-OUT1為Application-2讀出數(shù)據(jù)庫的時區(qū)抛腕,TZ-OUT2為Application-2所處的時區(qū)芋绸。如前所述,TZ-IN2和TZ-OUT1由連接參數(shù)或者數(shù)據(jù)庫配置決定担敌。

整個數(shù)據(jù)流轉(zhuǎn)過程摔敛,會涉及3次顯式的時區(qū)轉(zhuǎn)換和1次隱式的時區(qū)轉(zhuǎn)換。

  • 轉(zhuǎn)換①(顯式):TZ-IN1轉(zhuǎn)TZ-IN2全封,這個轉(zhuǎn)換由MySQL Connect/J完成(參考com.mysql.cj.ClientPreparedQueryBindings#setTimestamp()马昙,限于篇幅,此處不再展開分析)刹悴。
  • 轉(zhuǎn)換②(隱式):TZ-IN2轉(zhuǎn)無時區(qū)给猾,MySQL內(nèi)部存儲時間類型的字段時或者忽略時區(qū)(DateTime類型)或者使用UTC(Timestamp類型),參考MySQL官宣的時間類型部分颂跨。
  • 轉(zhuǎn)換③(顯式):無時區(qū)轉(zhuǎn)TZ-OUT1,將MySQL讀出的無時區(qū)時間置為TZ-OUT1時區(qū)(參考com.mysql.cj.result.SqlTimestampValueFactory#localCreateFromTimestamp())扯饶。
  • 轉(zhuǎn)換④(顯式):TZ-OUT1轉(zhuǎn)TZ-OUT2恒削,這個轉(zhuǎn)換由Application-2負責池颈,一般在DAO層完成。

仔細分析這4次時區(qū)轉(zhuǎn)換钓丰,其中①躯砰、②、③都是由MySQL完成携丁,正確性不用懷疑琢歇,但由于TZ-IN2和TZ-OUT1都是由應用指定,如果兩者值不相同梦鉴,那么最后結(jié)果就會出現(xiàn)偏差(我們踩到的就是這個坑)李茫。至于④,那么就得靠應用來保證正確性了肥橙,一般也不會出錯魄宏。說句題外話,不管是時區(qū)轉(zhuǎn)換存筏,還是其他類型的數(shù)據(jù)轉(zhuǎn)換(比如字符集轉(zhuǎn)換)宠互,我們可以發(fā)現(xiàn),正確轉(zhuǎn)換的關鍵在于數(shù)據(jù)接收方必須使用和數(shù)據(jù)發(fā)送方相同的格式椭坚。這看上去像是一句廢話予跌,卻是解決此類問題的底層心法。

至此善茎,這個MySQL Connect/J 8.0的時區(qū)陷阱就算被填平了券册,希望你從中有所收獲。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末巾表,一起剝皮案震驚了整個濱河市汁掠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌集币,老刑警劉巖考阱,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鞠苟,居然都是意外死亡乞榨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門当娱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吃既,“玉大人,你說我怎么就攤上這事跨细○幸校” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵冀惭,是天一觀的道長震叙。 經(jīng)常有香客問我掀鹅,道長,這世上最難降的妖魔是什么媒楼? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任乐尊,我火速辦了婚禮,結(jié)果婚禮上划址,老公的妹妹穿的比我還像新娘扔嵌。我一直安慰自己,他們只是感情好夺颤,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布痢缎。 她就那樣靜靜地躺著,像睡著了一般拂共。 火紅的嫁衣襯著肌膚如雪牺弄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天宜狐,我揣著相機與錄音势告,去河邊找鬼。 笑死抚恒,一個胖子當著我的面吹牛咱台,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播俭驮,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼回溺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了混萝?” 一聲冷哼從身側(cè)響起遗遵,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎逸嘀,沒想到半個月后车要,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡崭倘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年翼岁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片司光。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡琅坡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出残家,到底是詐尸還是另有隱情榆俺,我是刑警寧澤,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站谴仙,受9級特大地震影響迂求,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晃跺,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望毫玖。 院中可真熱鬧掀虎,春花似錦、人聲如沸付枫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阐滩。三九已至二打,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掂榔,已是汗流浹背继效。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留装获,地道東北人瑞信。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像穴豫,于是被迫代替她去往敵國和親凡简。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容