暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

SpringBoot时间戳与MySql数据库记录相差14小时排错

程序员打怪之路 2021-05-19
856

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

问题

  1. CREATE TABLE `incident` (

  2. `id` int(11) NOT NULL AUTO_INCREMENT,

  3. `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,

  4. `recovery_time` timestamp NULL DEFAULT NULL,

  5. PRIMARY KEY (`id`)

  6. ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4;

以上为数据库建表语句,其中 created_time
是插入记录时自动设置, recovery_time
需要手动进行设置. 测试时发现, created_time
为正确的北京时间,然而 recovery_time
则与设置时间相差14小时.

尝试措施

jvm时区设置

  1. //设置jvm默认时间

  2. System.setProperty("user.timezone", "UTC");

数据库时区查询

查看数据库时区设置:

  1. show variables like '%time_zone%';

  2. --- 查询结果如下所示:

  3. --- system_time_zone: CST

  4. --- 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
进行存储,因此跟踪 packageorg.apache.ibatis.type
下的 InstantTypeHandler
.

  1. @UsesJava8

  2. public class InstantTypeHandler extends BaseTypeHandler<Instant> {


  3. @Override

  4. public void setNonNullParameter(PreparedStatement ps, int i, Instant parameter, JdbcType jdbcType) throws SQLException {

  5. ps.setTimestamp(i, Timestamp.from(parameter));

  6. }


  7. //...代码shenglve

  8. }

调试时发现 parameter
为正确的 UTC
时. 函数中调用 Timestamp.from
Instant
转换为 Timestamp
实例,检查无误.

  1. /**

  2. * Sets the designated parameter to the given <code>java.sql.Timestamp</code> value.

  3. * The driver

  4. * converts this to an SQL <code>TIMESTAMP</code> value when it sends it to the

  5. * database.

  6. *

  7. * @param parameterIndex the first parameter is 1, the second is 2, ...

  8. * @param x the parameter value

  9. * @exception SQLException if parameterIndex does not correspond to a parameter

  10. * marker in the SQL statement; if a database access error occurs or

  11. * this method is called on a closed <code>PreparedStatement</code> */

  12. void setTimestamp(int parameterIndex, java.sql.Timestamp x)

  13. throws SQLException;

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

Sql Driver转换

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

  1. @Override

  2. public void setTimestamp(int parameterIndex, Timestamp x) throws java.sql.SQLException {

  3. synchronized (checkClosed().getConnectionMutex()) {

  4. ((PreparedQuery<?>) this.query).getQueryBindings().setTimestamp(getCoreParameterIndex(parameterIndex), x);

  5. }

  6. }

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

  1. @Override

  2. public void setTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength) {

  3. if (x == null) {

  4. setNull(parameterIndex);

  5. } else {


  6. x = (Timestamp) x.clone();


  7. if (!this.session.getServerSession().getCapabilities().serverSupportsFracSecs()

  8. || !this.sendFractionalSeconds.getValue() && fractionalLength == 0) {

  9. x = TimeUtil.truncateFractionalSeconds(x);

  10. }


  11. if (fractionalLength < 0) {

  12. // default to 6 fractional positions

  13. fractionalLength = 6;

  14. }


  15. x = TimeUtil.adjustTimestampNanosPrecision(x, fractionalLength, !this.session.getServerSession().isServerTruncatesFracSecs());


  16. //注意此处时区转换

  17. this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", targetCalendar,

  18. targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone());


  19. StringBuffer buf = new StringBuffer();

  20. buf.append(this.tsdf.format(x));

  21. if (this.session.getServerSession().getCapabilities().serverSupportsFracSecs()) {

  22. buf.append('.');

  23. buf.append(TimeUtil.formatNanos(x.getNanos(), 6));

  24. }

  25. buf.append('\'');


  26. setValue(parameterIndex, buf.toString(), MysqlType.TIMESTAMP);

  27. }

  28. }

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

  1. this.session.getServerSession().getDefaultTimeZone()

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

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

  1. /**

  2. * Gets the time zone offset, for current date, modified in case of

  3. * daylight savings. This is the offset to add to UTC to get local time.

  4. * <p>

  5. * This method returns a historically correct offset if an

  6. * underlying <code>TimeZone</code> implementation subclass

  7. * supports historical Daylight Saving Time schedule and GMT

  8. * offset changes.

  9. *

  10. * @param era the era of the given date.

  11. * @param year the year in the given date.

  12. * @param month the month in the given date.

  13. * Month is 0-based. e.g., 0 for January.

  14. * @param day the day-in-month of the given date.

  15. * @param dayOfWeek the day-of-week of the given date.

  16. * @param milliseconds the milliseconds in day in <em>standard</em>

  17. * local time.

  18. *

  19. * @return the offset in milliseconds to add to GMT to get local time.

  20. *

  21. * @see Calendar#ZONE_OFFSET

  22. * @see Calendar#DST_OFFSET

  23. */

  24. public abstract int getOffset(int era, int year, int month, int day,

  25. int dayOfWeek, int milliseconds);

offset
表示 本地时间
UTC
时的 时间间隔(ms)
. 计算数值 offset
,发现其表示 美国中部时间
,即 UTC-06:00
.

  • Driver
    推断 Session
    时区为 UTC-6
    ;

  • Driver
    将 Timestamp
    转换为 UTC-6
    的 String
    ;

  • MySql
    认为 Session
    时区在 UTC+8
    ,将 String
    转换为 UTC+8
    .

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

解决方案

解决方案当然是熟练的告知运维,修改 mysql
的时区即可.

  1. mysql> set global time_zone = '+08:00';

  2. Query OK, 0 rows affected (0.00 sec)


  3. mysql> set time_zone = '+08:00';

  4. Query OK, 0 rows affected (0.00 sec)

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

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

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

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


文章转载自程序员打怪之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论