项目中遇到存储的时间戳与真实时间相差14小时的现象,以下为解决步骤.

问题

CREATE TABLE `incident` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`recovery_time` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4;

以上为数据库建表语句,其中created_time是插入记录时自动设置,recovery_time需要手动进行设置.

测试时发现,created_time为正确的北京时间,然而recovery_time则与设置时间相差14小时.

尝试措施

jvm时区设置

//设置jvm默认时间
System.setProperty("user.timezone", "UTC");

数据库时区查询

查看数据库时区设置:

show variables like '%time_zone%';
--- 查询结果如下所示:
--- system_time_zone: CST
--- time_zone:SYSTEM

查询CST发现其指代比较混乱,有四种含义(参考网址:https://juejin.im/post/5902e087da2f60005df05c3d):

  • 美国中部时间 Central Standard Time (USA) UTC-06:00
  • 澳大利亚中部时间 Central Standard Time (Australia) UTC+09:30
  • 中国标准时 China Standard Time UTC+08:00
  • 古巴标准时 Cuba Standard Time UTC-04:00

此处发现如果按照美国中部时间进行推算,相差14小时,与Bug吻合.

验证过程

MyBatis转换

代码中,时间戳使用Instant进行存储,因此跟踪package org.apache.ibatis.type下的InstantTypeHandler.

@UsesJava8
public class InstantTypeHandler extends BaseTypeHandler<Instant> { @Override
public void setNonNullParameter(PreparedStatement ps, int i, Instant parameter, JdbcType jdbcType) throws SQLException {
ps.setTimestamp(i, Timestamp.from(parameter));
} //...代码shenglve
}

调试时发现parameter为正确的UTC时.

函数中调用Timestamp.fromInstant转换为Timestamp实例,检查无误.

    /**
* Sets the designated parameter to the given <code>java.sql.Timestamp</code> value.
* The driver
* converts this to an SQL <code>TIMESTAMP</code> value when it sends it to the
* database.
*
* @param parameterIndex the first parameter is 1, the second is 2, ...
* @param x the parameter value
* @exception SQLException if parameterIndex does not correspond to a parameter
* marker in the SQL statement; if a database access error occurs or
* this method is called on a closed <code>PreparedStatement</code> */
void setTimestamp(int parameterIndex, java.sql.Timestamp x)
throws SQLException;

继续跟踪setTimestamp接口,其具体解释见代码注释.

Sql Driver转换

项目使用com.mysql.cj.jdbc驱动,跟踪其setTimestampClientPreparedStatement类下的具体实现(PreparedStatementWrapper类下实现未进入).

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

继续跟踪上端代码中的getQueryBindings().setTimestamp()实现(com.mysql.cj.ClientPreparedQueryBindings).

    @Override
public void setTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength) {
if (x == null) {
setNull(parameterIndex);
} else { x = (Timestamp) x.clone(); if (!this.session.getServerSession().getCapabilities().serverSupportsFracSecs()
|| !this.sendFractionalSeconds.getValue() && fractionalLength == 0) {
x = TimeUtil.truncateFractionalSeconds(x);
} if (fractionalLength < 0) {
// default to 6 fractional positions
fractionalLength = 6;
} x = TimeUtil.adjustTimestampNanosPrecision(x, fractionalLength, !this.session.getServerSession().isServerTruncatesFracSecs()); //注意此处时区转换
this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", targetCalendar,
targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone()); StringBuffer buf = new StringBuffer();
buf.append(this.tsdf.format(x));
if (this.session.getServerSession().getCapabilities().serverSupportsFracSecs()) {
buf.append('.');
buf.append(TimeUtil.formatNanos(x.getNanos(), 6));
}
buf.append('\''); setValue(parameterIndex, buf.toString(), MysqlType.TIMESTAMP);
}
}

注意此处时区转换,会调用如下语句获取默认时区:

this.session.getServerSession().getDefaultTimeZone()

获取TimeZone数据,具体如下图所示:

检查TimeZone类中offset含义,具体如下所示:

    /**
* Gets the time zone offset, for current date, modified in case of
* daylight savings. This is the offset to add to UTC to get local time.
* <p>
* This method returns a historically correct offset if an
* underlying <code>TimeZone</code> implementation subclass
* supports historical Daylight Saving Time schedule and GMT
* offset changes.
*
* @param era the era of the given date.
* @param year the year in the given date.
* @param month the month in the given date.
* Month is 0-based. e.g., 0 for January.
* @param day the day-in-month of the given date.
* @param dayOfWeek the day-of-week of the given date.
* @param milliseconds the milliseconds in day in <em>standard</em>
* local time.
*
* @return the offset in milliseconds to add to GMT to get local time.
*
* @see Calendar#ZONE_OFFSET
* @see Calendar#DST_OFFSET
*/
public abstract int getOffset(int era, int year, int month, int day,
int dayOfWeek, int milliseconds);

offset表示本地时间UTC时的时间间隔(ms).

计算数值offset,发现其表示美国中部时间,即UTC-06:00.

  • Driver推断Session时区为UTC-6;
  • DriverTimestamp转换为UTC-6String;
  • MySql认为Session时区在UTC+8,将String转换为UTC+8.

因此,最终结果相差14小时,bug源头找到.

解决方案

参照https://juejin.im/post/5902e087da2f60005df05c3d.

