毫秒時間位數,時而1位,時而2位,時而3位,搞得我好亂吶!

開心一刻

  今天我突然頓悟了,然後跟我媽聊天

  我:媽,我發現一個餓不死的辦法

  媽:什麼辦法

  我:我先養個狗,再養個雞

  媽:然後了

  我:我拉的狗喫,狗拉的雞喫,雞下的蛋我喫,如此反覆,我們三都餓不死

  媽:你整那麼多中間商幹啥,你就自己拉的自己吃得了,還省事

  我又頓悟了,回到:也是啊

  說句很重要的心裏話:祝大家在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 是如何對應的,就交給你們了,可以從最新版本着手去分析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章