1. 程式人生 > >Mysql 時間差了 14 或 13 小時 com.mysql.cj.jdbc.Driver

Mysql 時間差了 14 或 13 小時 com.mysql.cj.jdbc.Driver

檢視mysql當前使用時區

show variables like '%time_zone%';

解決辦法

serverTimezone=CTT

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://xxx.xxx.xxx.xxx:3306/shys?serverTimezone=CTT&useUnicode=true&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false
spring.datasource.username=xxxxxx
spring.datasource.password=xxxxxx
spring.datasource.initialize=false

排錯過程

mysql-connector-java-8.0.13.jar

當 JDBC 與 MySQL 開始建立連線時

com.mysql.cj.jdbc.ConnectionImpl.initializePropsFromServer() throws SQLException {
    ...
    this.session.getProtocol().initServerSession();
    ...
}

com.mysql.cj.protocol.a.NativeProtocol.initServerSession() {
    configureTimezone();
    ...
}

com.mysql.cj.protocol.a.NativeProtocol.configureTimezone() {
    String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");

    if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
        configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone");
    }

    String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue();

    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 {
                canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor());
            } catch (IllegalArgumentException iae) {
                throw ExceptionFactory.createException(WrongArgumentException.class, iae.getMessage(), getExceptionInterceptor());
            }
        }
    }

    if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
        this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone));

        //
        // The Calendar class has the behavior of mapping unknown timezones to 'GMT' instead of throwing an exception, so we must check for this...
        //
        if (!canonicalTimezone.equalsIgnoreCase("GMT") && this.serverSession.getServerTimeZone().getID().equals("GMT")) {
            throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Connection.9", new Object[] { canonicalTimezone }),
                    getExceptionInterceptor());
        }
    }

    this.serverSession.setDefaultTimeZone(this.serverSession.getServerTimeZone());
}

追蹤程式碼可知,當 MySQL 的 time_zone 值為 SYSTEM 時,會取 system_time_zone 值作為協調時區。

重點在這裡!若 String configuredTimeZoneOnServer 得到的是 CST 那麼 Java 會誤以為這是 CST -0500 ,因此 TimeZone.getTimeZone(canonicalTimezone) 會給出錯誤的時區資訊。

如圖所示,本機預設時區是 Asia/Shanghai +0800 ,誤認為伺服器時區為 CST -0500 ,實際上伺服器是 CST +0800

com.mysql.cj.ClientPreparedQueryBindings.setTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength) {
    if (x == null) {
        setNull(parameterIndex);
    } else {

        x = (Timestamp) x.clone();

        if (!this.session.getServerSession().getCapabilities().serverSupportsFracSecs()
                || !this.sendFractionalSeconds.getValue() && fractionalLength == 0) {
            x = TimeUtil.truncateFractionalSeconds(x);
        }

        if (fractionalLength < 0) {
            // default to 6 fractional positions
            fractionalLength = 6;
        }

        x = TimeUtil.adjustTimestampNanosPrecision(x, fractionalLength, !this.session.getServerSession().isServerTruncatesFracSecs());

        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);
    }
}

原因

Timestamp 被轉換為會話時區的時間字串了。問題到此已然明晰:

  1. JDBC 誤認為會話時區在 CST-5
  2. JBDC 把 Timestamp+0 轉為 CST-5 的 String-5
  3. MySQL 認為會話時區在 CST+8,將 String-5 轉為 Timestamp-13

最終結果相差 13 個小時!如果處在冬令時還會相差 14 個小時!