记排查jdbc版本升级后时间差14小时排查

背景

项目中使用slic 3.1.0 版本,配合mysql-connector-java-5.1.24,当有需求将jdbc升级至高版本以适配可能的mysql 8.0版本时,发现插入数据库的时间比实际早了14个小时

调研

从代码部分出发,问题出在slick的update函数,我们的代码调用update更新数据库的部分类型为Timestamp的字段,slick包中的update函数源码

// slick_2.11-3.1.0.jar
package slick.driver

/** An Action that updates the data selected by this query. */
    def update(value: T): DriverAction[Int, NoStream, Effect.Write] = {
      new SimpleJdbcDriverAction[Int]("update", Vector(sres.sql)) {
        def run(ctx: Backend#Context, sql: Vector[String]): Int = ctx.session.withPreparedStatement(sql.head) { st =>
          st.clearParameters
          converter.set(value, st)
          sres.setter(st, converter.width+1, param)
          st.executeUpdate
        }
      }
    }

传入参数value即为要更新的值,如下图,可见到此处的时间还是正确的。update函数通过UPDATE TABLE_NAME SET status = ?, time1 = ? , time2 = ? where TABLE_NAME.ID = 233; ,结合value参数,转化为实际的update sql语句,然后由executeUpdate执行拼接好的sql语句。
记排查jdbc版本升级后时间差14小时排查_第1张图片
运行至set函数内部,源码如下,会根据value的类型T对其做转换,本例中是将代码中的java.util.Date类型转化为java.sql.Timestamp,会调用jdbc包中的setTimestamp函数

def set(value: T, pp: Writer) = {
    var i = 0
    while(i < len) {
      cha(i).asInstanceOf[ResultConverter[M, Any]].set(value.productElement(i), pp)
      i += 1
    }
  }

此处两个版本差别较大,部分源码如下。

  • 8.0.13版本:其中参数targetCalendar为用户是否设置TimeZone,即请求url中是否有类似serverTimezone=Asia/Shanghai,本例未加,因此targetCalendar=null,其this.tsdf.calendar.zone如下图
    记排查jdbc版本升级后时间差14小时排查_第2张图片
    接着在同样的环境测试,其余条件不变,只在url中添加参数serverTimezone=Asia/Shanghaithis.tsdf.calendar.zone如下图
    记排查jdbc版本升级后时间差14小时排查_第3张图片
// mysql-connector-java-8.0.13.
package com.mysql.cj;

@Override
    public void setTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength) {
     	... ...
        this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", targetCalendar,
                    targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone());

		... ...
  • 5.1.24版本:重要部分为传入setTimestampInternal的第四个参数,其决定了内部参数的时区,即setTimestampInternal的tz参数,如下图可见,其ZoneInfo的 id = Asia/Shanghai, offset = 28800000,即为中国标准时区
    记排查jdbc版本升级后时间差14小时排查_第4张图片
/ mysql-connector-java-5.1.24.jar
package com.mysql.jdbc;

/**
	 * Set a parameter to a java.sql.Timestamp value. The driver converts this
	 * to a SQL TIMESTAMP value when it sends it to the database.
	 * 
	*/
public void setTimestamp(int parameterIndex, Timestamp x)
			throws java.sql.SQLException {
		setTimestampInternal(parameterIndex, x, null, Util.getDefaultTimeZone(), false);
	}

/**
	 * Set a parameter to a java.sql.Timestamp value. The driver converts this
	 * to a SQL TIMESTAMP value when it sends it to the database.
	 */
	 private void setTimestampInternal(int parameterIndex,
			Timestamp x, Calendar targetCalendar,
			TimeZone tz, boolean rollForward)

剖析

在程序一开始,TimeZone初始为空,则调用getDefaultRef为其设置一个初始值,如下所示,根据调用链显示,最终调用native方法getSystemTimeZoneID,传入参数为javaHome,设置完后this.defaultTimeZone.getID() = Asia/Shanghai,两个版本表现一致。

