Java與MySQL導(dǎo)致的時(shí)間不一致問題分析
時(shí)間戳與時(shí)區(qū)的關(guān)系
時(shí)間戳一般指的是Unix 時(shí)間戳:是從1970年1月1日(UTC/GMT的午夜)開始所經(jīng)過的秒數(shù),不考慮閏秒。
那么和時(shí)區(qū)又有什么關(guān)系呢?
public static void main(String[] args) throws ParseException {
TimeZone bjTimeZone = TimeZone.getTimeZone("Asia/Shanghai");
TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
// 時(shí)間戳在不同時(shí)區(qū)下的日期
Date date = new Date(0L);
System.out.println("時(shí)間戳 0 對應(yīng)的系統(tǒng)時(shí)間:" + date);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(bjTimeZone);
System.out.println("時(shí)間戳 0 在東八時(shí)區(qū)下表達(dá)的時(shí)間:" + sdf.format(date));
sdf.setTimeZone(utcTimeZone);
System.out.println("時(shí)間戳 0 在UTC時(shí)區(qū)下表達(dá)的時(shí)間:" + sdf.format(date));
// 日期在不同時(shí)區(qū)下的時(shí)間戳
sdf.setTimeZone(bjTimeZone);
System.out.println("2024-02-25 00:00:00 在東八時(shí)區(qū)下的時(shí)間戳:" + sdf.parse("2024-02-25 00:00:00").getTime());
sdf.setTimeZone(utcTimeZone);
System.out.println("2024-02-25 00:00:00 在UTC時(shí)區(qū)下的時(shí)間戳:" + sdf.parse("2024-02-25 00:00:00").getTime());
}
時(shí)間戳 0 對應(yīng)的系統(tǒng)時(shí)間:Thu Jan 01 08:00:00 CST 1970
時(shí)間戳 0 在東八時(shí)區(qū)下表達(dá)的時(shí)間:1970-01-01 08:00:00
時(shí)間戳 0 在UTC時(shí)區(qū)下表達(dá)的時(shí)間:1970-01-01 00:00:00
2024-02-25 00:00:00 在東八時(shí)區(qū)下的時(shí)間戳:1708790400
2024-02-25 00:00:00 在UTC時(shí)區(qū)下的時(shí)間戳:1708819200
- 一個(gè)時(shí)間戳在不同時(shí)區(qū)下所表達(dá)的時(shí)間是不一樣的。
- 一個(gè)日期在不同時(shí)區(qū)下的時(shí)間戳是不同的。東八時(shí)區(qū)(北京時(shí)間)與 UTC 世界協(xié)調(diào)時(shí)間相差了八小時(shí)??梢酝ㄟ^時(shí)間戳的差值進(jìn)行計(jì)算驗(yàn)證:
(1708819200 - 1708790400) / 60 / 60 = 8 - 一個(gè)日期在不同時(shí)區(qū)下的時(shí)間戳的差值:時(shí)區(qū)間的偏移量
- 兩個(gè)時(shí)區(qū)不同,但時(shí)間相同的日期表達(dá)的意義也不一樣,就如北京八點(diǎn)與美國八點(diǎn)的區(qū)別
查詢、修改 Java 程序使用的時(shí)區(qū)
public static void main(String[] args) {
// 查看默認(rèn)時(shí)區(qū)與時(shí)區(qū)ID
TimeZone defaultTimeZone = TimeZone.getDefault();
ZoneId systemDefaultZoneId = ZoneId.systemDefault();
// sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=29,lastRule=null]
System.out.println(defaultTimeZone);
// Asia/Shanghai
System.out.println(systemDefaultZoneId);
// 設(shè)置默認(rèn)時(shí)區(qū)
TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("UTC")));
// sun.util.calendar.ZoneInfo[id="UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
System.out.println(TimeZone.getDefault());
}
查詢、修改 MySQL 數(shù)據(jù)庫使用的時(shí)區(qū)
查詢:
show global variables like "%time_zone%";
| Variable_name | Value |
|---|---|
| system_time_zone(系統(tǒng)時(shí)區(qū)) | UTC |
| time_zone(會話時(shí)區(qū)) | SYSTEM |
系統(tǒng)時(shí)區(qū):UTC,即比東八時(shí)區(qū)慢8個(gè)小時(shí)??梢酝ㄟ^
SELECT NOW()查詢當(dāng)前時(shí)間對比 PC 上的時(shí)間驗(yàn)證:MYSQL: 2024-02-24 19:07:31 / PC: 2024-02-25 03:07:31。該值讀取的就是 MySQL 服務(wù)所在的操作系統(tǒng)上使用的時(shí)區(qū),以 Linux 系統(tǒng)為例,可通過date -R查看:Sat, 24 Feb 2024 19:31:56 +0000。
會話時(shí)區(qū):采用系統(tǒng)時(shí)區(qū),即 UTC。
修改:系統(tǒng)時(shí)區(qū):修改系統(tǒng)時(shí)區(qū),即修改 MySQL 服務(wù)所在服務(wù)器的時(shí)區(qū),以 Linux 操作系統(tǒng)為例:cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime,將時(shí)區(qū)文件 copy 到 etc 目錄下且命名為 localtime。
會話時(shí)區(qū):
- SQL 的方式:
set global time_zone = '+8:00'orset global time_zone = 'Asia/Shanghai' - 修改
my.cnf配置文件中的default-time-zone=Asia/Shanghai屬性
JDBC 讀取并設(shè)置 MySQL 服務(wù)使用的時(shí)區(qū)的流程
以mysql-connector-j-8.0.33.jar 為例:
com.mysql.cj.protocol.a.NativeProtocol.configureTimeZone 設(shè)置MySQL服務(wù)使用的時(shí)區(qū)的流程:
- 優(yōu)先讀取
connectionTimeZone或serverTimezonejdbc 屬性作為會話使用的時(shí)區(qū) - 若配置的屬性為
SERVER,則在第一次調(diào)用ServerSession.getSessionTimeZone()時(shí)讀取數(shù)據(jù)庫中配置的時(shí)區(qū) - 若沒有配置則以本地服務(wù)器的時(shí)區(qū)作為 MySQL 服務(wù)器的時(shí)區(qū)

