JDBC與mysql同為CST時(shí)區(qū)導(dǎo)致數(shù)據(jù)庫時(shí)間和客戶端時(shí)間差13或者14小時(shí)

摘要
線上排查問題時(shí)候碰到一個(gè)奇怪的問題怖竭,代碼中讀取一天的記錄。代碼中設(shè)置時(shí)間是從零點(diǎn)到夜里二十四點(diǎn)陡蝇。但是讀取出來的記錄的開始是既然是從13點(diǎn)開始的痊臭。然后看了JDBC的源碼發(fā)現(xiàn)主要原因是Mysql的CST時(shí)間與Java中CST時(shí)間是不一樣的,下面給出問題的排查過程登夫。

情景再現(xiàn)

1广匙、代碼中用的java.util.Date類型、換成TimeStamp類型也沒有解決問題
2恼策、數(shù)據(jù)庫中用的TimeStamp類型
3鸦致、mysql 版本5.6.x
4、jdk版本1.8
5涣楷、mysql-connector-java 8.0.13
我的數(shù)據(jù)庫時(shí)區(qū)信息如下:

select @@system_time_zone;
+--------------------+
| @@system_time_zone |
+--------------------+
| CST                |
+--------------------+
1 row in set (4.81 sec)

mysql> select @@time_zone;
+-------------+
| @@time_zone |
+-------------+
| SYSTEM      |
+-------------+
1 row in set (0.04 sec)

CST時(shí)間

CST時(shí)間有四種解釋分唾,所以不同項(xiàng)目中可能代碼的意義不一樣,比如Mysql和Java狮斗。這也是這次錯(cuò)誤的主要原因绽乔。Java和Mysql協(xié)商時(shí)區(qū)時(shí)把Mysql的CST時(shí)間當(dāng)成了美國中部時(shí)間既UTC-5(美國從“3月11日”至“11月7日”實(shí)行夏令時(shí),美國中部時(shí)間改為 UTC-05:00,其他時(shí)候是UTC-06:00)情龄。我們國家是UTC+08:00 時(shí)區(qū)迄汛,所以差了13個(gè)小時(shí)(13小時(shí)還是14小時(shí)捍壤,取決于你傳遞給數(shù)據(jù)庫的時(shí)間)骤视,

  • 美國中部時(shí)間 Central Standard Time (USA) UTC-05:00 / UTC-06:00
  • 澳大利亞中部時(shí)間 Central Standard Time (Australia) UTC+09:30
  • 中國標(biāo)準(zhǔn)時(shí) China Standard Time UTC+08:00
  • 古巴標(biāo)準(zhǔn)時(shí) Cuba Standard Time UTC-04:00

CST in Java

SimpleDateFormat f1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
f1.setTimeZone(TimeZone.getTimeZone("CST"));
System.out.println(f1.parse("2015-09-01 00:00:00"));

上面代碼的輸出如下:

Tue Sep 01 13:00:00 CST 2015

比實(shí)際的時(shí)間多了13個(gè)小時(shí)。所以你在Java中的時(shí)間被認(rèn)為是UTC-5時(shí)間鹃觉,而數(shù)據(jù)庫任務(wù)時(shí)間是UTC-8時(shí)間专酗。這就是我們上面錯(cuò)誤的原因。

源碼分析

關(guān)鍵的代碼在ConnectionImpl類中盗扇,代碼如下祷肯,已經(jīng)去掉一些無關(guān)重要的代碼

