数据库时间慢了14个小时,Mybatis说,这个锅我不背


针对上述问题可通过数据库层面和代码层面进行解决。

方案一:修改数据库时区

既然是Mysql理解错了CST指定的时区,那么就将其设置为正确的。

连接Mysql数据库,设置正确的时区:

[root@xxxxx ~]# mysql -uroot -p

mysql> set global time_zone = ‘+8:00’;

mysql> set time_zone = ‘+8:00’

mysql> flush privileges;

复制代码

再次执行show命令:

show variables like ‘%time_zone%’;

±---------------------------+

|Variable         | Value |

±---------------------------+

|system_time_zone |CST   |

|time_zone     |+08:00 |

复制代码

可以看到时区已经成为东八区的时间了。再次执行单元测试,问题得到解决。

此种方案也可以直接修改MySQL的my.cnf文件进行指定时区。

方案二:修改数据库连接参数

在代码连接数据库时,通过参数指定所使用的时区。

在配置数据库连接的URL后面添加上指定的时区serverTimezone=Asia/Shanghai

url: jdbc:mysql://xx.xx.xx.xx:3306/db_name?useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=Asia/Shanghai

复制代码

再次执行单元测试,问题同样可以得到解决。

问题完了?


经过上述分析与操作,时区的问题已经解决了。问题就这么完事了吗?为什么是这样呢?

为了验证时区问题,在时区错误的数据库中,创建了一个字段,该字段类型为datetime,默认值为CURRENT_TIMESTAMP。

那么,此时插入一条记录,让Mysql自动生成该字段的时间,你猜该字段的时间是什么?中国时间。

神奇不?为什么同样是CST时区,系统自动生成的时间是正确的,而代码插入的时间就有时差问题呢?

到底是Mysql将CST时区理解为美国时间了,还是Mybatis、连接池或驱动程序将其理解为美国时间了?

重头戏开始

为了追查到底是代码中哪里出了问题,先开启Mybatis的debug日志,看看insert时是什么值:

2021-11-25 11:05:28.367 [|1637809527983|] DEBUG 20178 — [   scheduling-1] c.h.s.m.H.listByCondition               : ==> Parameters: 2021-11-25 11:05:27(String), 0(Integer), 1(Integer), 2(Integer), 3(Integer), 4(Integer)

复制代码

上面是insert时的参数,也就是说在Mybatis层面时间是没问题的。排除一个。

那是不是连接池或驱动程序的问题?连接池本身来讲跟数据库连接的具体操作关系不大,就直接来排查驱动程序。

Mybatis是xml中定义日期字段类型为TIMESTAMP,扒了一下mysql-connector-Java-8.0.x的源码,发现SqlTimestampValueFactory是用来处理TIMESTAMP类型的。

SqlTimestampValueFactory的构造方法上打上断点,执行单元测试:

timezone

可以明确的看到,Calendar将时区设置为Locale.US,也就是美国时间,时区为CST,offset为-21600000。-21600000单位为毫秒,转化为小时,恰好是“-6:00”,这与北京时间“GMT+08:00”恰好相差14个小时。

于是一路往上最终追溯调用链路,该TimeZone来自NativeServerSession的serverTimeZone,而serverTimeZone的值是由NativeProtocol类的configureTimezone方法设置的。

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) {

// 此处设置TimeZone

this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone));

if (!canonicalTimezone.equalsIgnoreCase(“GMT”) && this.serverSession.getServerTimeZone().getID().equals(“GMT”)) {

throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString(“Connection.9”, new Object[] { canonicalTimezone }),

getExceptionInterceptor());

}

}

}

复制代码

debug跟踪一下上述代码,显示信息如下:

CST获得

至此,通过canonicalTimezone值的获取,可以看出URL后面配置serverTimezone=Asia/Shanghai的作用了。其中,上面第一个代码块获取time_zone的值,第二个代码块中获取system_time_zone的值。这与查询数据库获得的值一致。

因为出问题时并未在url中添加参数serverTimezone=Asia/Shanghai,所以走canonicalTimezone为null的情况。随后逻辑中调用了TimeUtil.getCanonicalTimezone方法:

public static String getCanonicalTimezone(String timezoneStr, ExceptionInterceptor exceptionInterceptor) {

if (timezoneStr == null) {

return null;

}

timezoneStr = timezoneStr.trim();

// handle ‘+/-hh:mm’ form …

if (timezoneStr.length() > 2) {

if ((timezoneStr.charAt(0) == ‘+’ || timezoneStr.charAt(0) == ‘-’) && Character.isDigit(timezoneStr.charAt(1))) {

return “GMT” + timezoneStr;

}

}

synchronized (TimeUtil.class) {

if (timeZoneMappings == null) {

loadTimeZoneMappings(exceptionInterceptor);

}

}

String canonicalTz;

if ((canonicalTz = timeZoneMappings.getProperty(timezoneStr)) != null) {

return canonicalTz;

}

throw ExceptionFactory.createException(InvalidConnectionAttributeException.class,

Messages.getString(“TimeUtil.UnrecognizedTimezoneId”, new Object[] { timezoneStr }), exceptionInterceptor);

}

复制代码

上述代码中最终走到了loadTimeZoneMappings(exceptionInterceptor);方法:

private static void loadTimeZoneMappings(ExceptionInterceptor exceptionInterceptor) {

timeZoneMappings = new Properties();

try {

timeZoneMappings.load(TimeUtil.class.getResourceAsStream(TIME_ZONE_MAPPINGS_RESOURCE));

} catch (IOException e) {

throw ExceptionFactory.createException(Messages.getString(“TimeUtil.LoadTimeZoneMappingError”), exceptionInterceptor);

}

// bridge all Time Zone ids known by Java

for (String tz : TimeZone.getAvailableIDs()) {

if (!timeZoneMappings.containsKey(tz)) {

timeZoneMappings.put(tz, tz);

}

}

}

