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%';
变量名 | 说明 |
---|---|
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 中国标准时间。
此时 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()
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小时。
6、为什么 mysql-connector-java:5.1.46
没有出现时区协商歧义?
com.mysql.jdbc.ConnectionImpl.configureTimezone()
跟踪代码发现,并没有修改 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]