一、CST 时区混乱
CST是一个混乱的时区,它有四种含义:
- 美国标准时间 Central Standard Time (USA):
UTC-06:00
(或UTC-05:00
)。夏令时:3月11日至11月7日,使用UTC-05:00
;冬令时:11月8日至次年3月11日,使用UTC-06:00
- 澳大利亚标准时间 Central Standard Time (Australia):
UTC+09:30
- 中国标准时 China Standard Time:
UTC+08:00
- 古巴标准时 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:00
或UTC-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:00
或UTC-06:00
)
Java 设置默认时区:
- 通过代码指定,TimeZone.setDefault(timeZone)
- 通过JVM参数指定,-Duser.timezone=Asia/Shanghai
- 通过环境变量指定,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 时区有:
- 获得 mysql 服务端的 time_zone 值,若 time_zone 为 SYSTEM,获得 system_time_zone 的值,可叫做服务端配置的时区
- 获取 url 配置的 serverTimezone 的值,可叫做客户端配置的时区
- 若 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:00
或 UTC-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 时区有:
- 获取 url 配置的 serverTimezone 的值,可叫做客户端配置的时区
- 若 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