复制代码

该方法加载了配置文件"/com/mysql/cj/util/TimeZoneMapping.properties"里面的值,经过转换,timeZoneMappings中,对应CST的为"CST"。

最终得到canonicalTimezone为“CST”,而TimeZone获得是通过TimeZone.getTimeZone(canonicalTimezone)方法获得的。

也就是说TimeZone.getTimeZone(“CST”)的值为美国时间。写个单元测试验证一下:

public class TimeZoneTest {

@Test

public void testTimeZone(){

System.out.println(TimeZone.getTimeZone(“CST”));

}

}

复制代码

打印结果:

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]]

复制代码

很显然,该方法传入CST之后,默认是美国时间。

至此,问题原因基本明朗

  • Mysql中设置的server_time_zone为CST,time_zone为SYSTEM

  • Mysql驱动查询到time_zone为SYSTEM,于是使用server_time_zone的值,为”CST“ 。

  • JDK中TimeZone.getTimeZone(“CST”)获得的值为美国时区

  • 以美国时区构造的Calendar类

  • SqlTimestampValueFactory使用上述Calendar来格式化系统获取的中国时间,时差问题便出现了

  • 最终反映在数据库数据上就是错误的时间

serverVariables变量


再延伸一下,其中server_time_zone和time_zone都来自于NativeServerSession的serverVariables变量,该变量在NativeSession的loadServerVariables方法中进行初始化,关键代码:

if (versionMeetsMinimum(5, 1, 0)) {

StringBuilder queryBuf = new StringBuilder(versionComment).append(“SELECT”);

queryBuf.append(" @@session.auto_increment_increment AS auto_increment_increment");

queryBuf.append(“, @@character_set_client AS character_set_client”);

queryBuf.append(“, @@character_set_connection AS character_set_connection”);

queryBuf.append(“, @@character_set_results AS character_set_results”);

queryBuf.append(“, @@character_set_server AS character_set_server”);

queryBuf.append(“, @@collation_server AS collation_server”);

queryBuf.append(“, @@collation_connection AS collation_connection”);

queryBuf.append(“, @@init_connect AS init_connect”);

queryBuf.append(“, @@interactive_timeout AS interactive_timeout”);

if (!versionMeetsMinimum(5, 5, 0)) {

queryBuf.append(“, @@language AS language”);

}

queryBuf.append(“, @@license AS license”);

queryBuf.append(“, @@lower_case_table_names AS lower_case_table_names”);

queryBuf.append(“, @@max_allowed_packet AS max_allowed_packet”);

queryBuf.append(“, @@net_write_timeout AS net_write_timeout”);

queryBuf.append(“, @@performance_schema AS performance_schema”);

if (!versionMeetsMinimum(8, 0, 3)) {

queryBuf.append(“, @@query_cache_size AS query_cache_size”);

queryBuf.append(“, @@query_cache_type AS query_cache_type”);

}

queryBuf.append(“, @@sql_mode AS sql_mode”);

queryBuf.append(“, @@system_time_zone AS system_time_zone”);

queryBuf.append(“, @@time_zone AS time_zone”);

if (versionMeetsMinimum(8, 0, 3) || (versionMeetsMinimum(5, 7, 20) && !versionMeetsMinimum(8, 0, 0))) {

queryBuf.append(“, @@transaction_isolation AS transaction_isolation”);

} else {

queryBuf.append(“, @@tx_isolation AS transaction_isolation”);

}

queryBuf.append(“, @@wait_timeout AS wait_timeout”);

NativePacketPayload resultPacket = sendCommand(this.commandBuilder.buildComQuery(null, queryBuf.toString()), false, 0);

Resultset rs = ((NativeProtocol) this.protocol).readAllResults(-1, false, resultPacket, false, null,

new ResultsetFactory(Type.FORWARD_ONLY, null));

Field[] f = rs.getColumnDefinition().getFields();

if (f.length > 0) {

ValueFactory vf = new StringValueFactory(this.propertySet);

Row r;

if ((r = rs.getRows().next()) != null) {

for (int i = 0; i < f.length; i++) {

this.protocol.getServerSession().getServerVariables().put(f[i].getColumnLabel(), r.getValue(i, vf));

}

}

}

复制代码

在上述StringBuilder的append操作中,有"@@time_zone AS time_zone"和"@@system_time_zone AS system_time_zone"两个值,然后查询数据库,从数据库获得值之后,put到serverVariables中。

再来debug一下:

面试结束复盘查漏补缺

每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。

以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~

重要的事说三遍,关注+关注+关注!

历经30天,说说我的支付宝4面+美团4面+拼多多四面,侥幸全获Offer

image.png

更多笔记分享

历经30天,说说我的支付宝4面+美团4面+拼多多四面,侥幸全获Offer

}

}

}

复制代码

在上述StringBuilder的append操作中,有"@@time_zone AS time_zone"和"@@system_time_zone AS system_time_zone"两个值,然后查询数据库,从数据库获得值之后,put到serverVariables中。

再来debug一下:

面试结束复盘查漏补缺

每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。

以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~

重要的事说三遍,关注+关注+关注!

[外链图片转存中…(img-dURpFCIM-1720112008613)]

[外链图片转存中…(img-Efqi5MjT-1720112008614)]

更多笔记分享

[外链图片转存中…(img-xY33QbdY-1720112008615)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值