毫秒時間位數,時而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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...