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");
        }
      // 從配置中查找你對(duì)時(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)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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