首页 > 数据库 >记一次 MySQL timestamp 精度问题的排查 → 过程有点曲折

记一次 MySQL timestamp 精度问题的排查 → 过程有点曲折

时间:2024-01-15 09:00:11浏览次数:39  
标签:四舍五入 timestamp TIME 排查 源码 user MySQL 精度

开心一刻

  下午正准备出门,跟正刷着手机的老妈打个招呼

  我:妈,今晚我跟朋友在外面吃,就不在家吃了

  老妈拿着手机跟我说道:你看这叫朋友骗缅北去了,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());
        }
    }
}
View Code

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

  我们来看下日志输出

  保存的时候, 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 跟我们有什么关系,影响我们送外卖吗?

标签:四舍五入,timestamp,TIME,排查,源码,user,MySQL,精度
From: https://www.cnblogs.com/youzhibing/p/17953134

相关文章

  • Ubuntu22.04安装Mysql
    1、下载mysql1.1使用仓库安装工具下载wgethttps://dev.mysql.com/get/mysql-apt-config_0.8.29-1_all.deb安装使用sudodpkg-i./mysql-apt-config_0.8.29-1_all.deb1.2安装mysql更新仓库sudoaptupgradesudoaptupdate安装mysqlsudoapt-getinstall......
  • 【Vue】前端直接显示MySQL Datatime时间,显示为英文如何处理
    问题如图想让时间显示为自己想要的格式,可以自己编写一个函数constformatDate=(timestamp)=>{constdate=newDate(timestamp);constyear=date.getFullYear();constmonth=String(date.getMonth()+1).padStart(2,'0');constday=String(date.getDate......
  • MySQL常用命令
    操作数据库--链接数据库mysql-uroot-p--退出数据库quit/exit--显示数据库版本selectversion();--查看当前使用的数据库selectdatabase();--查看所有数据库showdatabases;--创建数据库createdatabasekunamecharset=utf8;--查看创建数据库的语句show......
  • 学习MySQL总结
    每一行称为记录每一列称为字段 SQLSQL语句的作用是实现数据库D客户端和服务端之间的通信.其表现口形式为:D带有一定格式的字符串.1970年E.F.Codd的《ARelationalModelofDataforLargeSharedDataBanks》的论文开始讲起。该论文奠定了关系模型的理论基础,Codd的同事DonCham......
  • MySql索引详情分析
    索引是帮助MySql高效获取数据的排好序的数据结构。(B+tree)为何是B+Tree这个数据结构呢?二叉树:对于单边增值的数据会造成数据倾斜,最终导致数据查询效率不高。红黑树:对于数据量大的时候树的高度会很高,也会导致查找次数变高。B-Tree叶节点具有相同的深度,叶节点的指......
  • MySQL修改安全策略时报错:ERROR 1193 (HY000): Unknown system variable ‘validate_pa
    我使用的版本是MySQL5.73,环境是LinuxCentOS7,其他版本不知道是否可行,望谅解。当我们想设置简单的密码的时候,看了别人发的如何修改安全策略的代码,如下:setglobalvalidate_password_policy=0;setglobalvalidate_password_length=1;但是当我们使用的时候,却报了这样一个......
  • 探索MySQL隔离级别
    深入理解与实战示例数据库事务的隔离级别是一个重要的概念,它定义了一个事务可能受其他并发事务影响的程度。MySQL提供了四种标准的隔离级别,每个级别都以不同的方式平衡了一致性和性能。本文将详细介绍这些隔离级别,并提供相应的示例。1.读未提交(ReadUncommitted)概念:这是最低......
  • mysql8.0数据目录
    1、数据库和文件系统1.1、查看默认数据库SHOWDATABASES;可以看到有4个数据库是属于MySQL自带的系统数据库。mysqlMySQL系统自带的核心数据库,它存储了MySQL的用户账户和权限信息,一些存储过程、事件的定义信息,一些运行过程中产生的日志信息,一些帮助信息以及时区信息等。information......
  • 【笔记-MySql】表处理语句
    【笔记-MySql】表处理语句查看表SHOWTABLES;查看列SHOWCOLUMNSFROM<tableName>;查看约束SHOWINDEXESFROM<tableName>;创建表CREATE[TEMPORARY]TABLE<tableName>(字段描述语句[,...][表级约束]);修改表名RENAMETABLE<oldtableName>TO<tabl......
  • 【笔记-MySql】库处理语句
    【笔记-MySql】库处理语句连接数据库mysql-u<user>-p<password>查看SHOWDATABASES;创建CREATEDATABASE[IFNOTEXISTS]<name>;删除DROPDATABASE<name>;选择USE<name>;......