mysql> set global time_zone = '+08:00';
Query OK, 0 rows affected (0.00 sec) mysql> set time_zone = '+08:00';
Query OK, 0 rows affected (0.00 sec)

告知运维设置时区,重启MySql服务,问题解决.

此外,作为防御措施,可以在jdbc url中设置时区(如此设置可以不用修改MySql配置):

jdbc:mysql://localhost:3306/table_name?useTimezone=true&serverTimezone=GMT%2B8

此时,就告知连接进行时区转换,并且时区为UTC+8.

PS:

如果您觉得我的文章对您有帮助,请关注我的微信公众号,谢谢!

SpringBoot时间戳与MySql数据库记录相差14小时排错的更多相关文章

  1. mysql数据库优化课程---14、常用的sql技巧

    mysql数据库优化课程---14.常用的sql技巧 一.总结 一句话总结:其实就是sql中那些函数的使用 1.mysql中函数如何使用? 选择字段 其实就是作用域select的选择字段 3.转大写: ...

  2. 【入门】Spring-Boot项目配置Mysql数据库

    前言 前面参照SpringBoot官网,自动生成了简单项目点击打开链接 配置数据库和代码遇到的问题 问题1:cannot load driver class :com.mysql.jdbc.Drive ...

  3. 通用mapper版+SpringBoot+MyBatis框架+mysql数据库的整合

    转:https://blog.csdn.net/qq_35153200/article/details/79538440 开发环境: 开发工具:Intellij IDEA 2017.2.3 JDK : ...

  4. springboot 时间戳和 数据库时间相差14个小时

    在 springboot 开发过程中遇到一个奇怪的问题,就是已经设置系统时间GMT+8, 但是时间到数据库后会减少14个小时.后来发现是 jvm 时区和数据库时区设置不一致的问题. jvm 设置的是 ...

  5. mysql数据库记录

    ON DELETE restrict(约束):当在父表(即外键的来源表)中删除对应记录时,首先检查该记录是否有对应外键,如果有则不允许删除. no action:意思同restrict.即如果存在从数 ...

  6. [PHP]全国省市区信息,mysql数据库记录

    下载地址: https://files.cnblogs.com/files/wukong1688/T_Area.zip 或者也可以复制如下内容: CREATE TABLE IF NOT EXISTS ...

  7. mysql 时区问题导致的时间相差14小时

    1.mysql 字段名称 类型 begin_time TIME begin_time=08:18:39 2.java数据库连接串 jdbc:mysql://x.x.x.x:3306/y?useUnic ...

  8. MySQL数据库---记录相关操作

    序 表中记录的相关操作一共四种:插入,更新,删除.查询.其中使用最多,也是最难的就是查询. 记录的插入 1. 插入完整数据(顺序插入) 语法一: INSERT INTO 表名(字段1,字段2,字段3… ...

  9. springboot后端时间到前端,相差8小时,时间格式不对

    spring boot后台时间正确,返回给前台的时间不正确,和后台差8个小时 { "code": 1, "msg": "SUCCESS", ...

随机推荐

  1. Build 2019 彩蛋

    N久没写过博客了… 最近在玩 APEX 但是手残党表示打到15级了,至今杀敌 4 人… 当快递员是越来越顺手了… 今年巨硬的 Build 大会会在 5 月 6-8 号召开 新发布的 Hololens ...

  2. 【JAVA】反射总结

    反射是什么? 反射就是指程序在运行的时候可以知道一个类的自身信息. 自身信息有哪些:类方法.属性.成员变量.构造方法.包等 动态编译和静态编译  静态编译:在编译的时候进确定类型,如果绑定对象成功,n ...

  3. C++ 面向对象的三大特性和五个原则

    1.三大特性: 封装:就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏.一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体.在一个对象内 ...

  4. 如何在.Net Core调用NodeJs

    概述 Microsoft.AspNetCore.NodeServices库 实例 新建aspnet core站点 添加nuget包 建立node环境,此处示例用于扫描wifi环境 建立nodejs的程 ...

  5. 强制.net 程序以32位模式运行

    You can modify the PE header of the executable (using the "Corflags.exe" .NET Framework SD ...

  6. notes for lxf(四)

    类名首字母通常大写 创建实例 类名 +() __init__方法 创建实例时把一些属性绑上去 __init__方法第一参数永远是self 表示船舰的实例本身 类是实例的模板 实例是一个一个具体的对象 ...

  7. 向mysql中导入向导时如表xlsx

    如果出现这种问题那么是因为没有打开这个文件,如果想导入这个文件需要到开这个文件,然后再导入

  8. sublime2 nodejs 执行编译无反应

    这个问题困扰了我得一周了,好不容易解决了, 一.问题描述: 安装网上的一些教程在sublime text 2 里面安装Nodejs 的编译环境,但是安装完之后执行编译没有任何输出信息,编译没有反应,只 ...

  9. scrapy递归解析和post请求

    递归解析 递归爬取解析多页页面数据 每一个页面对应一个url,则scrapy工程需要对每一个页码对应的url依次发起请求,然后通过对应的解析方法进行作者和段子内容的解析. 实现方案: 1.将每一个页码 ...

  10. 简繁体转化处理 opencc 安装【centos 7】

    代码 #准备工作 yum install cmake yum install git #下载代码 git clone https://github.com/BYVoid/OpenCC #安装文档生成 ...