JDBC 如何應(yīng)用的時(shí)區(qū)
說明:當(dāng)前Java 程序東八時(shí)區(qū),MySQL服務(wù) UTC 時(shí)區(qū)。
存儲日期數(shù)據(jù)時(shí),com.mysql.cj.protocol.a.SqlTimestampValueEncoder.getString 對于 Date 類型字段值的處理:將Java程序時(shí)區(qū)下的日期的時(shí)間戳,轉(zhuǎn)為MySQL服務(wù)時(shí)區(qū)下的日期(2024-02-25 11:52:56 > 2024-02-25 03:52:56)

查詢?nèi)掌跀?shù)據(jù)時(shí),com.mysql.cj.result.SqlTimestampValueFactory.localCreateFromDatetime 對于 Date 類型字段的處理:將MySQL服務(wù)時(shí)區(qū)下的日期的時(shí)間戳,轉(zhuǎn)為Java程序時(shí)區(qū)下的日期。(2024-02-24 21:34:55 > 2024-02-25 05:34:55)

根據(jù)源代碼的實(shí)現(xiàn)可以發(fā)現(xiàn)一個(gè)規(guī)律:都是先將日期根據(jù)所屬時(shí)區(qū)轉(zhuǎn)換為時(shí)間戳后,在根據(jù)需要轉(zhuǎn)換的時(shí)區(qū)轉(zhuǎn)換為最終日期。
Java 程序時(shí)區(qū)與 MySQL 服務(wù)使用時(shí)區(qū)不一致導(dǎo)致的問題
在 JDBC 讀取并設(shè)置 MySQL 服務(wù)使用的時(shí)區(qū)的流程 中說到:MySQL 服務(wù)使用的時(shí)區(qū)會受到 jdbc 參數(shù)的影響,也就是說可能會出現(xiàn):實(shí)際的數(shù)據(jù)庫時(shí)區(qū)與 jdbc 參數(shù)聲明的時(shí)區(qū)是不一樣的。最壞情況下會出現(xiàn):Java 程序時(shí)區(qū)、jdbc 聲明時(shí)區(qū)、實(shí)際數(shù)據(jù)庫時(shí)區(qū)都是不同的。
不對每種情況的流程進(jìn)行逐個(gè)分析。一般開發(fā)時(shí)遇到時(shí)間不一致時(shí),大概都是分為以下的兩種情況:
Java 程序時(shí)區(qū) 與 jdbc 參數(shù)時(shí)區(qū)一致,實(shí)際數(shù)據(jù)庫時(shí)區(qū)不一致:這種情況下,會出現(xiàn)程序、數(shù)據(jù)庫中展示日期都是相同的,但兩個(gè)日期分別對應(yīng)的時(shí)間戳不相同。看起來是沒有問題,但實(shí)際上,數(shù)據(jù)庫中存儲的日期的時(shí)間戳已經(jīng)不是Java程序中該日期所對應(yīng)的時(shí)間戳了。如最開始說到的:一個(gè)日期在不同時(shí)區(qū)下的時(shí)間戳是不同的,那么表達(dá)的意義也不一樣,就如北京八點(diǎn)與美國八點(diǎn)的區(qū)別。整理一下 JDBC 驅(qū)動(dòng)包在這種情況下的處理流程:
1)存儲:
2)讀?。軌蛟谵D(zhuǎn)為 Java 程序中的正確日期):
- 獲取日期在 Java 程序時(shí)區(qū)下的時(shí)間戳
- 根據(jù) JDBC 參數(shù)時(shí)區(qū)將時(shí)間戳轉(zhuǎn)為 MySQL 服務(wù)時(shí)區(qū)下的日期字符串(由于二者一致,所以結(jié)果沒有變化)
- 組裝為數(shù)據(jù)包發(fā)送給實(shí)際的 MySQL 服務(wù)
- MySQL 服務(wù)根據(jù)該日期字符串,轉(zhuǎn)為實(shí)際數(shù)據(jù)庫時(shí)區(qū)下的該日期(此時(shí)時(shí)間戳已經(jīng)不同了)
- 讀取實(shí)際數(shù)據(jù)庫中的日期字符串
- 根據(jù) JDBC 參數(shù)時(shí)區(qū)將日期轉(zhuǎn)為對應(yīng)時(shí)間戳
- 將時(shí)間戳在根據(jù) Java 程序時(shí)區(qū)轉(zhuǎn)為對應(yīng)的日期(由于與 JDBC 參數(shù)時(shí)區(qū)一致,最終程序中的日期還是相同的)
Java 程序時(shí)區(qū) 與 實(shí)際數(shù)據(jù)庫時(shí)區(qū)不一致:這種情況下不考慮 jdbc 參數(shù)時(shí)區(qū)的影響,會出現(xiàn)程序、數(shù)據(jù)庫中展示的日期不同,但兩個(gè)不同日期的時(shí)間戳是一致的。捋一下 JDBC 處理流程:
1)存儲:
- 獲取日期在 Java 程序時(shí)區(qū)下的時(shí)間戳
- 獲取實(shí)際 MySQL 服務(wù)的時(shí)區(qū),并轉(zhuǎn)為該時(shí)區(qū)下的日期字符串(由于二者不一致,所以轉(zhuǎn)換結(jié)果日期也不同)
- 組裝為數(shù)據(jù)包發(fā)送給實(shí)際的 MySQL 服務(wù)
- MySQL 服務(wù)根據(jù)該日期字符串,轉(zhuǎn)為實(shí)際數(shù)據(jù)庫時(shí)區(qū)下的該日期(此時(shí)展示的日期已經(jīng)不同,但時(shí)間戳還是相同的)
2)讀?。軌蛟谵D(zhuǎn)為 Java 程序中的正確日期):
- 讀取實(shí)際數(shù)據(jù)庫中的日期字符串
- 獲取實(shí)際 MySQL 服務(wù)的時(shí)區(qū),將日期轉(zhuǎn)為對應(yīng)時(shí)間戳
- 將時(shí)間戳在根據(jù) Java 程序時(shí)區(qū)轉(zhuǎn)為對應(yīng)的日期(雖然時(shí)區(qū)不同,但是時(shí)間戳還是同一個(gè),能夠在正確轉(zhuǎn)為 Java 程序中的正確日期)
僅依靠數(shù)據(jù)庫時(shí)區(qū)生成時(shí)間數(shù)據(jù)(這是一種特殊情況):
當(dāng)我們在為創(chuàng)建、修改時(shí)間字段添加了以下自動(dòng)獲取時(shí)間屬性時(shí):
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間', `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時(shí)間'
1)存儲:僅依賴數(shù)據(jù)庫時(shí)區(qū),不涉及時(shí)區(qū)轉(zhuǎn)換的問題
2)讀?。軌蛟谵D(zhuǎn)為 Java 程序中的正確日期):
- 讀取實(shí)際數(shù)據(jù)庫中的日期字符串
- 獲取實(shí)際 MySQL 服務(wù)的時(shí)區(qū),將日期轉(zhuǎn)為對應(yīng)時(shí)間戳
- 將時(shí)間戳在根據(jù) Java 程序時(shí)區(qū)轉(zhuǎn)為對應(yīng)的日期(雖然時(shí)區(qū)不同,但是時(shí)間戳沒有發(fā)生變化,能夠在正確轉(zhuǎn)為 Java 程序中的正確日期)
解決方式:
- 將 Java 程序時(shí)區(qū)、jdbc 聲明的參數(shù)時(shí)區(qū)、實(shí)際數(shù)據(jù)庫時(shí)區(qū)設(shè)置為相同的時(shí)區(qū)
- 將 Java 程序時(shí)區(qū)、實(shí)際數(shù)據(jù)庫時(shí)區(qū)設(shè)置為相同的時(shí)區(qū),且不設(shè)置 jdbc 聲明的參數(shù)時(shí)區(qū)這一干擾配置
到此這篇關(guān)于Java與MySQL導(dǎo)致的時(shí)間不一致問題分析的文章就介紹到這了,更多相關(guān)Java MySQL時(shí)區(qū)不一致 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot項(xiàng)目打成war包部署到tomcat遇到的一些問題
這篇文章主要介紹了springboot項(xiàng)目打成war包部署到tomcat遇到的一些問題,需要的朋友可以參考下2017-06-06
Java時(shí)間處理第三方包Joda?Time使用詳解
這篇文章主要為大家介紹了Java時(shí)間處理第三方包Joda?Time使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07

