毫秒時間位數,時而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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...