记一次 MySQL timestamp 精度问题的排查 → 过程有点曲折
开心一刻下午正准备出门,跟正刷着手机的老妈打个招呼
我:妈,今晚我跟朋友在外面吃,就不在家吃了
老妈拿着手机跟我说道:你看这叫朋友骗缅北去了,tm血都抽干了,多危险
我:那是他不行,你看要是吴京去了指定能跑回来
老妈:还吴京八经的,特么牛魔王去了都得耕地,唐三藏去了都得打出舍利,孙悟空去了都得演大马戏
我:那照你这么说,唐僧师徒取经走差地方了呗
老妈:那可没走错,他当年搁西安出发,他要是搁云南出发呀,上午到缅北,下午他就到西天
我:哈哈哈,那西游记就两级呗,那要是超人去了呢?
老妈:那超人去了,回来光剩超,人留那了
https://img2024.cnblogs.com/blog/747662/202401/747662-20240114154931804-134475538.gif
问题复现
我简化下业务与项目
数据库: MySQL 8.0.25
基于 spring-boot 2.2.10.RELEASE 搭建 demo :spring-boot-jpa-demo
表: tbl_user
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111102455652-2065649982.png
测试代码:
https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gifhttps://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif/**
* @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());
}
}
}View Codehttps://img2024.cnblogs.com/blog/747662/202401/747662-20240111105646296-1318490475.png
这么清晰的代码,大家都能看懂吧?
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111105838036-1800078710.png
我们来看下日志输出
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111110011613-107798170.png
保存的时候, lastModifiedTime 的值是 2024-01-11 09:33:26.643 ,从数据库查询得到的却是: 2024-01-11 09:33:27.0
是不是被震惊到了?
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111110811916-577272083.jpg
曲折排查
先确认下 MySQL 表中存的值是多少
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111132948947-1973951809.png
数据库表中的值就是 2024-01-11 09:33:27 ,此刻我只想来一句:卧槽!
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111133229048-954615283.jpg
这说明数据入库有问题,而不是读取有问题
我们来梳理下数据入库经历了哪些环节
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111140539189-799071248.png
那问题肯定出在 Spring Data JPA 至 mysql-connector-java 之间
MySQL 肯定是没问题的!
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111145934078-2041713775.gif
源码跟踪
既然问题出在 Spring Data JPA 与 mysql-connector-java 之间,那么我们就直接来个一穿到底,翻了它的源码老底
大家请坐好,我要开始装逼了
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111151354075-164758143.jpg
JPA 用的少,一时还不知道从哪里开始去跟源码,但不要慌,楼主有 葵花宝典 :杂谈篇之我是怎么读源码的,授人以渔
断点追踪源码,一时用一时爽,一直用一直爽
直接在 userRepository.save(user) 前面打个断点,然后一步一步往下跟,我就不细跟了,我只在容易跟丢的地方指出来,给你们合适的方向
当断点到 SessionImpl#firePersist 方法时
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111182318602-375379666.png
我们应该去跟 PersistEventListener::onPersist 了,一路跟下去,会来到 AbstractSaveEventListener#performSaveOrReplicate 方法
里面有如下代码
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111183009012-1507258612.png
添加的 Action 的实际类型是: EntityIdentityInsertAction
这里涉及到了 hibernate 的 事件机制 ,简单来说就是 EntityIdentityInsertAction 的 execute 方法会被调用
所以我们继续从 EntityIdentityInsertAction#execute 跟,会来到 GetGeneratedKeysDelegate#executeAndExtract
重点来了,大家打起精神
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111184955240-1918735070.png
继续跟进 session.getJdbcCoordinator().getResultSetReturn().executeUpdate( insert ) 的 executeUpdate
它长这样
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111185414134-1571747717.png
如果不是断点跟的话
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111185543586-2136142700.png
你知道接下来跟谁吗?
当然,非常熟悉源码的人(比如我),肯定知道跟谁
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111192328116-1748017453.png
但是用了断点,大家都知道跟谁了
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111185905347-1500820231.png
继续往下跟,当我们来到 ClientPreparedStatement#executeInternal 时,真相已经揭晓
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111190745395-313356963.png
此时已经来到了 mysql-connector-java ,发送给 MySQL Server 的 SQL 是:
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111191552510-886253908.png
last_modified_time 精度没丢!
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111192714690-275443419.jpg
那问题出在哪?
还能出在哪, MySQL 呗!
说好的 MySQL 没问题的了?
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111192742427-1194458357.png
MySQL 时间精度
用排除法,排的只剩 MySQL 了,直接执行 SQL 试试
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111193835972-1474633204.png
哦豁,敢情前面的源码分析全白分析了,我此刻的心情你们懂吗
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111194402301-1215173433.png
这必须得找 MySQL 要个说法,真是太狗了
我们去 MySQL 官方文档找找看(注意参考手册版本要和我们使用的 MySQL 版本一致)
大家不要通篇去读,那样太费时间,直接 search 用起来
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111195201169-1691883045.png
The DATE, DATETIME, and TIMESTAMP Types 有这么一段比较关键
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111195402171-282822246.png
我给大家翻译一下
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111201157513-394685298.png
继续看 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 值到相同类型的列时,如果值的小数位与精度不匹配时,会进行四舍五入
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111203607342-1673809766.png
四舍五入的判断位置是精度的后一位,比如精度是 0,则看值的第 1 位小数,来决定是舍还是入,如果精度是 2,则看值的第 3 位小数
简单来说:值的精度大于列类型的精度,就会存在四舍五入,否则值是多少就存多少
当发生四舍五入时,既不会告警也不会报错,因为这就是 SQL 规范
那如果我不像要四舍五入了,有没有什么办法?
MySQL 也给出了支持,就是启用 SQL mode :TIME_TRUNCATE_FRACTIONAL
启用之后,当值的精度大于列类型的精度时,就是直接按列类型的精度截取,而不是四舍五入
那这么看下来,不是 MySQL 的锅呀, MySQL 表示这锅我不背
https://img2024.cnblogs.com/blog/747662/202401/747662-20240113121900183-1772133459.jpg
那是谁的锅?
只能说是开发人员的锅,为什么不按 MySQL 使用说明书使用?
我要强调的是,产生这次问题的代码不是我写的,我写的代码怎么可能有 bug
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111205023380-949215707.jpg
总结
1、 源码 debug 堆栈
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111192147077-1799836780.png
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111192037287-1904081976.png
2、MySQL 时间精度
MySQL 的 TIME , DATETIME 和 TIMESTAMP 类型都支持微妙级别(6位数)的精度
默认情况下会四舍五入,若想直接截断,则需要开启 SQL mode : TIME_TRUNCATE_FRACTIONAL
3、规范
阿里巴巴的开发手册中明确指出不能用: java.sql.Timestamp
https://img2024.cnblogs.com/blog/747662/202401/747662-20240111205521529-1159852735.png
另外很多公司的 MySQL 开发规范会强调:没有特殊要求,时间类型用 datetime
主要出于两点考虑:1、 datetime 可用于分区,而 timestamp 不行,2、 timestamp 的范围只到 2038-01-19 03:14:07.499999
有的开发小伙伴可能会问:如果到了 2038-01-19 03:14:07.499999 之后, timestamp 该怎么办?
我只能说:小伙子你想的太远了, 2038 跟我们有什么关系,影响我们送外卖吗?
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]