java · 2020-06-30 0

Java与MySQL时区

一、CST 时区混乱

CST是一个混乱的时区,它有四种含义:

  1. 美国标准时间 Central Standard Time (USA):UTC-06:00(或UTC-05:00)。夏令时:3月11日至11月7日,使用 UTC-05:00;冬令时:11月8日至次年3月11日,使用 UTC-06:00
  2. 澳大利亚标准时间 Central Standard Time (Australia):UTC+09:30
  3. 中国标准时 China Standard Time:UTC+08:00
  4. 古巴标准时 Cuba Standard Time:UTC-04:00

中国其实也实行过夏令时,(1992年之后中国已经没有再实行过夏令时了),当实行夏令时,中国标准时间的时区偏移量就是+09:00,当非夏令时,中国标准时间的时区偏移量就是+08:00

CST在Linux、MySQL、Java中的含义:

  • 在Linux或MySQL中,CST表示的是:中国标准时间(UTC+08:00)
  • 在Java中,CST表示的是:中央标准时间(美国标准时间)(UTC-05:00UTC-06:00)

注:Java 的 Date 中的CST是表示的中国标准时间

早期基准是:GMT(格林尼治标准时间)
后来基准是:UTC(协调世界时)

二、Java 时区

@Test
public void testTimeZone() {
    TimeZone defaultTZ = TimeZone.getDefault();
    TimeZone shanghaiTZ = TimeZone.getTimeZone("Asia/Shanghai");
    TimeZone chinaTZ = TimeZone.getTimeZone("GMT+08:00");
    TimeZone tokyoTZ = TimeZone.getTimeZone("Asia/Tokyo");
    TimeZone utc = TimeZone.getTimeZone("UTC");
    TimeZone gmt = TimeZone.getTimeZone("GMT");
    TimeZone cst = TimeZone.getTimeZone("CST");

    Date date = new Date(0L);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    sdf.setTimeZone(defaultTZ);
    System.out.println(String.format("%-15s %-15s %s", defaultTZ.getID(), defaultTZ.getDisplayName(), sdf.format(date)));

    sdf.setTimeZone(shanghaiTZ);
    System.out.println(String.format("%-15s %-15s %s", shanghaiTZ.getID(), shanghaiTZ.getDisplayName(), sdf.format(date)));

    sdf.setTimeZone(chinaTZ);
    System.out.println(String.format("%-15s %-15s %s", chinaTZ.getID(), chinaTZ.getDisplayName(), sdf.format(date)));

    sdf.setTimeZone(tokyoTZ);
    System.out.println(String.format("%-15s %-15s %s", tokyoTZ.getID(), tokyoTZ.getDisplayName(), sdf.format(date)));

    sdf.setTimeZone(utc);
    System.out.println(String.format("%-15s %-15s %s", utc.getID(), utc.getDisplayName(), sdf.format(date)));

    sdf.setTimeZone(gmt);
    System.out.println(String.format("%-15s %-15s %s", gmt.getID(), gmt.getDisplayName(), sdf.format(date)));

    sdf.setTimeZone(cst);
    System.out.println(String.format("%-15s %-15s %s", cst.getID(), cst.getDisplayName(), sdf.format(date)));
}

控制台输出:

Asia/Shanghai   中国标准时间          1970-01-01 08:00:00
Asia/Shanghai   中国标准时间          1970-01-01 08:00:00
GMT+08:00       GMT+08:00       1970-01-01 08:00:00
Asia/Tokyo      日本标准时间          1970-01-01 09:00:00
UTC             协调世界时           1970-01-01 00:00:00
GMT             格林尼治标准时间        1970-01-01 00:00:00
CST             北美中部标准时间        1969-12-31 18:00:00

在Java中,CST表示的是:中央标准时间(美国标准时间)(UTC-05:00UTC-06:00)

Java 设置默认时区:

  1. 通过代码指定,TimeZone.setDefault(timeZone)
  2. 通过JVM参数指定,-Duser.timezone=Asia/Shanghai
  3. 通过环境变量指定,export TZ=Asia/Shanghai

