毫秒時間位數,時而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/C++的SDK,沒有搜尋到一個合適的封裝了的C#庫,故自己動手,簡單的封裝了一下,方便大家也方便自己使用和二次開發 ...
  • 前言 MediatR 是 .NET 下的一個實現消息傳遞的庫,輕量級、簡潔高效,用於實現進程內的消息傳遞機制。它基於中介者設計模式,支持請求/響應、命令、查詢、通知和事件等多種消息傳遞模式。通過泛型支持,MediatR 可以智能地調度不同類型的消息,非常適合用於領域事件處理。 在本文中,將通過一個簡 ...
  • 前言 今天給大家推薦一個超實用的開源項目《.NET 7 + Vue 許可權管理系統 小白快速上手》,DncZeus的願景就是做一個.NET 領域小白也能上手的簡易、通用的後臺許可權管理模板系統基礎框架。 不管你是技術小白還是技術大佬或者是不懂前端Vue 的新手,這個項目可以快速上手讓我們從0到1,搭建自 ...
  • 第1章:WPF概述 本章目標 瞭解Windows圖形演化 瞭解WPF高級API 瞭解解析度無關性概念 瞭解WPF體繫結構 瞭解WPF 4.5 WPF概述 ​ 歡迎使用 Windows Presentation Foundation (WPF) 桌面指南,這是一個與解析度無關的 UI 框架,使用基於矢 ...
  • 在日常開發中,並不是所有的功能都是用戶可見的,還在一些背後默默支持的程式,這些程式通常以服務的形式出現,統稱為輔助角色服務。今天以一個簡單的小例子,簡述基於.NET開發輔助角色服務的相關內容,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 第3章:佈局 本章目標 理解佈局的原則 理解佈局的過程 理解佈局的容器 掌握各類佈局容器的運用 理解 WPF 中的佈局 WPF 佈局原則 ​ WPF 視窗只能包含單個元素。為在WPF 視窗中放置多個元素並創建更貼近實用的用戶男面,需要在視窗上放置一個容器,然後在這個容器中添加其他元素。造成這一限制的 ...
  • 前言 在平時項目開發中,定時任務調度是一項重要的功能,廣泛應用於後臺作業、計劃任務和自動化腳本等模塊。 FreeScheduler 是一款輕量級且功能強大的定時任務調度庫,它支持臨時的延時任務和重覆迴圈任務(可持久化),能夠按秒、每天/每周/每月固定時間或自定義間隔執行(CRON 表達式)。 此外 ...
  • 目錄Blazor 組件基礎路由導航參數組件參數路由參數生命周期事件狀態更改組件事件 Blazor 組件 基礎 新建一個項目命名為 MyComponents ,項目模板的交互類型選 Auto ,其它保持預設選項: 客戶端組件 (Auto/WebAssembly): 最終解決方案裡面會有兩個項目:伺服器 ...
  • 先看一下效果吧: isChecked = false 的時候的效果 isChecked = true 的時候的效果 然後我們來實現一下這個效果吧 第一步:創建一個空的wpf項目; 第二步:在項目裡面添加一個checkbox <Grid> <CheckBox HorizontalAlignment=" ...
  • 在編寫上位機軟體時,需要經常處理命令拼接與其他設備進行通信,通常對不同的命令封裝成不同的方法,擴展稍許麻煩。 本次擬以特性方式實現,以兼顧維護性與擴展性。 思想: 一種命令對應一個類,其類中的各個屬性對應各個命令段,通過特性的方式,實現其在這包數據命令中的位置、大端或小端及其轉換為對應的目標類型; ...