记一次mysql5.7.x 版本 数据库字段类型为date的日期转换的bug

业务场景描述

系统人员表需要录入个人的身份证号, 根据身份证号来解析性别, 生日. 合同签约时, 读取人员的生日字段作为合同的内容.

生日字段在Java代码中的类型为java.util.Date; 在数据库中生日字段的类型为date.

问题发现

一位1990年的用户签约时, 发现合同中展示的生日比自己实际的生日少一天, 寻求我们解决问题以便正常签约.

问题排查

使用单元测试排查问题时, mybatisplus 打印待执行的sql为

-- 省略其余关键字
INSERT INTO odd_job ( birthday ) VALUES ('1990-06-18 00:00:00');

但是查询数据库中生日字段时, 日期却变成了
image

问题已复现, 先列配置:

数据库链接配置: serverTimezone=GMT%2B8

初步排查认为 jdbc进行转换的时候将日期是别为了UTC格式, 而 MySQL 又以 system_time_zone=+08:00 再转一次,结果 1990-06-18 00:00:00 UTC → 1990-06-17 16:00:00 CST,最后日期部分被截成 1990-06-17。

因此解决方案就定为了 修改日期的格式, 主动将日期格式转换为UTC

// 旧: 使用hutool工具包中的工具类解析
Date birthDate = IdcardUtil.getBirthDate(birthDateStr);

// 新: 
Date birthDate = Date.from(
    LocalDateTimeUtil.parseDate(IdcardUtil.getBirthByIdCard(birthDateStr), "yyyyMMdd").atStartOfDay(ZoneOffset.UTC).toInstant())

改完之后, 立竿见影, 打印的sql发生了变化

INSERT INTO odd_job (birthday) VALUES ('1990-06-18 09:00:00')

数据库中的生日也正确了, 搞完收工!
image

问题发现2

就在我认为改完代码, 以后不会再出现任何问题的时候, 问题就又tmd来了, 依然是相同的问题, 需要我尽快修复

问题排查2

业务流程没有进行变更, 原有问题已经修复, 暂无头绪的我按照使用流程尝试复现.

最终发现程序先selectById(xxx)查询实体数据, 在后面updateById(person)更新数据, 看来问题就出现在这里了, 继续debug

查询后生日的格式为
image

mybatisplus 待执行的sql为

UPDATE odd_job SET birthday = '1990-06-18 00:00:00' WHERE id = 2014627413589270529

得了, 这下看懂了, 又是因为system_time_zone=+08:00这个配置, 日期又进行了转换, 使得生日又少了一天;

直接改update语句不是一个一劳永逸的方法, 我得想想其他办法, 避免后续写更新语句的时候, 出现相同的问题

既然 mysql中的date类型会存在截取日期字符串的问题, 那我把字段类型改成``datetime`不就万无一失了么.

开搞!

  1. 先更新数据库字段的类型 - date ---> datetime

  2. 录入时获取生日的代码也进行调整

    Date birthDate = Date.from(
        LocalDateTimeUtil.parseDate(IdcardUtil.getBirthByIdCard(birthDateStr), "yyyyMMdd").atStartOfDay(ZoneId.of("Asia/Shanghai")).toInstant())
    

修改完进行测试的时候, 发现在新增时执行的sql依然不是00:00:00, 这是为什么呢?

INSERT INTO odd_job (birthday) VALUES ('1990-06-18 01:00:00')

在咨询我的ai大哥后, 终于知道了问题出在哪里

提问: 数据库GMT+8 与 ZoneId.of("Asia/Shanghai")的含义是否都是中国时区

回答:

  1. 偏移量 vs 时区规则
    • GMT+8 / +08:00 只是固定偏移量,永远比 UTC 快 8 小时,没有历史,也没有未来
    • ZoneId.of("Asia/Shanghai") 背后是一份完整规则表(IANA tzdb),里面记录了:
      – 1901–1928 用 +08:00
      – 1935–1961 曾切到 +09:00
      – 1974–1975 依旧是 +09:00
      – 1979 依旧是 +09:00
      – 1986–1991 夏令时期间又变成 +09:00
      – 1992 年至今回到 +08:00
      未来如果中国再调政策,只要升级 JDK 的 tzdb,规则会自动生效,而 GMT+8 永远不知道这些变化。

因此这位生日是1990-06-18的人员在保存数据时, 时间存储的是 UTC时间 +9小时, 而数据库连接配置为 GMT+8 固定为+8小时, 因此会出现查询语句时间格式为01:00:00的问题

经历上述问题后, 更新数据库连接配置 - serverTimezone=GMT%2B8 ---> serverTimezone=Asia%2FShanghai

因为将字段类型从date -> datetime时, 原始字段的值也发生了更新, 由yyyy-MM-dd 变成了yyyy-MM-dd 00:00:00, 同时因为默认时区为UTC, 因此需要进行数据更新.

然而又又又又又出了问题, 在执行下面的sql时, 发生了错误

UPDATE odd_job SET birthday = '1945-07-20 00:00:00' WHERE id = 2014627413589270529;
Caused by: java.lang.IllegalArgumentException: HOUR_OF_DAY: 0 -> 1
        at java.base/java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2790)
        at java.base/java.util.Calendar.updateTime(Calendar.java:3411)
        at java.base/java.util.Calendar.getTimeInMillis(Calendar.java:1805)
        at com.mysql.cj.result.SqlTimestampValueFactory.localCreateFromDatetime(SqlTimestampValueFactory.java:192)
        ... 157 common frames omitted

依旧是ai哥来回答

这个错误是由于 夏令时(Daylight Saving Time) 导致的。1945年7月20日在中国历史上正处于夏令时期间,导致 00:00:00 这个时间不存在(被跳过了)。


中国在1945年确实实行过夏令时:

  • 1945年夏令时:从4月15日开始,到9月30日结束
  • 在夏令时生效期间,时钟会向前拨1小时,因此 00:00:00 这个时间点实际上不存在,直接从 00:00 跳到了 01:00

当你尝试设置 1945-07-20 00:00:00 时,JDBC 驱动在将本地时间(Asia/Shanghai)转换为 UTC 时,发现这个时间无效,因此报错 HOUR_OF_DAY: 0 -> 1

最后的最后, 将字段类型还原为date, 在次更新后, 终于解决了问题.

问题总结

  1. 数据库只保存日期时, java中更推荐以LocalDate类来保存数据
  2. 如果使用了Date来保存数据, 则日期转换工具设置的时区应该与jdbc连接时配置的时区保持一致
posted @ 2026-01-26 16:33  浅唱z2  阅读(1)  评论(0)    收藏  举报