三、MySQL 服务端时区

  • system_time_zone (系统时区):在 MySQL 启动时会检查当前系统的时区并根据系统时区设置全局参数 system_time_zone 的值,默认值一般为 CST
  • time_zone (全局时区或当前会话时区)

可通过 SQL 语句查看:

mysql> show global variables like '%time_zone%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | CST    |
| time_zone        | SYSTEM |
+------------------+--------+
2 rows in set (0.00 sec)

四、Java 与 MySQL 时区转换

1. jdbc 驱动

url : jdbc:mysql://127.0.0.1:3306/nginx_log?serverTimezone=GMT%2B9

对于 mysql-connector-java-8.0.18.jar,设置 jdbc 的 serverSession 时区有:

  1. 获得 mysql 服务端的 time_zone 值,若 time_zone 为 SYSTEM,获得 system_time_zone 的值,可叫做服务端配置的时区
  2. 获取 url 配置的 serverTimezone 的值,可叫做客户端配置的时区
  3. 若 serverTimezone 为 null,设置 serverSession 的时区是服务端的时区;若 serverTimezone 有值,设置 serverSession 的时区是客户端的时区

设置 serverSession 时区优先级:

  • time_zone 是 SYSTEM,serverTimezone > system_time_zone
  • time_zone 不是 SYSTEM,serverTimezone > time_zone

注:从 mysql 服务端获得 system_time_zone 若为 CST,java 中 CST 表示 UTC-05:00UTC-06:00

// com.mysql.cj.protocol.a.NativeProtocol.java
    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());
                }
            }
        }

        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-connector-java-8.0.29.jar 驱动,设置 jdbc 的 serverSession 时区有:

  1. 获取 url 配置的 serverTimezone 的值,可叫做客户端配置的时区
  2. 若 serverTimezone 为 null,设置 serverSession 的时区是 java 的默认时区;若 serverTimezone 有值,设置 serverSession 的时区是客户端的时区

设置 serverSession 时区优先级:

  • serverTimezone > java 默认时区
// com.mysql.cj.protocol.a.NativeProtocol.java
    public void configureTimeZone() {
        String connectionTimeZone = getPropertySet().getStringProperty(PropertyKey.connectionTimeZone).getValue();

        TimeZone selectedTz = null;

        if (connectionTimeZone == null || StringUtils.isEmptyOrWhitespaceOnly(connectionTimeZone) || "LOCAL".equals(connectionTimeZone)) {
            selectedTz = TimeZone.getDefault();

        } else if ("SERVER".equals(connectionTimeZone)) {
            // Session time zone will be detected after the first ServerSession.getSessionTimeZone() call.
            return;

        } else {
            selectedTz = TimeZone.getTimeZone(ZoneId.of(connectionTimeZone)); // TODO use ZoneId.of(String zoneId, Map<String, String> aliasMap) for custom abbreviations support
        }

        this.serverSession.setSessionTimeZone(selectedTz);

        if (getPropertySet().getBooleanProperty(PropertyKey.forceConnectionTimeZoneToSession).getValue()) {
            // TODO don't send 'SET SESSION time_zone' if time_zone is already equal to the selectedTz (but it requires time zone detection)

            StringBuilder query = new StringBuilder("SET SESSION time_zone='");

            ZoneId zid = selectedTz.toZoneId().normalized();
            if (zid instanceof ZoneOffset) {
                String offsetStr = ((ZoneOffset) zid).getId().replace("Z", "+00:00");
                query.append(offsetStr);
                this.serverSession.getServerVariables().put("time_zone", offsetStr);
            } else {
                query.append(selectedTz.getID());
                this.serverSession.getServerVariables().put("time_zone", selectedTz.getID());
            }

            query.append("'");
            sendCommand(getCommandBuilder().buildComQuery(null, query.toString()), false, 0);
        }
    }

综上:解决时区问题,可设置 url 中的 serverTimezone,serverTimezone 与 mysql 服务端的 system_time_zone 保持一致

2.java 与 mysql

Java 系统时区:Asia/Shanghai(东8区)
JDBC 数据库连接时区:serverTimezone=-5
MySQL 全局时区:time_zone=+08:00

jdbc