開心一刻 今天我突然頓悟了,然後跟我媽聊天 我:媽,我發現一個餓不死的辦法 媽:什麼辦法 我:我先養個狗,再養個雞 媽:然後了 我:我拉的狗吃,狗拉的雞吃,雞下的蛋我吃,如此反覆,我們三都餓不死 媽:你整那麼多中間商幹啥,你就自己拉的自己吃得了,還省事 我又頓悟了,回到:也是啊 說句很重要的心裡話: ...
開心一刻
今天我突然頓悟了,然後跟我媽聊天
我:媽,我發現一個餓不死的辦法
媽:什麼辦法
我:我先養個狗,再養個雞
媽:然後了
我:我拉的狗吃,狗拉的雞吃,雞下的蛋我吃,如此反覆,我們三都餓不死
媽:你整那麼多中間商幹啥,你就自己拉的自己吃得了,還省事
我又頓悟了,回到:也是啊
說句很重要的心裡話:祝大家在2024年,身體健康,萬事如意!
場景重溫
為了讓大家更好的明白問題,先做下相關準備工作
環境準備
資料庫: MySQL 8.0.30 ,表: tbl_order
DROP TABLE IF EXISTS `tbl_order`; CREATE TABLE `tbl_order` ( `id` bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵', `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '業務名', `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '創建時間', `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最終修改時間', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '訂單' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of tbl_order -- ---------------------------- INSERT INTO `tbl_order` VALUES (1, '123456', '2023-04-20 07:37:34.000', '2023-04-20 07:37:34.720'); INSERT INTO `tbl_order` VALUES (2, '654321', '2023-04-20 07:37:34.020', '2023-04-20 07:37:34.727');View Code
基於 JDK1.8 、 druid 1.1.12 、 mysql-connector-java 8.0.21 、 Spring 5.2.3.RELEASE
完整代碼:druid-timeout
毫秒位數捉摸不透
直接運行 com.qsl.DruidTimeoutTest#main ,會看到如下結果
資料庫表中的值: 2023-04-20 07:37:34.000 運行出來後是 2023-04-20 07:37:34.0 , 2023-04-20 07:37:34.720 對應 2023-04-20 07:37:34.72
2023-04-20 07:37:34.020 對應 2023-04-20 07:37:34.02 , 2023-04-20 07:37:34.727 對應 2023-04-20 07:37:34.727
毫秒位數時而1位,時而2位,時而3位,搞的我好亂吶
原因分析
大家註意看這個代碼
獲取列值, sqlRowSet.getObject(i) 返回的類型是 Object ,我們調整下輸出: System.out.println(obj.getClass().getName() + " " + obj);
此時輸出結果如下
可以看到, java 程式中,此時的時間類型是 java.sql.Timestamp
有了這個依托點,原因就很好分析了
Timestamp的toString
我們知道, java 中直接輸出對象,會調用對象的 toString 方法,如果自身沒有重寫 toString 則會沿用 Object 的 toString 方法
我們先來看一下 Object 的 toString 方法
粗略看一下,返回值明顯不是 2023-04-20 07:37:34.0 這種時間字元串格式
那說明什麼?
說明 Timestamp 肯定重寫了 toString 方法嘛
java.sql.Timestamp#toString 內容如下
/** * Formats a timestamp in JDBC timestamp escape format. * <code>yyyy-mm-dd hh:mm:ss.fffffffff</code>, * where <code>ffffffffff</code> indicates nanoseconds. * <P> * @return a <code>String</code> object in * <code>yyyy-mm-dd hh:mm:ss.fffffffff</code> format */ @SuppressWarnings("deprecation") public String toString () { int year = super.getYear() + 1900; int month = super.getMonth() + 1; int day = super.getDate(); int hour = super.getHours(); int minute = super.getMinutes(); int second = super.getSeconds(); String yearString; String monthString; String dayString; String hourString; String minuteString; String secondString; String nanosString; String zeros = "000000000"; String yearZeros = "0000"; StringBuffer timestampBuf; if (year < 1000) { // Add leading zeros yearString = "" + year; yearString = yearZeros.substring(0, (4-yearString.length())) + yearString; } else { yearString = "" + year; } if (month < 10) { monthString = "0" + month; } else { monthString = Integer.toString(month); } if (day < 10) { dayString = "0" + day; } else { dayString = Integer.toString(day); } if (hour < 10) { hourString = "0" + hour; } else { hourString = Integer.toString(hour); } if (minute < 10) { minuteString = "0" + minute; } else { minuteString = Integer.toString(minute); } if (second < 10) { secondString = "0" + second; } else { secondString = Integer.toString(second); } if (nanos == 0) { nanosString = "0"; } else { nanosString = Integer.toString(nanos); // Add leading zeros nanosString = zeros.substring(0, (9-nanosString.length())) + nanosString; // Truncate trailing zeros char[] nanosChar = new char[nanosString.length()]; nanosString.getChars(0, nanosString.length(), nanosChar, 0); int truncIndex = 8; while (nanosChar[truncIndex] == '0') { truncIndex--; } nanosString = new String(nanosChar, 0, truncIndex + 1); } // do a string buffer here instead. timestampBuf = new StringBuffer(20+nanosString.length()); timestampBuf.append(yearString); timestampBuf.append("-"); timestampBuf.append(monthString); timestampBuf.append("-"); timestampBuf.append(dayString); timestampBuf.append(" "); timestampBuf.append(hourString); timestampBuf.append(":"); timestampBuf.append(minuteString); timestampBuf.append(":"); timestampBuf.append(secondString); timestampBuf.append("."); timestampBuf.append(nanosString); return (timestampBuf.toString()); }View Code
註意看註釋: yyyy-mm-dd hh:mm:ss.fffffffff ,說明精度是到納秒級別,不只是到毫秒哦!
該方法很長,我們只需要關註 fffffffff 的處理,也就是如下代碼
nanos 類型是 int : private int nanos; ,用來存儲秒後面的那部分值
資料庫表中的值: 2023-04-20 07:37:34.000 對應的 nanos 的值是 0, 2023-04-20 07:37:34.720 對應的 nanos 的值是多少了?
不是、不是、不是 720 ,因為它的格式是 fffffffff ,所以應該是 720000000
那 2023-04-20 07:37:34.020 對應的 nanos 的值又是多少?
不是、不是、不是 200000000 ,而是 20000000 ,因為 nanos 是 int 類型,不能以0開頭
再回到上述代碼,當 nanos 等於 0 時, nanosString 即為字元串0,所以 2023-04-20 07:37:34.000 對應 2023-04-20 07:37:34.0
當 nanos 不等於 0 時
1、先將 nanos 轉換成字元串 nanosString , nanosString 的位數與 nanos 一致
2、 nanosString 前補0, nanos 的位數與 9 差多少就前補多少個0
例如 2023-04-20 07:37:34.020 對應的 nanos 是 20000000 ,只有8位,前補1個0,則 nanosString 的值是 020000000
3、去掉末尾的0
020000000 去掉末尾的0,得到 02
原因是不是找到了?
總結下就是: java.sql.Timestamp#toString 會格式化掉 nanosString 末尾的0!(註意: nanos 的值是沒有變的)
是不是很精辟
但是問題又來了:為什麼要格式化末尾的0?
說實話,我沒有找到一個確切的、準確的說明
只是自己給自己編造了一個勉強的理由:簡潔化,提高可讀性
去掉 nanosString 末尾的 0,並沒有影響時間值的準確性,但是可以簡化整個字元串,末尾跟著一串0,可讀性會降低
如果非要保留末尾的0,可以自定義格式化方法,想保留幾個0就保留幾個0
類型對應
MySQL 類型和 JAVA 類型是如何對應的,是不是很想知道這個問題?
那就安排起來,如何尋找了?
別慌,我有葵花寶典:雜談篇之我是怎麼讀源碼的,授人以漁
為了節約時間,我就不帶你們一步一步 debug 了,直接帶你們來到關鍵點 com.mysql.cj.protocol.a.ColumnDefinitionReader#read
裡面有如下關鍵代碼
為了方便你們跟源碼,我把此刻的堆棧信息貼一下
我們繼續跟進 unpackField ,會發現裡面有這樣一行代碼
恭喜你,只差臨門一腳了
按住 ctrl 鍵,滑鼠左擊 MysqlType ,歡迎來到 類型對應 世界: com.mysql.cj.MysqlType
其構造方法
我們暫時只需要關註: mysqlTypeName 、 jdbcType 和 javaClass
接下來我們找到 MySQL 的 DATETIME
此處的 Timestamp.class 就是 java.sql.Timestamp
其他的對應關係,大家也可以看看,比如
額外拓展
TIMESTAMP範圍
回答這個問題的時候,一定要說明前提條件
MySQL8 ,範圍是 '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC
JDK8 , Timestamp 構造方法
入參是 long 類型,其最大值是 9223372036854775807 ,1 年是 365*24*60*60*1000=31536000000 毫秒
也就是 long 最大可以記錄 6269161692 年,所以範圍是 1970 ~ (1970 + 6269161692) ,不會有 2038年問題
MySQL 的 TIMESTAMP 和 JAVA 的 Timestamp 是對應關係,並不是對等關係,大家別搞混了
關於不允許使用java.sql.Timestamp
阿裡巴巴的開發手冊中明確指出不能用: java.sql.Timestamp
為什麼 mysql-connector-java 還要用它?
可以從以下幾點來分析
1、 java.sql.Timestamp 存在有存在的道理,它有它的優勢
1.1 精度到了納秒級別
1.2 被設計為與 SQL TIMESTAMP 類型相容,這意味著在資料庫交互中,使用 Timestamp 可以減少數據類型轉換的問題,提高數據的一致性和準確性
1.3 時間方面的計算非常方便
2、在某些特定情況下才會觸發 Timestamp 的 bug ,我們不能以此就完全否定 Timestamp 吧
況且 JDK9 也修複了
3、 MySQL 的 TIMESTAMP 如果不對應 java.sql.Timestamp ,那該對應 JAVA 的哪個類型?
MySQL的DATETIME為什麼也對應java.sql.Timestamp
MySQL 的 TIMESTAMP 對應 java.sql.Timestamp ,對此我相信大家都沒有疑問
為何 MySQL 的 DATETIME 也對應 java.sql.Timestamp ?
我反問一句,不對應 java.sql.Timestamp 對應哪個?
LocalDateTime ?試問 JDK8 之前有 LocalDateTime 嗎?
不過 mysql-connector-java 還是做了調整,我們來看下
我把 mysql-connector-java 的源碼 clone 下來了,更方便我們查看提交記錄
找到 com.mysql.cj.MysqlType#DATETIME ,在其前面空白處右擊
滑鼠左擊 Annotate with Git Blame ,會看到每一行的最新修改提交記錄
我們繼續左擊 DATETIME 的最新修改提交記錄
可以看到詳細的提交信息
雙擊 MysqlType.java ,可以看到修改內容
可以看到 MySQL 的 DATETIME 對應的 JAVA 類型從 java.sql.Timestamp 調整成了 java.time.LocalDateTime
那 mysql-connector-java 哪個版本開始生效的了?
它是開源的,那就直接在 github 上找 mysql-connector-java 的 issue : Bug#102321
但是你會發現搜不到
這是因為 mysql-connector-java 調整成了 mysql-connector-j ,相關 issue 沒有整合
那麼我們就換個方式搜,就像這樣
回車,結果如下
也沒有搜到!!!
但你去點一下左側的 Commits ,你會發現有結果!!!
Commits 不是 0 嗎,怎麼有結果,誰來都懵呀
這絕對是 github 的 Bug 呀(這個我回頭找下官方確認下,不深究!)
我們點擊 Commits 的這個搜索結果,會來到如下界面
答案已經揭曉
從 8.0.24 開始, MySQL 的 DATETIME 對應的 JAVA 類型從 java.sql.Timestamp 調整成 java.time.LocalDateTime
總結
java.sql.Timestamp
1、設計初衷就是為了對應 SQL TIMESTAMP ,所以不管是 MySQL 還是其他資料庫,其 TIMESTAMP 對應的 JAVA 類型都是 java.sql.Timestamp
2、 MySQL 的 TIMESTAMP 有 2038年 問題,是因為它的底層存儲是 4 個位元組,並且最高位是符號位,至於其他類型的資料庫是否有該問題,得看具體實現
3、在清楚使用情況的前提下(不觸發 JDK8 BUG )是可以使用的,有些場景使用 java.sql.Timestamp 確實更方便
DATETIME對應類型
SQL DATETIME 對應的 JAVA 類型,沒有統一標準,需要看具體資料庫的 jdbc 版本
比如 mysql-connector-java , 8.0.24 之前, DATETIME 對應的 JAVA 類型是 java.sql.Timestamp ,而 8.0.24 及之後,對應的是 java.time.LocalDateTime
至於其他資料庫的 jdbc 是如何對應的,就交給你們了,可以從最新版本著手去分析