引言
【JVM时区配置-两行代码让我们一帮子人熬了一个通宵】描述了由于代码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
getTimestamp
ResultSet的实现类是:com.mysql.cj.jdbc.result.ResultSetImpl,getTimestamp主要涉及两部分:
- MysqlTextValueDecoder将原始报文字段解析为InternalTimestamp对象
- SqlTimestampValueFactory将InternalTimestamp对象解析为应用使用的Timestamp
MysqlTextValueDecoder
SqlTimestampValueFactory
总结
以上是对数据库字段类型为datetime在新增、查询时候的一些逻辑,记录下来以备忘;
另外数据库字段类型为timestamp的在存储的时候还会有一次转换,使用的时候需要注意。