天天看點

時區問題-datetime在JVM時區和MySQL Session時區的轉換

作者:大禹的足迹

引言

【JVM & MySQL時區配置問題-兩行代碼讓我們一幫子人熬了一個通宵】描述了由于代碼BUG導緻存儲到資料庫的時間比正常時間少八小時的案例。案例中對于資料庫字段類型是datetime和timestamp的時區轉換關系進行了描述,本文試圖從代碼角度描述以下邏輯:

  • JDBC場景下MySQL Session時區如何配置的
  • JDBC場景下datetime類型的資料如何轉換的

測試環境

MySQL

配置項 說明
MySQL version Windows MySQL Server 8.0.30.0
time_zone +08:00
system_time_zone
建立測試庫 create database test;
建立測試表 create table datetimetest( dt datetime);

應用資訊

java version

java version "1.8.0_341"
Java(TM) SE Runtime Environment (build 1.8.0_341-b10)
Java HotSpot(TM) Client VM (build 25.341-b10, mixed mode, sharing)           

pom

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.30</version>
</dependency>           

分析過程

測試場景

  • JVM是UTC + 8,MySQL time_zone是UTC + 8,MySQL JDBC Driver配置的是UTC + 0
  • JVM 應用程式原始時間是(UTC + 8):2022-10-16 10:00:00
  • MySQL JDBC Driver發送給MySQL server的時間是:2022-10-16 02:00:00(時間由UTC + 8轉換為UTC + 0)
  • MySQL server最終存儲的時間為:2022-10-16 02:00:00
  • MySQL JDBC Driver從資料庫中查出的時間是:2022-10-16 02:00:00
  • 應用程式最終讀取到的時間是:2022-10-16 10:00:00

測試代碼

時區問題-datetime在JVM時區和MySQL Session時區的轉換

測試代碼

getConnection

時區問題-datetime在JVM時區和MySQL Session時區的轉換

擷取連接配接

從圖中可以看出建立一個資料庫連接配接是個非常重量級的操作,選擇一個高效的連接配接池很重要。與本篇文章主要相關的是圖中斜體紅色加粗部分。

關注點一

關注跟time_zone相關的幾個配置項。

相關類及配置說明檔案:PropertyDefinitions、LocalizedErrorMessages.properties。

配置項 預設值 sinceVersion
connectionTimeZone 字元串類型,預設值:null 3.0.2
forceConnectionTimeZoneToSession 布爾類型,預設值:false 8.0.23
preserveInstants 布爾類型,預設值:true 8.0.23

關注點二

時區問題-datetime在JVM時區和MySQL Session時區的轉換

時區配置

executeUpdate

PreparedStatement的實作類是:com.mysql.cj.jdbc.ClientPreparedStatement,跟本次文章相關的内容如下:

編碼器

在NativeProtocol類初始化的時候會将不同資料類型的編碼器注冊&初始化:

static Map<Class<?>, Supplier<ValueEncoder>> DEFAULT_ENCODERS = new HashMap<>();
static {
          DEFAULT_ENCODERS.put(BigDecimal.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(BigInteger.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(Blob.class, BlobValueEncoder::new);
          DEFAULT_ENCODERS.put(Boolean.class, BooleanValueEncoder::new);
          DEFAULT_ENCODERS.put(Byte.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(byte[].class, ByteArrayValueEncoder::new);
          DEFAULT_ENCODERS.put(Calendar.class, UtilCalendarValueEncoder::new);
          DEFAULT_ENCODERS.put(Clob.class, ClobValueEncoder::new);
          DEFAULT_ENCODERS.put(Date.class, SqlDateValueEncoder::new);
          DEFAULT_ENCODERS.put(java.util.Date.class, UtilDateValueEncoder::new);
          DEFAULT_ENCODERS.put(Double.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(Duration.class, DurationValueEncoder::new);
          DEFAULT_ENCODERS.put(Float.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(InputStream.class, InputStreamValueEncoder::new);
          DEFAULT_ENCODERS.put(Instant.class, InstantValueEncoder::new);
          DEFAULT_ENCODERS.put(Integer.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(LocalDate.class, LocalDateValueEncoder::new);
          DEFAULT_ENCODERS.put(LocalDateTime.class, LocalDateTimeValueEncoder::new);
          DEFAULT_ENCODERS.put(LocalTime.class, LocalTimeValueEncoder::new);
          DEFAULT_ENCODERS.put(Long.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(OffsetDateTime.class, OffsetDateTimeValueEncoder::new);
          DEFAULT_ENCODERS.put(OffsetTime.class, OffsetTimeValueEncoder::new);
          DEFAULT_ENCODERS.put(Reader.class, ReaderValueEncoder::new);
          DEFAULT_ENCODERS.put(Short.class, NumberValueEncoder::new);
          DEFAULT_ENCODERS.put(String.class, StringValueEncoder::new);
          DEFAULT_ENCODERS.put(Time.class, SqlTimeValueEncoder::new);
          DEFAULT_ENCODERS.put(Timestamp.class, SqlTimestampValueEncoder::new);
          DEFAULT_ENCODERS.put(ZonedDateTime.class, ZonedDateTimeValueEncoder::new);
}           

與datetime相關的是SqlTimestampValueEncoder。

SqlTimestampValueEncoder

時區問題-datetime在JVM時區和MySQL Session時區的轉換

TimestampEncoder

getTimestamp

ResultSet的實作類是:com.mysql.cj.jdbc.result.ResultSetImpl,getTimestamp主要涉及兩部分:

  • MysqlTextValueDecoder将原始封包字段解析為InternalTimestamp對象
  • SqlTimestampValueFactory将InternalTimestamp對象解析為應用使用的Timestamp

MysqlTextValueDecoder

時區問題-datetime在JVM時區和MySQL Session時區的轉換

TimestampDecoder

SqlTimestampValueFactory

時區問題-datetime在JVM時區和MySQL Session時區的轉換

localTimestamp

總結

以上是對資料庫字段類型為datetime在新增、查詢時候的一些邏輯,記錄下來以備忘;

另外資料庫字段類型為timestamp的在存儲的時候還會有一次轉換,使用的時候需要注意。