引言
【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
測試代碼
測試代碼
getConnection
擷取連接配接
從圖中可以看出建立一個資料庫連接配接是個非常重量級的操作,選擇一個高效的連接配接池很重要。與本篇文章主要相關的是圖中斜體紅色加粗部分。
關注點一
關注跟time_zone相關的幾個配置項。
相關類及配置說明檔案:PropertyDefinitions、LocalizedErrorMessages.properties。
配置項 | 預設值 | sinceVersion |
connectionTimeZone | 字元串類型,預設值:null | 3.0.2 |
forceConnectionTimeZoneToSession | 布爾類型,預設值:false | 8.0.23 |
preserveInstants | 布爾類型,預設值:true | 8.0.23 |
關注點二
時區配置
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
TimestampEncoder
getTimestamp
ResultSet的實作類是:com.mysql.cj.jdbc.result.ResultSetImpl,getTimestamp主要涉及兩部分:
- MysqlTextValueDecoder将原始封包字段解析為InternalTimestamp對象
- SqlTimestampValueFactory将InternalTimestamp對象解析為應用使用的Timestamp
MysqlTextValueDecoder
TimestampDecoder
SqlTimestampValueFactory
localTimestamp
總結
以上是對資料庫字段類型為datetime在新增、查詢時候的一些邏輯,記錄下來以備忘;
另外資料庫字段類型為timestamp的在存儲的時候還會有一次轉換,使用的時候需要注意。