package java.util.TimeZone
/**
     * Returns the reference to the default TimeZone object. This
     * method doesn't create a clone.
     */
    static TimeZone getDefaultRef() {
        TimeZone defaultZone = defaultTimeZone;
        if (defaultZone == null) {
            // Need to initialize the default time zone.
            defaultZone = setDefaultZone();
            assert defaultZone != null;
        }
        // Don't clone here.
        return defaultZone;
    }
    
    private static synchronized TimeZone setDefaultZone() {
        TimeZone tz;
        // get the time zone ID from the system properties
        String zoneID = AccessController.doPrivileged(
                new GetPropertyAction("user.timezone"));

        // if the time zone ID is not set (yet), perform the
        // platform to Java time zone ID mapping.
        if (zoneID == null || zoneID.isEmpty()) {
            String javaHome = AccessController.doPrivileged(
                    new GetPropertyAction("java.home"));
            try {
                zoneID = getSystemTimeZoneID(javaHome);
                if (zoneID == null) {
                    zoneID = GMT_ID;
                }
            } catch (NullPointerException e) {
                zoneID = GMT_ID;
            }
        }

        // Get the time zone for zoneID. But not fall back to
        // "GMT" here.
        tz = getTimeZone(zoneID, false);

        if (tz == null) {
            // If the given zone ID is unknown in Java, try to
            // get the GMT-offset-based time zone ID,
            // a.k.a. custom time zone ID (e.g., "GMT-08:00").
            String gmtOffsetID = getSystemGMTOffsetID();
            if (gmtOffsetID != null) {
                zoneID = gmtOffsetID;
            }
            tz = getTimeZone(zoneID, true);
        }
        ... ...
        return tz;
    }
    
    /**
     * Gets the platform defined TimeZone ID.
     **/
    private static native String getSystemTimeZoneID(String javaHome);

前文提过,jdbc每次connect之前,都会通过configureTimezone设置时区,源码如下,同样分为8.0.13版本和5.1.24版本

// 8.0.13
package com.mysql.cj.protocol.a;
/**
     * Configures the client's timezone if required.
     * 
     * @throws CJException
     *             if the timezone the server is configured to use can't be
     *             mapped to a Java timezone.
     */
    public void 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());
                }
            }
        }
        
        // canonicalTimezone = "CST"
        if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
            this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone));

            ... ...

        this.serverSession.setDefaultTimeZone(this.serverSession.getServerTimeZone());
    }
    
// 5.1.24
package com.mysql.jdbc;
/**
	 * Configures the client's timezone if required.
	 * 
	 * @throws SQLException
	 *             if the timezone the server is configured to use can't be
	 *             mapped to a Java timezone.
	 */
	private void configureTimezone() throws SQLException {
		String configuredTimeZoneOnServer = this.serverVariables.get("timezone");

		if (configuredTimeZoneOnServer == null) {
			configuredTimeZoneOnServer = this.serverVariables.get("time_zone");

			if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
				configuredTimeZoneOnServer = this.serverVariables.get("system_time_zone");
			}
		}

		String canoncicalTimezone = getServerTimezone();
		
		if ((getUseTimezone() || !getUseLegacyDatetimeCode()) && configuredTimeZoneOnServer != null){... ...}
		
		// canoncicalTimezone = null
		if (canoncicalTimezone != null && canoncicalTimezone.length() > 0) {
				this.serverTimezoneTZ = TimeZone.getTimeZone(canoncicalTimezone);

			... ...
		}
	}
  • 8.0.13 版本:在url不显式设置TimeZone时,canonicalTimezone = "CST",因此会进入下面的if分支,调用TimeZone.getTimeZone(),传入参数”CST“,输出时区信息如下
    记排查jdbc版本升级后时间差14小时排查_第5张图片
  • 5.1.24版本:在url不显式设置TimeZone时,canonicalTimezone = null,因此不会进入if分支,直接退出,采用默认时区,即前文采用native方法getSystemTimeZoneID获取的Asia/Shanghai
// 病根
//  CST -> 美国时区
//  Asia/Shanghai -> 中国八区
private static TimeZone getTimeZone(String ID, boolean fallback) {
        TimeZone tz = ZoneInfo.getTimeZone(ID);
        if (tz == null) {
            tz = parseCustomTimeZone(ID);
            if (tz == null && fallback) {
                tz = new ZoneInfo(GMT_ID, 0);
            }
        }
        return tz;
    }

总结

两个版本对时区默认设置的策略不同,如下表

5.1.24 8.0.13
不显式设置时区 采用系统默认(native) 采用mysql server时区(CST)
显式设置 设置优先 设置优先

你可能感兴趣的:(学到的知识点,Web相关)