MYSQL 时区问题

背景

5月1号 ABM 侧告知我方在 5月1号 10点 上传的 4月30号的对账单数据有异常 - 表现为缺失了 68 笔数据。

事故原因

1、直接原因:

生成对账文件的数据库表的 create_time 字段的时间晚于正常时间 13 个小时,以至于一部分实际是在 4月30号 产生的数据,create_time 记录成了 4月29 号的数据了。

这就使得在 5月1号 10点 生成的 4月30号 的对账文件中少了 68 笔。而 4月30号 10点 生成的 4月29号的对账文件多了 68 笔。

2、根本原因

ABM 充值接口由 api-upc 项目拆分到 api-uoc 后,在代码和 mysql 数据库连接完全一样的情况下出现了时间小于真实时间 13 小时。


GMT

GMT Greenwich Mean Time 格林威治标准时间,英国伦敦格林威治定为0°经线开始的地方,地球每15°经度 被分为一个时区,共分为24个时区,相邻时区相差一小时;例: 中国北京位于东八区,GMT时间比北京时间慢8小时。

UCT

UCT Coordinated Universal Time 协调世界时,是以原子时秒长为基础,精确到秒,误差在0.9s以内,在时刻上尽量接近于 GMT, 是比 GMT 更为精确的世界时间。

GMT = UCT

查看 mysql 时区

show variables like '%time_zone%';

140110

变量名 说明
system_time_zone 在 mysql 启动的时候读取操作系统的时区
time_zone 如果 time_zone 为 SYSTEM 就使用 system_time_zone

CST 时区的说明

CST 时区是容易产生歧义的时区,美国中部、澳大利亚中部、中国、古巴的标准时间的简写都是 CST。

时区简写 时区全称 说明
CST Central Standard Time (USA) UT-6:00 美国中部标准时间
CDT Central Daylight Time (USA) UT-500 美国中部夏令时间
ACST Australia Central Standard Time (Australia) UT+9:30 澳大利亚中部标准时间
CST China Standard Time UT+8:00 中国标准时间
CST Cuba Standard Time UT-5:00 古巴标准时间
CDT Cuba Daylight Time UT-4:00 古巴夏令时间

写入 mysql 数据库时间晚于系统时间 13/14 小时

1、如何复现

确保操作系统的时区为 CST - China Standard Time 中国标准时间。

143028

143122

此时 mysql system_time_zone = CST 表示的是 China Standard Time。

mysql 驱动版本:mysql-connector-java:8.0.16

2、实验结果

3、出现原因

该版本的 mysql 驱动在读取到 system_time_zone = CST 时将这个 CST 理解成了 Central Standard Time (USA) 美国中部标准时间,因此,5+8=13、6+8=14 差了 13 或 14 小时。

4、源码分析 mysql-connector-java:8.0.16

com.mysql.cj.jdbc.ConnectionImpl.initializePropsFromServer()

145914

public void configureTimezone() {
  			// 获取mysql时区配置,结果是SYSTEM
        String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");
    //因为我的数据库time_zone是SYSTEM,所以就使用system_time_zone作为数据的时区,如一开始mysql查询结果,时区为CST,既configuredTimeZoneOnServer=CST
        if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
            configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone");
        }
      // 从配置中查找你对时区的配置,如果你没有这里为null。getPropertySet()就保存了你在url链接中设置的属性
        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 {
                  //协商java中的时区,因为Mysql为CST,所以这里也是CST,但是这个 CST 并不是 GMT+8 而是GMT-5/GMT-6 出现歧义。
                    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());
            }
        }
//将默认从 JVM 中获取的时区改为将刚刚得到的 Java 的时区
this.serverSession.setDefaultTimeZone(this.serverSession.getServerTimeZone());
}

5、在时间转换时使用到了时区

由于此时的默认时区被上一步修改为了 CST (实际是 GMT-5/GMT-6,而非GMT+8),因此在格式化时时间差了 13、14小时。

152938

6、为什么 mysql-connector-java:5.1.46 没有出现时区协商歧义?

com.mysql.jdbc.ConnectionImpl.configureTimezone()

image-20200504154921402

跟踪代码发现,并没有修改 session 中的默认时区。因此没有出现时区协商歧义。

sun.util.calendar.ZoneInfo[id="CST",offset=-21600000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=CST,offset=-21600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]]
sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=29,lastRule=null]