民工心事 发表于 2024-4-4 11:05:48

记一次 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]
查看完整版本: 记一次 MySQL timestamp 精度问题的排查 → 过程有点曲折