记一次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');
但是查询数据库中生日字段时, 日期却变成了

问题已复现, 先列配置:
数据库链接配置: 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')
数据库中的生日也正确了, 搞完收工!

问题发现2
就在我认为改完代码, 以后不会再出现任何问题的时候, 问题就又tmd来了, 依然是相同的问题, 需要我尽快修复
问题排查2
业务流程没有进行变更, 原有问题已经修复, 暂无头绪的我按照使用流程尝试复现.
最终发现程序先selectById(xxx)查询实体数据, 在后面updateById(person)更新数据, 看来问题就出现在这里了, 继续debug
查询后生日的格式为

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`不就万无一失了么.
开搞!
-
先更新数据库字段的类型 -
date--->datetime -
录入时获取生日的代码也进行调整
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")的含义是否都是中国时区
回答:
- 偏移量 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, 在次更新后, 终于解决了问题.
问题总结
- 数据库只保存日期时, java中更推荐以
LocalDate类来保存数据 - 如果使用了
Date来保存数据, 则日期转换工具设置的时区应该与jdbc连接时配置的时区保持一致

浙公网安备 33010602011771号