77百科网
当前位置: 首页 生活百科

mysql时区问题(不指定时区会踩坑)

时间:2023-07-10 作者: 小编 阅读量: 2 栏目名: 生活百科

使用的驱动是com.mysql.cj.JDBC.Driver。当应用启动后,首次发起数据库操作,就会创建JDBC的代码,MyBatis把这事情干了,获取连接,从连接池,笔者使用HikariDataSource,HikariPool连接池。

前言

旧项目 MySQL Java 升级驱动,本来一切都好好的,但是升级到 8.x 的驱动后,发现入库的时间比实际时间相差 13 个小时,这就很奇怪了,如果相差 8 小时,那么还可以说是时区不对,从驱动源码分析看看。

1. Demo

pom 依赖,构造一个真实案例,这里的 8.0.22 版本。

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>Spring-boot-starter-web</artifactId><version>2.5.4</version></dependency><dependency><groupId>org.MyBatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version><exclusions><exclusion><artifactId>slf4j-api</artifactId><groupId>org.slf4j</groupId></exclusion></exclusions></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version><scope>runtime</scope></dependency></dependencies>

随意写一个 da o controller main 。

@SpringBootApplication@MapperScan("com.feng.mysql.rep")public class MySQLDateMain {public static void main(String[] args) {SpringApplication.run(MySQLDateMain.class, args);}} @RestControllerpublic class UserController {@Autowiredprivate UserRepository userRepository;@RequestMapping(value = "/Users/User", method = RequestMethod.POST)public String addUser(){UserEntity userEntity = new UserEntity();userEntity.setAge(12);userEntity.setName("tom");userEntity.setCreateDate(new Date(SYSTEM.currentTimeMillis()));userEntity.setUpdateDate(new Timestamp(System.currentTimeMillis()));userRepository.insertUser(userEntity);return "ok";}} @Mapperpublic interface UserRepository {@Insert("insert into User (name, age, createDate, updateDate) values (#{name}, #{age}, #{createDate}, #{updateDate})")int insertUser(UserEntity userEntity);}

数据库设计:

