开心一刻
下午正准备出门,跟正刷着手机的老妈打个招呼
我:妈,今晚我跟朋友在外面吃,就不在家吃了
老妈拿着手机跟我说道:你看这叫朋友骗缅北去了,tm血都抽干了,多危险
我:那是他不行,你看要是吴京去了指定能跑回来
老妈:还吴京八经的,特么牛魔王去了都得耕地,唐三藏去了都得打出舍利,孙悟空去了都得演大马戏
我:那照你这么说,唐僧师徒取经走差地方了呗
老妈:那可没走错,他当年搁西安出发,他要是搁云南出发呀,上午到缅北,下午他就到西天
我:哈哈哈,那西游记就两级呗,那要是超人去了呢?
老妈:那超人去了,回来光剩超,人留那了
问题复现
我简化下业务与项目
数据库:MySQL 8.0.25
基于spring-boot 2.2.10.RELEASE搭建demo :spring-boot-jpa-demo
表:tbl_user
/**
* @description: xxx描述
* @author: 青石路
* @date: 2024/1/9 21:42
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class UserTest {
@Resource
private UserRepository userRepository;
@Test
public void get() {
DateTimeFormatter dft = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
Timestamp lastModifiedTime = Timestamp.valueOf(LocalDateTime.parse("2024-01-11 09:33:26.643", dft));
// 1.先保存一个user
User user = new User();
user.setUserName("zhangsan");
user.setPassword("zhangsan");
user.setBirthday(LocalDate.now().minusYears(25));
user.setLastModifiedTime(lastModifiedTime);
log.info("user.lastModifiedTime = {}", user.getLastModifiedTime());
userRepository.save(user);
log.info("user 保存成功,userId = {}", user.getUserId());
// 2.然后再根据id查询这个user
Optional<User> userOptional = userRepository.findById(user.getUserId());
if (userOptional.isPresent()) {
log.info("从数据库查询到的user,user.lastModifiedTime = {}", userOptional.get().getLastModifiedTime());
}
}
}
就分2步:1、先保存User,2、根据保存返回的userId再去数据库查User
这么清晰的代码,大家都能看懂吧?
大家仔细看,保存的时候,lastModifiedTime的值是2024-01-11 09:33:26.643,从数据库查询得到的却是:2024-01-11 09:33:27.0
是不是有点懵,是不是很震惊?
曲折排查
先确认下MySQL表中存的值是多少
数据库表中的值竟然是2024-01-11 09:33:27,而不是2024-01-11 09:33:26.643
此刻我只想来一句:卧槽!
那么接下来,我们梳理下数据入库经历了哪些环节
大家应该能看懂吧,环节应该没有漏吧?
那问题肯定出在Spring Data JPA至mysql-connector-java之间,你说是不是?
MySQL肯定是没问题的!
源码跟踪
既然问题出在Spring Data JPA与mysql-connector-java之间,那么我们就直接来个一穿到底,翻了它的源码老底
大家请坐好,我要开始装逼了
JPA用的少,一时还不知道从哪里开始去跟源码,但不要慌,楼主有葵花宝典:杂谈篇之我是怎么读源码的,授人以渔
断点追踪源码,一时用一时爽,一直用一直爽
直接在userRepository.save(user)前面打个断点,然后一步一步往下跟,我就不细跟了,我只在容易跟丢的地方指出来,给你们合适的方向
当断点到SessionImpl#firePersist方法时
我们应该去跟PersistEventListener::onPersist了
一路跟下去,会来到AbstractSaveEventListener#performSaveOrReplicate方法
里面有如下代码
添加的Action的实际类型是: EntityIdentityInsertAction
这里涉及到了Hibernate的事件机制 ,简单来说就是EntityIdentityInsertAction的execute方法会被调用
所以我们继续从EntityIdentityInsertAction#execute跟
会来到GetGeneratedKeysDelegate#executeAndExtract
重点来了,大家打起精神
继续跟进session.getJdbcCoordinator().getResultSetReturn().executeUpdate( insert )的executeUpdate
它长这样
面对statement.executeUpdate,如果不是断点跟的话,你可能就迷失在源码的海洋了
你知道接下来跟谁吗?
当然,非常熟悉源码的人(比如我),肯定知道跟谁
但是用了断点,大家都知道接下来跟谁了
继续往下跟,当我们来到ClientPreparedStatement#executeInternal时,真相已经揭晓
此时已经来到了mysql-connector-java,发送给MySQL Server的SQL是:
大家看仔细了,last_modified_time对应的值是:2024-01-11 09:33:26.643,精度没丢
是不是很意外?
那问题出在哪?
还能出在哪,MySQL呗!
说好的MySQL没问题的了?
MySQL 时间精度
用排除法,排的只剩MySQL了,直接执行SQL试试
哦豁,敢情前面的源码分析全白分析了,我此刻的心情你们懂吗
这必须得找MySQL要个说法,真是太狗了
我们去MySQL官方文档找找看(注意参考手册版本要和我们使用的MySQL版本一致,别搞鸡同鸭讲的乌龙)
大家不要通篇去读,那样太费时间,直接search用起来
The DATE, DATETIME, and TIMESTAMP Types 有这么一段比较关键
我给大家翻译一下
继续看 Fractional Seconds in Time Values,内容不多,大家可以通篇读完
MySQL的TIME,DATETIME和TIMESTAMP都支持微妙级别(6位数)的小数位
精度直接在括号中指定,例如:CREATE TABLE t1 (t TIME(3), dt DATETIME(6))
小数位的范围是 0 到 6。0 表示没有小数部分,如果小数位缺省,则默认是0(SQL规范规定的默认是 6,MySQL8 默认值取 0 是为了兼容 MySQL 以前的版本)
当插入带有小数部分的TIME,DATETIME或TIMESTAMP值到相同类型的列时,如果值的小数位与精度不匹配时,会进行四舍五入
四舍五入的判断位置是精度的后一位,比如精度是0,则看值的第1位小数,来决定是舍还是入,如果精度是2,则看值的第3位小数
简单来说:值的精度大于列类型的精度,就会存在四舍五入,否则值是多少就存多少
当发生四舍五入时,既不会告警也不会报错,因为这就是 SQL 规范
那如果我不想要四舍五入了,有没有什么办法?
MySQL也给出了支持,就是启用SQL mode:TIME_TRUNCATE_FRACTIONAL
启用之后,当值的精度大于列类型的精度时,就是直接按列类型的精度截取,而不是四舍五入
那这么看下来,不是MySQL的锅呀,MySQL表示这锅我不背
那是谁的锅?
只能说是开发人员的锅,为什么不按MySQL使用说明书使用?
我要强调的是,产生这次问题的代码不是我写的,我写的代码怎么可能有bug
总结
源码debug堆栈
MySQL 时间精度
MySQL的TIME,DATETIME和TIMESTAMP类型都支持微妙级别(6位数)的精度,默认情况下会四舍五入,若想直接截断,则需要开启SQL mode:TIME_TRUNCATE_FRACTIONAL
开发规范
阿里巴巴的开发手册中明确指出不能用:java.sql.Timestamp
另外很多公司的MySQL开发规范会强调:没有特殊要求,时间类型用 datetime
主要出于两点考虑:
1、datetime可用于分区,而timestamp不行
2、timestamp的范围只到2038-01-19 03:14:07.499999
有的开发小伙伴可能会问:如果到了2038-01-19 03:14:07.499999之后,timestamp该怎么办?
我只能说:小伙子你想的太远了,2038跟我们有什么关系,影响我们送外卖吗?
补充
关于上面讲到的timestamp不能分区,进行一下补充
它能分区,但是和DATE和DATETIME有一丢丢区别
MySQL 5.7 说明如下
MySQL 8.0 说明如下
timestamp类型的列只能基于UNIX_TIMESTAMP函数进行分区