private void initializePropsFromServer() throws SQLException {
        String connectionInterceptorClasses = this.propertySet.getStringProperty(PropertyKey.connectionLifecycleInterceptors).getStringValue();

        this.session.setSessionVariables();

        this.session.loadServerVariables(this.getConnectionMutex(), this.dbmd.getDriverVersion()); //查詢數(shù)據(jù)庫一些重要的系統(tǒng)配置

        this.autoIncrementIncrement = this.session.getServerSession().getServerVariable("auto_increment_increment", 1);

        this.session.buildCollationMapping();

        this.session.getProtocol().initServerSession();// 初始化會(huì)話,協(xié)商時(shí)區(qū)代碼就在里面

        checkTransactionIsolationLevel();

        this.session.checkForCharsetMismatch();

        this.session.configureClientCharacterSet(false);

        handleAutoCommitDefaults();

其中this.session.loadServerVariables(this.getConnectionMutex(), this.dbmd.getDriverVersion());查詢Mysql重要配置疗隶。比如時(shí)區(qū)佑笋。具體查詢的信息如下:

StringBuilder queryBuf = new StringBuilder(versionComment).append("SELECT");
                queryBuf.append("  @@session.auto_increment_increment AS auto_increment_increment");
                queryBuf.append(", @@character_set_client AS character_set_client");
                queryBuf.append(", @@character_set_connection AS character_set_connection");
                queryBuf.append(", @@character_set_results AS character_set_results");
                queryBuf.append(", @@character_set_server AS character_set_server");
                queryBuf.append(", @@collation_server AS collation_server");
                queryBuf.append(", @@collation_connection AS collation_connection");
                queryBuf.append(", @@init_connect AS init_connect");
                queryBuf.append(", @@interactive_timeout AS interactive_timeout");
                if (!versionMeetsMinimum(5, 5, 0)) {
                    queryBuf.append(", @@language AS language");
                }
                queryBuf.append(", @@license AS license");
                queryBuf.append(", @@lower_case_table_names AS lower_case_table_names");
                queryBuf.append(", @@max_allowed_packet AS max_allowed_packet");
                queryBuf.append(", @@net_write_timeout AS net_write_timeout");
                if (!versionMeetsMinimum(8, 0, 3)) {
                    queryBuf.append(", @@query_cache_size AS query_cache_size");
                    queryBuf.append(", @@query_cache_type AS query_cache_type");
                }
                queryBuf.append(", @@sql_mode AS sql_mode");
                queryBuf.append(", @@system_time_zone AS system_time_zone");
                queryBuf.append(", @@time_zone AS time_zone");
                if (versionMeetsMinimum(8, 0, 3) || (versionMeetsMinimum(5, 7, 20) && !versionMeetsMinimum(8, 0, 0))) {
                    queryBuf.append(", @@transaction_isolation AS transaction_isolation");
                } else {
                    queryBuf.append(", @@tx_isolation AS transaction_isolation");
                }
                queryBuf.append(", @@wait_timeout AS wait_timeout");

this.session.getProtocol().initServerSession();這就是協(xié)商時(shí)區(qū)的代碼,也是我們重點(diǎn)需要關(guān)注的代碼斑鼻。如下

public void configureTimezone() {
        // 獲取mysql時(shí)區(qū)配置蒋纬,結(jié)果是SYSTEM
        String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");
        //因?yàn)槲业臄?shù)據(jù)庫time_zone是SYSTEM,所以就使用system_time_zone作為數(shù)據(jù)的時(shí)區(qū),如一開始mysql查詢結(jié)果蜀备,時(shí)區(qū)為CST关摇,既configuredTimeZoneOnServer=CST
        if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
            configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone");
        }
      // 從配置中查找你對時(shí)區(qū)的配置,如果你沒有這里為null碾阁。getPropertySet()就保存了你的數(shù)據(jù)庫用戶名输虱、密碼、字符編碼啊等你在url鏈接中設(shè)置的屬性
        String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue();
      //因?yàn)槲覜]有配置serverTimezone屬性脂凶,所以canonicalTimezone==null
        if (configuredTimeZoneOnServer != null) {
            // user can override this with driver properties, so don't detect if that's the case
            if (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) {
                try {
//協(xié)商java中的時(shí)區(qū)宪睹,因?yàn)镸ysql為CST,所以這里也是CST
                    canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor());
                } catch (IllegalArgumentException iae) {
                    throw ExceptionFactory.createException(WrongArgumentException.class, iae.getMessage(), getExceptionInterceptor());
                }
            }
        }

        if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
//將剛剛得到的Java的時(shí)區(qū)設(shè)置到會(huì)話中
            this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone));
        }
        this.serverSession.setDefaultTimeZone(this.serverSession.getServerTimeZone());
    }

再來看一下蚕钦,如果給sql語句的占位符中傳遞值的時(shí)候代碼

  this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", targetCalendar,
                    targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone());

            StringBuffer buf = new StringBuffer();
            buf.append(this.tsdf.format(x));
            if (this.session.getServerSession().getCapabilities().serverSupportsFracSecs()) {
                buf.append('.');
                buf.append(TimeUtil.formatNanos(x.getNanos(), 6));
            }
            buf.append('\'');

            setValue(parameterIndex, buf.toString(), MysqlType.TIMESTAMP);

代碼中把Date或者TimeStamp轉(zhuǎn)換為String横堡,而且用了協(xié)商的時(shí)區(qū)。
分析到這里冠桃,問題基本上說清楚了命贴。那么我們?nèi)绾谓鉀Q這個(gè)問題呢?

總結(jié)

1食听、數(shù)據(jù)庫時(shí)區(qū)最好不要設(shè)置成CST胸蛛,以免出現(xiàn)上面的錯(cuò)誤
2、當(dāng)數(shù)據(jù)庫中的時(shí)間用的是時(shí)間類型時(shí)候樱报,Java中可以用String葬项,但是不適應(yīng)做國際化
3、在數(shù)據(jù)庫連接字符串中設(shè)置時(shí)區(qū)迹蛤。如下(推薦的方式):

jdbc:mysql://xxxx:3306/table_name?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末民珍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子盗飒,更是在濱河造成了極大的恐慌嚷量,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逆趣,死亡現(xiàn)場離奇詭異蝶溶,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)宣渗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門抖所,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人痕囱,你說我怎么就攤上這事田轧。” “怎么了鞍恢?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵傻粘,是天一觀的道長巷查。 經(jīng)常有香客問我,道長抹腿,這世上最難降的妖魔是什么岛请? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮警绩,結(jié)果婚禮上崇败,老公的妹妹穿的比我還像新娘。我一直安慰自己肩祥,他們只是感情好后室,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著混狠,像睡著了一般岸霹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上将饺,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天贡避,我揣著相機(jī)與錄音,去河邊找鬼予弧。 笑死刮吧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的掖蛤。 我是一名探鬼主播杀捻,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蚓庭!你這毒婦竟也來了致讥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤器赞,失蹤者是張志新(化名)和其女友劉穎垢袱,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拳魁,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惶桐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了潘懊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贿衍,死狀恐怖授舟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情贸辈,我是刑警寧澤释树,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響奢啥,放射性物質(zhì)發(fā)生泄漏秸仙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一桩盲、第九天 我趴在偏房一處隱蔽的房頂上張望寂纪。 院中可真熱鬧,春花似錦赌结、人聲如沸捞蛋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拟杉。三九已至,卻和暖如春量承,著一層夾襖步出監(jiān)牢的瞬間搬设,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榴鼎,地道東北人物舒。 一個(gè)月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像贞言,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子阀蒂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

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