CREATE TABLE `work`.`User`(`id` int(10) UNSIGNED ZEROFILL NOT NULL AUTO_INCREMENT,`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,`age` int NULL DEFAULT NULL,`createDate` timestamp NULL DEFAULT NULL,`updateDate` datetime NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 29 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

1.1 验证

系统时间

调用 HTTP 接口 http://localhost:8080/Users/User

可以看到与真实时间相差 13 小时,诡异了,明明数据库时间是正确的, 而且我的系统时间也是正确的,那么我们可以就只能在驱动找原因,因为当我使用。

2. 问题原因分析

2.1 时区获取

上一步,我们看见系统时间,数据库时间都是正确的,那么做文章的推断就是驱动造成的,以 8.0.22 驱动为例。

使用的驱动是 com.mysql.cj.JDBC.Driver

当应用启动后,首次发起数据库操作,就会创建 JDBC 的代码,MyBatis 把这事情干了,获取连接,从连接池,笔者使用 HikariDataSource,HikariPool连接池。

在 com.mysql.cj.jdbc.ConnectionImpl 里面会初始化 session 的拦截器,属性Variables、列映射、自动提交信息等等,其中有一行代码初始化时区:

this.session.getProtocol().initServersession();

com.mysql.cj.protocol.a.NativeProtocol

public void configureTimezone() {//获取MySQL server端的时区String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");//如果是SYSTEM,则获取系统时区if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone");}//配置文件获取时区serverTimezone配置,即可以手动配置,这是一个解决问题的手段String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue();//未指定时区,且读取到MySQL时区,就if (configuredTimeZoneOnServer != null) {// user can override this with driver properties, so don't detect if that's the caseif (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) {try {//规范时区?难道直接读取的不规范:sweat_smile:,这步很重要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...//时区不规范,比如不是GMT,然而ID标识GMTif (!canonicalTimezone.equalsIgnoreCase("GMT") && this.serverSession.getServerTimeZone().getID().equals("GMT")) {throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Connection.9", new Object[] {canonicalTimezone}),getExceptionInterceptor());}}}

规范时区:

/*** Returns the 'official' Java timezone name for the given timezone* * @param timezoneStr*the 'common' timezone name* @param exceptionInterceptor*exception interceptor* * @return the Java timezone name for the given timezone*/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);}

比如我的数据库时区是CST,拿到了:

这是系统时区,拿到的是 CST,根源是读取了内置的时区值:

然而这个文件没有 CST 时区定义,需要去 JDK 去拿,然后缓存。这就说明一个道理,CST 这个时区定义不明确。

时区就是 CST 了,仅仅是 CST 时区而已,这里并不能说明 CST 有什么问题, 真正的问题是 CST 怎么比东八区少 13 个小时呢?

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

根源就是这几句代码:

public static TimeZone getTimeZone(String var0) {return ZoneInfoFile.getZoneInfo(var0);}

开始初始化,sun.timezone.ids.oldmapping 这个一般不会设置。

读取 $JAVA_HOME/lib/tzdb.dat,这是一个 JDK 时区存储文件。

其中 PRC 就是中国时区,但是这个文件并未定义 CST。

CST在这里定义的: addOldMapping();

private static void addOldMapping() {String[][] var0 = oldMappings;int var1 = var0.length;for (int var2 = 0; var2 < var1;var2) {String[] var3 = var0[var2];//这里就把CST时区设置为芝加哥时区aliases.put(var3[0], var3[1]);}if (USE_OLDMAPPING) {aliases.put("EST", "America/New_York");aliases.put("MST", "America/Denver");aliases.put("HST", "Pacific/Honolulu");} else {zones.put("EST", new ZoneInfo("EST", -18000000));zones.put("MST", new ZoneInfo("MST", -25200000));zones.put("HST", new ZoneInfo("HST", -36000000));}}

oldMappings 是啥呢?

private static String[][] oldMappings = new String[][] {{"ACT","Australia/Darwin"}, {"AET","Australia/Sydney"}, {"AGT","America/Argentina/Buenos_Aires"}, {"ART","Africa/Cairo"}, {"AST","America/Anchorage"}, {"BET","America/Sao_Paulo"}, {"BST","Asia/Dhaka"}, {"CAT","Africa/Harare"}, {"CNT","America/St_Johns"}, {"CST","America/Chicago"}, {"CTT","Asia/Shanghai"}, {"EAT","Africa/Addis_Ababa"}, {"ECT","Europe/Paris"}, {"IET","America/Indiana/Indianapolis"}, {"IST","Asia/Kolkata"}, {"JST","Asia/Tokyo"}, {"MIT","Pacific/Apia"}, {"NET","Asia/Yerevan"}, {"NST","Pacific/Auckland"}, {"PLT","Asia/Karachi"}, {"PNT","America/Phoenix"}, {"PRT","America/Puerto_Rico"}, {"PST","America/Los_Angeles"}, {"SST","Pacific/Guadalcanal"}, {"VST","Asia/Ho_Chi_Minh"}};

{"CST", "America/Chicago"} :sob:

private static ZoneInfo getZoneInfo0(String var0) {try {//缓存获取ZoneInfo var1 = (ZoneInfo) zones.get(var0);if (var1 != null) {return var1;} else {String var2 = var0;if (aliases.containsKey(var0)) {var2 = (String) aliases.get(var0);}int var3 = Arrays.binarySearch(regions, var2);if (var3 < 0) {return null;} else {byte[] var4 = ruleArray[indices[var3]];DataInputStream var5 = new DataInputStream(new ByteArrayInputStream(var4));var1 = getZoneInfo(var5, var2);//首次获取,存缓存zones.put(var0, var1);return var1;}}} catch (Exception var6) {throw new RuntimeException("Invalid binary time-zone data: TZDB:"var0", version: "versionId, var6);}}

就这样 CST 时区就被 JDK 认为是 美国芝加哥的时区 了,:confounded:

2.2 时区设置

那么 JDBC 在哪里设置时间的呢?

进一步可以看到在服务器上时区都是 OK 的。

但是,在 com.mysql.cj.ClientPreparedQueryBindings 的 setTimestamp 方法中,获取了 session 时区,然后 format,:sweat_smile:

时间从此丢失 13 小时,原因是 format 的锅,因为 用的美国芝加哥时间格式化 ,如果使用 long 时间的话或者什么都不处理就没有问题。

SimpleDateFormat 设置 CST 时区,前面已经分析了,这个时区就是美国芝加哥时区。

JDK 会认为 CST 是美国芝加哥的时区,UTC-5,但是我们的时间是 UTC 8,换算成 US的时间就是,当前时间 - 8 - 5,即时间少 13 小时。这里不设置时区(即使用客户端时区)即可正常返回时间。

那么 CST 时区是什么呢?笔者写博客的时间 是2021-09-22,是 CST 的夏令时

CST 是中部标准时间,现在是 UTC-5,即夏令时,冬季还会变成 UTC-6。

标准的 US 的 CST 时间是 UTC-6,我当前的时间是 23:56。

关键在于 CST 定义非常模糊,而 MySQL 驱动调用 SimpleDateFormat,使用的 CST 为美国芝加哥时区,当前的季节为 UTC-5。

3. 解决办法

根据上面的分析,解决 CST 时区的方法非常多。

  • 设置 MySQL Server 的时区为非 CST 时区;
  • 设置 MySQL 的系统时区为非 CST 时区;
  • 通过参数增加 serverTimezone设 置为明确的 MySQL 驱动的 properties 定义的时区;
  • 修改 MySQL Java 驱动,获取时区通过客户端获取,比如当前运行环境,通过 JDK 获取。

3.1 解决办法详细说明

设置 MySQL Server 的时区

set global time_zone = ' 08:00';

或者修改 MySQL 的配置文件 /etc/mysql/mysql.conf.d/mysqld.cnf。

[mysqld] 节点下增加:

default-time-zone = ' 08:00'

设置系统时区

以 Ubuntu 为例:

timedatectl set-timezone Asia/Shanghai

参数增加 serverTimezone

jdbc:mysql://localhost:3306/work?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai

修改MySQL驱动

比如获取时区通过 client 端获取,Date 数据使用什么时区,就使用这个时区 format,但是一般而言我们不会自己发布驱动,跟随 MySQL 官方更新,只有大厂有机会自己运营 MySQL 驱动。

3.2 官方解决方案

笔者在浏览 MySQL 8.0.x 驱动发布的时候在 8.0.23 版本发现了特别的发布记录,笔者在初始时使用 8.0.22 版本是有深意的,:smile:

MySQL :: MySQL Connector/J 8.0 Release Notes :: Changes in MySQL Connector/J 8.0.23 (2021-01-18, General Availability)

看来官方修复了。:smile:

来源码看看。果然,不配置就客户端获取时区了TimeZone.getDefault();

public void configureTimeZone() {//先读配置connectionTimeZoneString connectionTimeZone = getPropertySet().getStringProperty(PropertyKey.connectionTimeZone).getValue();TimeZone selectedTz = null;//如果没配参数,或者参数配LOCAL,就取客户端时区//配置其他选择,基本上参数决定了时区,不再MySQL server去获取时区了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);//默认不再强制把时区塞进session 的 Variables中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(this.commandBuilder.buildComQuery(null, query.toString()), false, 0);}}

再看看设置参数的地方,这里设计有点改变,通过 QueryBindings 接口抽象了处理逻辑:

public void setTimestamp(int parameterIndex, Timestamp x) throws java.sql.SQLException {synchronized(checkClosed().getConnectionMutex()) {((PreparedQuery << ? > ) this.query).getQueryBindings().setTimestamp(getCoreParameterIndex(parameterIndex), x, MysqlType.TIMESTAMP);}}

实现 com.mysql.cj.ClientPreparedQueryBindings:

public void bindTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength, MysqlType targetMysqlType) {if (fractionalLength < 0) {// default to 6 fractional positionsfractionalLength = 6;}x = TimeUtil.adjustNanosPrecision(x, fractionalLength, !this.session.getServerSession().isServerTruncatesFracSecs());StringBuffer buf = new StringBuffer();if (targetCalendar != null) {buf.append(TimeUtil.getSimpleDateFormat("''yyyy-MM-dd HH:mm:ss", targetCalendar).format(x));} else {this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", targetMysqlType == MysqlType.TIMESTAMP && this.preserveInstants.getValue() ? this.session.getServerSession().getSessionTimeZone() : this.session.getServerSession().getDefaultTimeZone());buf.append(this.tsdf.format(x));}if (this.session.getServerSession().getCapabilities().serverSupportsFracSecs() && x.getNanos() > 0) {buf.append('.');buf.append(TimeUtil.formatNanos(x.getNanos(), 6));}buf.append('\'');setValue(parameterIndex, buf.toString(), targetMysqlType);}

时区就是刚刚设置的,亚洲/上海。

总结

一个时区问题,居然里面有这么多玩头,MySQL 居然在 8.0.23 才修复这个,难道 MySQL 认为大家都会配置时区,还是服务器都不使用 CST 时区。 另外如果使用 UTC 时区,是一个精准的时区,表示 0 区时间,就会从一个坑跳另一个坑。 所以, 还是精准用 Asia/Shanghai 吧,或者驱动升级 8.0.23 及 以上版本,不配置时区。

原文链接: blog.csdn.net/fenglllle/article/details/120423274

    推荐阅读
  • dnf守护者转职什么好(2022dnf守护者4个转职排行)

    随着3.25版本的临近,国服终于迎来了守护者的三次觉醒。目前比较推荐的顺序是:第一辅助帕拉丁、第二小召唤黑曜神、第三飞天龙神、第四大地女神!我们主要是从下边2个方面分析的:DNF:守护者三觉,1、团本受欢迎程度。反而作为强力的34仔,帕拉丁有着先天的优势。不过龙神伤害更高一点,大地女神就算了,劲舞团操作太墨迹了。所以选择帕拉丁做一套巨龙史诗,反而是最简单的!未经允许,禁止转载!

  • 大家对走步健身有什么建议(不用10000步每天这样走)

    每天100秒,健康100分!若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与本网联系,我们将及时更正、删除,谢谢。

  • 哈利波特与魔法石小说简介(讲的是什么内容)

    《哈利·波特》是英国作家J·K·罗琳的魔幻文学系列小说,共7集,其中前六部以霍格沃茨魔法学校为主要舞台,描写的是主人公哈利·波特在霍格沃茨魔法学校六年的学习生活和冒险故事。第七本描写的是哈利·波特在校外寻找魂器并消灭伏地魔的故事。J.K.罗琳,生于英国的格温特郡的ChippingSodbury普通医院。毕业于英国埃克塞特大学,学习法语和古典文学,获文理学士学位。2000年,被母校授予荣誉文学博士学位。毕业后曾在英国曼彻斯特接受教学培训。

  • 买车位应该买大还是买小(买车位被坑数十万)

    280户业主共享9个停车位,合同款与实付款相差近10万!而衡量车位是否买卖的唯一标准就是能否办理产权证。值得注意的是小区的产权车位仅供小区业主购买,且不能转卖给小区以外的人员。人防车位就是防空地下室,主要用来避难避灾,价格一般都比普通车位低,但只有使用权,没有产权,一旦发生自然灾害,会被国家无偿征用。另外,车位的出租、转手往往仅限于本小区内进行,缺乏其他投资方式必要的灵活性。

  • 第三者责任险可以保哪些(你真的了解第三者责任险吗)

    第三者责任险的含义:指被保险人或其允许的驾驶员,在使用受保险的车辆时发生事故,导致第三者受到人身伤亡或财产直接损毁,依法应当由被保险人承担的经济责任,这时候可以由保险公司负责赔偿。第三者责任险赔偿范围:保险公司对被保险机动车发生道路交通事故造成受害人的人身伤亡、财产损失,在责任限额内予以赔偿。主要赔偿费用有死亡伤残、医疗费用、财产损失三者。

  • 关于北漂的爱情电影有哪些(电影频道出品电影立水桥北开机)

    电影《立水桥北》开机仪式现场由电影频道节目中心出品,中共北京昌平区委宣传部、中共北川羌族自治县委宣传部协拍,苏州柏乔文化传媒有限公司承制的电影《立水桥北》于9月26日在北京昌平举行了开机仪式。主创团队纷纷表示,必将全力以赴,把这部电影打造为后疫情时代又一部彰显人性光芒、温暖人心的现实主义佳作。

  • 当你口袋空空的时候你总要低头(当你仰望星空时)

    哆啦A梦世界中的宇宙,很有可能是所有科幻作品中对地球最友善的宇宙。另有理论声称,此事已经发生过了。因此,在中国古人眼中,星象与人事相关,倒是毋庸置疑的事实。在星象与人事关系密切这一点上,东西方古代文明的人类可谓一拍即合。宇宙初始阶段,则是“萌而未兆,并气同色,混沌不

  • 入住日租房有什么注意事项 入住日租房需要办理什么手续

    日租房一般不接受退房时交费,请谅解,填写入住单.店家向房客交代相关事宜。

  • 大众胎压复位(维修师傅是这样说的)

    以下内容大家不妨参考一二希望能帮到您!行驶十到几十公里,电脑会记忆当前车轮转动情况,当有某一个轮子漏气时,因为少气的轮子直径变化,引起转动与设定记忆时的转动有区别,激活胎压监测灯亮起警告。补胎后,重新充气,再按压SET键3秒,按的同时,仪表有胎压监测灯亮起,三秒后,听到“叮”一声,仪表胎压监测灯熄灭,表示设定成功,如此循环。

  • 月子里如何正确开窗通风(月子里正确开窗通风的方法)

    以下内容希望对你有帮助!月子里如何正确开窗通风每天早晚开窗通风半个小时,能有效减少空气中的细菌和病毒!况且月子里的小宝贝更需要呼吸新鲜的空气。需要开窗通风时,让产妇和宝宝到另外一个房间去,不要直接对着风吹。做好保暖工作,是不会得“月子病”的!