開心一刻
下午正準備出門,跟正刷著手機的老媽打個招呼
我:媽,今晚我跟朋友在外面吃,就不在家吃了
老媽拿著手機跟我說道:你看這叫朋友騙緬北去了,tm血都抽干了,多危險
我:那是他不行,你看要是吳京去了指定能跑回來
老媽:還吳京八經的,特么牛魔王去了都得耕地,唐三藏去了都得打出舍利,孫悟空去了都得演大馬戲
我:那照你這么說,唐僧師徒取經走差地方了唄
老媽:那可沒走錯,他當年擱西安出發(fā),他要是擱云南出發(fā)呀,上午到緬北,下午他就到西天
我:哈哈哈,那西游記就兩級唄,那要是超人去了呢?
老媽:那超人去了,回來光剩超,人留那了
問題復現
我簡化下業(yè)務與項目
數據庫:?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?,此刻我只想來一句:臥槽!
這說明數據入庫有問題,而不是讀取有問題
我們來梳理下數據入庫經歷了哪些環(huán)節(jié)
那問題肯定出在?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?方法會被調用
所以我們繼續(xù)從?EntityIdentityInsertAction#execute?跟,會來到?GetGeneratedKeysDelegate#executeAndExtract?
重點來了,大家打起精神
繼續(xù)跟進?session.getJdbcCoordinator().getResultSetReturn().executeUpdate( insert )?的?executeUpdate?
它長這樣
如果不是斷點跟的話
你知道接下來跟誰嗎?
當然,非常熟悉源碼的人(比如我),肯定知道跟誰
但是用了斷點,大家都知道跟誰了
繼續(xù)往下跟,當我們來到?ClientPreparedStatement#executeInternal?時,真相已經揭曉
此時已經來到了?mysql-connector-java?,發(fā)送給?MySQL Server?的?SQL?是:
?last_modified_time?精度沒丟!
那問題出在哪?
還能出在哪,?MySQL?唄!
說好的?MySQL?沒問題的了?
MySQL 時間精度
用排除法,排的只剩?MySQL?了,直接執(zhí)行?SQL?試試
哦豁,敢情前面的源碼分析全白分析了,我此刻的心情你們懂嗎
這必須得找?MySQL?要個說法,真是太狗了
我們去?MySQL?官方文檔找找看(注意參考手冊版本要和我們使用的?MySQL?版本一致)
大家不要通篇去讀,那樣太費時間,直接?search?用起來
The DATE, DATETIME, and TIMESTAMP Types 有這么一段比較關鍵
我給大家翻譯一下
繼續(xù)看?Fractional Seconds in Time Values,內容不多,大家可以通篇讀完
?MySQL?的?TIME?,?DATETIME?和?TIMESTAMP?都支持微妙級別(6位數)的小數位
精度直接在括號中指定,例如:?CREATE TABLE t1 (t TIME(3), dt DATETIME(6))?
小數位的范圍是 0 到 6。0 表示沒有小數部分,如果小數位缺省,則默認是0(SQL規(guī)范規(guī)定的默認是 6,MySQL8 默認值取 0 是為了兼容 MySQL 以前的版本)
當插入帶有小數部分的?TIME?,?DATETIME?或?TIMESTAMP?值到相同類型的列時,如果值的小數位與精度不匹配時,會進行四舍五入
四舍五入的判斷位置是精度的后一位,比如精度是 0,則看值的第 1 位小數,來決定是舍還是入,如果精度是 2,則看值的第 3 位小數
簡單來說:值的精度大于列類型的精度,就會存在四舍五入,否則值是多少就存多少
當發(fā)生四舍五入時,既不會告警也不會報錯,因為這就是 SQL 規(guī)范
那如果我不想要四舍五入了,有沒有什么辦法?
?MySQL?也給出了支持,就是啟用?SQL mode?:TIME_TRUNCATE_FRACTIONAL
啟用之后,當值的精度大于列類型的精度時,就是直接按列類型的精度截取,而不是四舍五入
那這么看下來,不是?MySQL?的鍋呀,?MySQL?表示這鍋我不背
那是誰的鍋?
只能說是開發(fā)人員的鍋,為什么不按?MySQL?使用說明書使用?
我要強調的是,產生這次問題的代碼不是我寫的,我寫的代碼怎么可能有?bug?
總結
1、?源碼?debug?堆棧
2、MySQL 時間精度
?MySQL?的?TIME?,?DATETIME?和?TIMESTAMP?類型都支持微妙級別(6位數)的精度
默認情況下會四舍五入,若想直接截斷,則需要開啟?SQL mode?:?TIME_TRUNCATE_FRACTIONAL?
3、規(guī)范
阿里巴巴的開發(fā)手冊中明確指出不能用:?java.sql.Timestamp?
另外很多公司的?MySQL?開發(fā)規(guī)范會強調:沒有特殊要求,時間類型用?datetime?
主要出于兩點考慮:1、?datetime?可用于分區(qū),而?timestamp?不行,2、?timestamp?的范圍只到?2038-01-19 03:14:07.499999?
有的開發(fā)小伙伴可能會問:如果到了?2038-01-19 03:14:07.499999?之后,?timestamp?該怎么辦?
我只能說:小伙子你想的太遠了,?2038?跟我們有什么關系,影響我們送外賣嗎?
補充
關于上面講到的?timestamp?不能分區(qū),進行一下補充(感謝 @xiaohuazi?指正)
它能分區(qū),但是和??DATE??和?DATETIME? 有一丟丟區(qū)別
MySQL 5.7 說明如下
MySQL 8.0 說明如下
文章來源:http://www.zghlxwxcb.cn/news/detail-789622.html
?timestamp?類型的列只能基于?UNIX_TIMESTAMP?函數進行分區(qū)文章來源地址http://www.zghlxwxcb.cn/news/detail-789622.html
到了這里,關于記一次 MySQL timestamp 精度問題的排查 → 過程有點曲折的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!