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

来源:https://www.cnblogs.com/youzhibing/p/18010748
-Advertisement-
Play Games

開心一刻 今天我突然頓悟了,然後跟我媽聊天 我:媽,我發現一個餓不死的辦法 媽:什麼辦法 我:我先養個狗,再養個雞 媽:然後了 我:我拉的狗吃,狗拉的雞吃,雞下的蛋我吃,如此反覆,我們三都餓不死 媽:你整那麼多中間商幹啥,你就自己拉的自己吃得了,還省事 我又頓悟了,回到:也是啊 說句很重要的心裡話: ...


開心一刻

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

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

  媽:什麼辦法

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

  媽:然後了

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

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

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

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 如何實現一個雨滴落下效果 前言 下雨天坐在車窗前,看著雨滴順著車窗漸漸落下,這一唯美的場景,忍不住想記錄下來。最近在糾結電腦壁紙時,無意間看到有類似的場景,可以將自己喜歡的壁紙加上這種效果。作為多年切圖仔,不由地想到了用css動畫應該可以 ...
  • 描述 這是一個用於 Tampermonkey 或其他支持用戶腳本的瀏覽器擴展的油猴腳本。 看到論壇經常有小伙伴們需要下載某創力文檔-某人文庫一些免費文檔,但是相關網站瀏覽體驗不好各種廣告,各種登錄驗證,需要很多步驟才能下載文檔,該腳本就是為瞭解決您的煩惱而誕生,儘可能做到自動化。 安裝 安裝 Tam ...
  • 已經用 uni-app+vue3+ts 開發了一段時間,記錄一下日常遇見的問題和解決辦法 uni-app 中的單端代碼 uni-app 是支持多端,如果你想讓你的代碼,只在部分平臺使用,那麼就需要用的它的單端處理語法 //#ifdef 和 //#ifndef 等。 1. //#ifdef xxx 只 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 前端開發中難免會遇到價格和金額計算的需求,這類需求所要計算的數值大多數情況下是要求精確到小數點後的多少位。但是因為JS語言本身的缺陷,在處理浮點數的運算時會出現一些奇怪的問題,導致計算不精確。 本文嘗試從現象入手,分析造成這一問題原 ...
  • 廢話不多說,龍年騰雲特效送給大家 預覽 線上預覽 龍年騰雲 源碼 龍是使用的 svg,你也可以替換成其他樣式的龍,而雲是圖片轉化成的 base64 編碼,所以整個文件就是一個 html。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="U ...
  • 本文介紹了GNU項目與Linux系統的關係,GNU項目提供了許多自由軟體,其中一些成為了Linux系統的核心組件。文章還討論了Shell的概念以及在Linux中的應用,以及X Window System和GNOME桌面環境在提供圖形界面方面的作用。 ...
  • 痞子衡嵌入式半月刊: 第 91 期 這裡分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年分二十四節氣,希望在每個交節之日準時發佈一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 ...
  • 註意本文是SQL執行順序,不是MySQL Server內部執行流程。 MySQL並非像PostgreSQL(被認為是最接近 SQL 標準的資料庫之一)一樣嚴格按照SQL標準,MySQL執行引擎會根據查詢的具體情況和優化策略來決定具體的執行順序,所以SQL執行順序是理論順序。 書寫順序 select. ...
一周排行
    -Advertisement-
    Play Games
  • 在C#中使用SQL Server實現事務的ACID(原子性、一致性、隔離性、持久性)屬性和使用資料庫鎖(悲觀鎖和樂觀鎖)時,你可以通過ADO.NET的SqlConnection和SqlTransaction類來實現。下麵是一些示例和概念說明。 實現ACID事務 ACID屬性是事務處理的四個基本特征, ...
  • 我們在《SqlSugar開發框架》中,Winform界面開發部分往往也用到了自定義的用戶控制項,對應一些特殊的界面或者常用到的一些局部界面內容,我們可以使用自定義的用戶控制項來提高界面的統一性,同時也增強了使用的便利性。如我們Winform界面中用到的分頁控制項、附件顯示內容、以及一些公司、部門、菜單的下... ...
  • 在本篇教程中,我們學習瞭如何在 Taurus.MVC WebMVC 中進行數據綁定操作。我們還學習瞭如何使用 ${屬性名稱} CMS 語法來綁定頁面上的元素與 Model 中的屬性。通過這些步驟,我們成功實現了一個簡單的數據綁定示例。 ...
  • 是在MVVM中用來傳遞消息的一種方式。它是在MVVMLight框架中提供的一個實現了IMessenger介面的類,可以用來在ViewModel之間、ViewModel和View之間傳遞消息。 Send 接受一個泛型參數,表示要發送的消息內容。 Register 方法用於註冊某個對象接收消息。 pub ...
  • 概述:在WPF中,通過EventHandler可實現基礎和高級的UI更新方式。基礎用法涉及在類中定義事件,併在UI中訂閱以執行更新操作。高級用法藉助Dispatcher類,確保在非UI線程上執行操作後,通過UI線程更新界面。這兩種方法提供了靈活而可靠的UI更新機制。 在WPF(Windows Pre ...
  • 概述:本文介紹了在C#程式開發中如何利用自定義擴展方法測量代碼執行時間。通過使用簡單的Action委托,開發者可以輕鬆獲取代碼塊的執行時間,幫助優化性能、驗證演算法效率以及監控系統性能。這種通用方法提供了一種便捷而有效的方式,有助於提高開發效率和代碼質量。 在軟體開發中,瞭解代碼執行時間是優化程式性能 ...
  • 概述:Cron表達式是一種強大的定時任務調度工具,通過配置不同欄位實現靈活的時間規定。在.NET中,Quartz庫提供了簡便的方式配置Cron表達式,實現精準的定時任務調度。這種靈活性和可擴展性使得開發者能夠根據需求輕鬆地制定和管理定時任務,例如每天備份系統日誌或其他重要操作。 Cron表達式詳解 ...
  • 概述:.NET提供多種定時器,如System.Windows.Forms.Timer適用於UI,System.Web.UI.Timer用於Web,System.Diagnostics.Timer用於性能監控,System.Threading.Timer和System.Timers.Timer用於一般 ...
  • 問題背景 有同事聯繫我說,在生產環境上,訪問不了我負責的common服務,然後我去檢查common服務的health endpoint, 沒問題,然後我問了下異常,timeout導致的System.OperationCanceledException。那大概率是客戶端的問題,會不會是埠耗盡,用ne ...
  • 前言: 在本篇 Taurus.MVC WebMVC 入門開發教程的第四篇文章中, 我們將學習如何實現數據列表的綁定,通過使用 List<Model> 來展示多個數據項。 我們將繼續使用 Taurus.Mvc 命名空間,同時探討如何在視圖中綁定並顯示一個 Model 列表。 步驟1:創建 Model ...