记一次 MySQL timestamp 精度问题的排查 → 过程有点曲折
开心一刻
下午正准备出门,跟正刷着手机的老妈打个招呼
我:妈,今晚我跟朋友在外面吃,就不在家吃了
老妈拿着手机跟我说道:你看这叫朋友骗缅北去了,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());
}
}
}

这么清晰的代码,大家都能看懂吧?

我们来看下日志输出

保存的时候, lastModifiedTime 的值是 2024-01-11 09:33:26.643 ,从数据库查询得到的却是: 2024-01-11 09:33:27.0
是不是被震惊到了?

曲折排查
先确认下 MySQL 表中存的值是多少

数据库表中的值就是 2024-01-11 09:33:27 ,此刻我只想来一句:卧槽!

这说明数据入库有问题,而不是读取有问题
我们来梳理下数据入库经历了哪些环节

那问题肯定出在 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
它长这样

如果不是断点跟的话

你知道接下来跟谁吗?
当然,非常熟悉源码的人(比如我),肯定知道跟谁

但是用了断点,大家都知道跟谁了

继续往下跟,当我们来到 ClientPreparedStatement#executeInternal 时,真相已经揭晓

此时已经来到了 mysql-connector-java ,发送给 MySQL Server 的 SQL 是:

last_modified_time 精度没丢!

那问题出在哪?
还能出在哪, 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

总结
1、 源码 debug 堆栈


2、MySQL 时间精度
MySQL 的 TIME , DATETIME 和 TIMESTAMP 类型都支持微妙级别(6位数)的精度
默认情况下会四舍五入,若想直接截断,则需要开启 SQL mode : TIME_TRUNCATE_FRACTIONAL
3、规范
阿里巴巴的开发手册中明确指出不能用: 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 跟我们有什么关系,影响我们送外卖吗?
记一次 MySQL timestamp 精度问题的排查 → 过程有点曲折的更多相关文章
- 解Bug之路-记一次中间件导致的慢SQL排查过程
解Bug之路-记一次中间件导致的慢SQL排查过程 前言 最近发现线上出现一个奇葩的问题,这问题让笔者定位了好长时间,期间排查问题的过程还是挺有意思的,正好博客也好久不更新了,就以此为素材写出了本篇文章 ...
- 记一次 MySQL 主从同步异常的排查记录,百转千回
你好,我是悟空. 这是悟空的第 183 篇原创文章 官网:www.passjava.cn 本文主要内容如下: 一.现象 最近项目的测试环境遇到一个主备同步的问题: 备库的同步线程停止了,无法同步主库的 ...
- 记一次MySQL数据库拒绝访问的解决过程
问题背景 用wordpress搭博客,数据库采用MySQL.为了调试方便,创建账户my_account ,允许它从任意主机访问数据库. CREATE USER `my_account`@'%' IDE ...
- 一起MySQL时间戳精度引发的血案
写在前面 最近工作中遇到两例mysql时间戳相关的问题,一个是mysql-connector-java和msyql的精度不一致导致数据查不到:另一例是应用服务器时区错误导致数据查询不到.通过这篇文章, ...
- 记一次MySQL表分区操作
最近一次日常迭代中,业务线需要对一张大表进行联合查询,查询性能可想而知,测试过程中服务接口直接响应超时,导致服务不可用,最后临时对该表进行分区操作,暂时缓解性能问题.由于是第一次操作表分区,姑且记录一 ...
- mysql timestamp字段定义的
Cause: java.sql.SQLException: Cannot convert value '2017-07-26 20:40:41.000000' from column 10 to TI ...
- 记一次 mysql 启动没反应
记一次 mysql 启动没反应 ,重启linux又可以启动 vim /var/log/mysqld.log 2018-02-04 13:22:49 28507 [ERROR] InnoDB: Cann ...
- 记一次mysql数据库被勒索(中)
背景在上一篇文章里面已经提过了. 现在面临的问题是nextcloud没有mysql数据库,用不起来了. 因为文件没丢,一种方法是启动新的mysql数据库,把文件重新提交一次. 为了程序员的面子,没有选 ...
- 记一次mysql数据库被勒索(下)
背景: nextcloud的mysql数据库被黑,删库勒索.参考:记一次mysql数据库被勒索(上) mysql数据库恢复成功,nextcloud还是无法连接.参考:记一次mysql数据库被勒索(中) ...
- 记一次mysql事务未提交导致锁未释放的问题
记一次mysql事务未提交导致锁未释放的问题 ## 查看未提交的事务(3秒内未操作的事务) SELECT p.ID AS conn_id, P.USER AS login_user, P.HOST A ...
随机推荐
- android 尺寸适配相关
Android上常见度量单位 px(像素):屏幕上的点,绝对长度,与硬件相关. in(英寸):长度单位. mm(毫米):长度单位. pt(磅):1/72英寸,point. dp(与密度无关的像素):一 ...
- 深入理解 Python 虚拟机:协程初探——不过是生成器而已
深入理解 Python 虚拟机:协程初探--不过是生成器而已 在 Python 3.4 Python 引入了一个非常有用的特性--协程,在后续的 Python 版本当中不断的进行优化和改进,引入了新的 ...
- YXの每日挂分记录
7.11 T1 不开两倍数组 100->60. 7.18 T2 dp+矩乘 转移不判边界 100->10. 7.20 T2 人类智慧 1e6 n log n 100->10,求前 5 ...
- YCSB对MongoDB数据库性能测试
一.安装部署 1.1前置条件 Install Java and Maven Go to http://www.oracle.com/technetwork/java/javase/downloads/ ...
- Python 机器学习入门:数据集、数据类型和统计学
机器学习是通过研究数据和统计信息使计算机学习的过程.机器学习是迈向人工智能(AI)的一步.机器学习是一个分析数据并学会预测结果的程序. 数据集 在计算机的思维中,数据集是任何数据的集合.它可以是从数组 ...
- c#利用反射获取枚举的信息
1.将不同的枚举类型作为形参传入某函数内时,形参为Enum,在函数体内进行类型强转. private T GetEnumType<T>(object o) { T enumVal = (T ...
- Kubernetes:kube-apiserver 之鉴权
kubernetes:kube-apiserver 系列文章: Kubernetes:kube-apiserver 之 scheme(一) Kubernetes:kube-apiserver 之 sc ...
- 前端本地导出文件 导出txt sql (简版版的字符串案例)
1.首页明确要导出的根据 一般有图片 excel 文字.针对不同的文件类型 配置不同的参数 2.知识点 Blob URL.createObjectUrl new Blob( arr ...
- 每天5分钟复习OpenStack(十)Ceph 架构
在很多关于Ceph的文章中,通常会介绍一堆概念.虽然这些概念很重要,但是对于一个新手来说,同时接受太多的概念实际上很难消化.因此,在阅读本章节时要保持轻松的心情,只需要对所有的概念有个了解就可以了,因 ...
- STM32一个定时器输出四路不同频率和占空比PWM波的方法
一般来说,一个定时器输出4路频率相同.占空比不同的PWM波是比较容易的,使用PWM模式即可实现.如果说是输出4路频率不同.占空比不同的PWM就没有现成的模式,是不是无法实现了呢?答案肯定是" ...