背景
时间瞬间是时间线上的特定时刻。当一个时间点总是指向同一时间点时,当它的值被存储到数据库或从数据库中检索时,无论数据库服务器和客户端在哪个时区运行,都可以说是被保留的。
TIMESTAMP
是唯一设计用于存储即时数据的 MySQL 数据类型。为了保留时间瞬间,服务器在需要时在传入或传出时间值中应用时区转换。传入值由服务器从连接会话的时区转换为协调世界时 (UTC) 以进行存储,传出值从 UTC 转换为会话时区。从 MySQL 8.0.19 开始,您还可以在存储
TIMESTAMP
值时指定时区偏移量(有关详细信息,请参阅
DATE、DATETIME 和 TIMESTAMP 类型),在这种情况下
TIMESTAMP
值从指定的偏移量而不是会话时区转换为 UTC。但是,一旦存储,原始偏移量信息将不再保留。
数据类型的情况就没那么简单了
DATETIME
:它不代表一个瞬间,并且当没有指定时区偏移量时,DATETIME
值没有时区转换,因此它们按原样存储和检索。但是,对于指定的时区偏移量,输入值在存储之前会转换为会话时区;结果是,当在具有与指定时区偏移量不同的不同会话中检索时,该
DATETIME
值将与原始输入值不同。
因为除此之外的 MySQL 数据类型
TIMESTAMP
(以及其他 MySQL 数据类型的 Java 包装类)不代表真实的时间点;在存储和检索值时混淆即时表示和非即时表示的日期时间类型可能会导致意外结果。例如:
java.sql.Timestamp
例如, 当存储到一DATETIME
列时,当将它检索到与存储值时客户端所在时区不同的客户端时,您可能无法取回相同的即时值。例如,在
java.time.LocalDateTime
将 a 存储到TIMESTAMP
列时,您可能没有为其存储正确的基于 UTC 的值,因为该值的时区实际上是未定义的。
因此,在使用服务器时,不要将即时日期时间类型 ( java.util.Calendar
,
java.util.Date
,
java.time.OffsetDateTime
,
java.sql.Timestamp
) 传递给非即时日期时间类型(例如 ,
java.sql.DATE
,
java.time.LocalDate
,
) java.time.LocalTime
,
java.time.OffsetTime
反之亦然。
本节的其余部分讨论如何在使用 Connector/J 时保留时间点。
用 Connector/J 保存瞬间
场景:让我们假设一个应用程序在某个应用程序服务器上运行,并使用 Connector/J 连接到 MySQL 服务器。某些事件发生在连接会话中,为其生成时间戳,并且事件时间戳与应用程序服务器的 JVM 时区相关联。这些时间戳将被存储到 MySQL 服务器上,并且稍后也会从中检索。
挑战:当使用 Connector/J 将时间戳保存到服务器或从服务器检索时,需要保留时间戳的即时值。因为 MySQL 服务器在保存到服务器或从服务器检索时总是隐含地假定一个时间值引用连接会话时区(由会话time_zone
变量设置),所以只有在以下情况下才能正确保留时间值:
当Connector/J 运行在与MySQL 服务器相同的时区(即服务器的会话时区与JVM 的时区相同)时,时间瞬间会自然保留,不需要进行时区转换。请注意,在这种情况下,仅当服务器和 JVM 将来继续始终在同一时区运行时,才会真正保留时间点。
当 Connector/J 运行在与 MySQL 服务器不同的时区(即 JVM 的时区与服务器的会话时区不同)时,Connector/.J 执行以下操作之一:
从服务器查询会话时区的值,并将事件时间戳在会话时区和JVM时区之间进行转换。
将服务器的会话时区更改为 JVM 时区的时区,之后不需要时区转换。
将服务器会话时区更改为用户指定的所需时区,然后在 JVM 时区和用户指定时区之间转换时间戳。
我们将上述用于时间即时保存的解决方案确定为解决方案 1、2a、2b 和 2c。为了实现这些解决方案,自 8.0.23 版以来,Connector/J 中引入了以下连接属性:
preserveInstants={true|false}
:是否尝试通过调整时间戳来保留即时值。如果是
false
,则不尝试转换;时间戳按原样发送到服务器进行存储,并保留其视觉呈现,而不是实际时间。当 Connector/J 从服务器检索它时,不同的时区可能与它相关联,因为检索可能发生在不同的 JVM 时区。例如: 例如:时区:JVM 为 UTC,服务器会话为 UTC+1
来自客户端的原始时间戳(UTC):
2020-01-01 01:00:00
连接器/J 发送到服务器的时间戳:(
2020-01-01 01:00:00
无转换)服务器内部存储的时间戳值:(
2020-01-01 00:00:00 UTC
内部转换2020-01-01 00:00:00 UTC+1
为 UTC 后)稍后检索到服务器部分的时间戳值(在 UTC+1 中):(在从 UTC 到 UTC+1
2020-01-01 01:00:00
的内部转换之后 )2020-01-01 00:00:00
Connector/J 在其他 JVM 时区之前构造的时间戳值(例如,在 UTC+3 中):
2020-01-01 01:00:00
评论:不保留时间瞬间
如果是,Connector/J 会尝试通过以连接属性和
true
定义的方式执行转换来保留时间点 。connectionTimeZone
forceConnectionTimeZoneToSession
存储值时,仅当目标数据类型(显式数据类型或默认数据类型)为
TIMESTAMP
. 检索值时,仅当源列具有TIMESTAMP
、DATETIME
或字符数据类型并且目标类是即时保留类(如java.sql.Timestamp
或 )时才执行转换java.time.OffsetDateTime
。
connectionTimeZone={LOCAL|SERVER|
: 指定 Connector/J 如何确定服务器的会话时区(时间戳保存到服务器时所参考的时区)。它采用以下值之一:user-defined-time-zone
}LOCAL
: Connector/J 假定服务器的会话时区 (a) 与 Connector/J 的 JVM 时区相同,或者 (b) 应设置为与 Connector/J 的 JVM 时区相同。Connector/J 根据连接属性的值将情况视为 (a) 或 (b)forceConnectionTimeZoneToSession
。服务器:连接器/J 应该从服务器查询会话的时区,而不是对其做任何假设。如果会话时区实际上与Connector/J 的JVM 时区不同 ,则
preserveInstants=true
Connector/J 在会话时区和JVM 时区之间进行时区转换。user-defined-time-zone
: Connector/J 假定服务器的会话时区 (a) 与用户定义的时区相同,或者 (b) 应设置为用户定义的时区。Connector/J 根据连接属性的值将情况视为 (a) 或 (b)forceConnectionTimeZoneToSession
。
笔记对于 Connector/J 8.0.23 及更高版本,
serverTimezone
是connectionTimeZone
. 对于 Connector/J 8.0.22 及更早版本,serverTimezone
用于覆盖服务器上的会话时区设置。forceConnectionTimeZoneToSession={true|false}
: 控制会话time_zone
变量是否设置为 中指定的值connectionTimeZone
。
现在,这里是连接属性值,用于实现上面定义的用于保留时间瞬间的解决方案:
解决方案 1:使用 preserveInstants=false或 connectionTimeZone=LOCAL& forceConnectionTimeZoneToSession=false。因为可以安全地假定服务器会话时区与连接器/J 的 JVM 时区相同,所以不会查询服务器会话时区,也不会发生时区转换。例如:
时区:JVM 和服务器会话的 UTC+1
来自客户端的原始时间戳(UTC+1):
2020-01-01 01:00:00
连接器/J 发送到服务器的时间戳:(
2020-01-01 01:00:00
无需转换)服务器内部存储的时间戳值:(
2020-01-01 00:00:00 UTC
从 UTC+1 内部转换为 UTC 后)时间戳值稍后检索到 Connector/J 连接到的 UTC+1 中的服务器时间会话:(
2020-01-01 01:00:00
在从 UTC 内部转换为 UTC+1 之后)由 Connector/J 在与之前相同的 JVM 时区 (UTC+1) 中构建并返回给应用程序的时间戳值:
2020-01-01 01:00:00
注释:时间瞬间保留,无需转换。
笔记此设置对应于 Connector/J 5.1 的默认行为
解决方案 2a:使用 preserveInstants=true&connectionTimeZone=SERVER 。然后Connector/J向服务器查询会话时区的值,并将事件时间戳在会话时区和JVM时区之间进行转换。例如:
时区:JVM 为 UTC+2,服务器会话为 UTC+1
来自客户端的原始时间戳(UTC+2):
2020-01-01 02:00:00
连接器/J 发送到服务器的时间戳:(
2020-01-01 01:00:00
从 UTC+2 转换为 UTC+1 后)服务器内部存储的时间戳值:(
2020-01-01 00:00:00 UTC
从 UTC+1 内部转换为 UTC 后)稍后检索到 UTC+1 中服务器会话的时间戳值:(
2020-01-01 01:00:00
从 UTC 内部转换为 UTC+1 后)由 Connector/J 在与之前相同的 JVM 时区 (UTC+2) 中构造并返回给应用程序的时间戳值:(
2020-01-01 02:00:00
从 UTC+1 转换为 UTC+2 之后)由 Connector/J 在另一个 JVM 时区(例如 UTC+3)中构造并返回给应用程序的时间戳值:(
2020-01-01 03:00:00
从 UTC+1 转换为 UTC+3 之后)评论:时间瞬间被保留。
笔记此设置对应于 Connector/J 8.0.22 及之前的默认行为以及 Connector/J 5.1 的行为
useLegacyDatetimeCode=false
。
解决方案 2b:使用 connectionTimeZone=LOCAL& forceConnectionTimeZoneToSession=true。Connector/J 然后将服务器的会话时区更改为 JVM 时区的时区,之后在存储或获取时间戳时不需要时区转换。例如:
Time zones: UTC+1 for JVM, UTC+2 for server session originally, but now modified to UTC+1 by Connector/J
来自客户端的原始时间戳(UTC+1):
2020-01-01 01:00:00
连接器/J 发送到服务器的时间戳:(
2020-01-01 01:00:00
无转换)服务器内部存储的时间戳值:(
2020-01-01 00:00:00
从 UTC+1 内部转换为 UTC 后)稍后检索到服务器会话中的时间戳值(UTC+1,由 Connector/J 设置):(
2020-01-01 01:00:00
从 UTC 内部转换为 UTC+1 后)Connector/J构造的时间戳值与之前相同的JVM时区(UTC+1):(
2020-01-01 01:00:00
无需转换)稍后检索到服务器会话中的时间戳值(连接器/J 将时区修改为 UTC+3):(
2020-01-01 03:00:00
在从 UTC 内部转换为 UTC+3 之后)Connector/J在UTC+3的JVM时区构造的时间戳值:(
2020-01-01 03:00:00
无需转换)评论:时间瞬间被保留而没有被 Connector/J 转换,因为会话时区被 Connector/J 更改为其 JVM 的值。
警告更改会话时区会影响 MySQL 函数的结果,例如
NOW()
、CURTIME()
或CURDATE()
— 如果您不希望这些函数受到影响,请不要使用此设置。如果您在不同时区的不同客户端上使用此设置,则客户端会将其连接会话的时区修改为不同的值;如果您想为所有客户端及其所有会话在同一时刻保持相同的视觉日期时间值表示,请将值存储到
DATETIME
而不是TIMESTAMP
列,并为它们使用非即时 Java 类,例如,java.time.LocalDateTime
.
解决方案 2c:使用 preserveInstants=true&connectionTimeZone=
user-defined-time-zone
& forceConnectionTimeZoneToSession=true。Connector/J 然后将服务器的会话时区更改为用户自定义时区,并在用户自定义时区和JVM 时区之间转换时间戳。此设置的典型用例是当已知服务器上的会话时区值无法被 Connector/J(例如,CST
或CEST
)识别时。例如:时区:UTC+2 for JVM,
CET
最初用于服务器会话,但现在Europe/Berlin
由 Connector/J 修改为用户指定来自客户端的原始时间戳(UTC+2):
2020-01-01 02:00:00
Connector/J发送给服务器的时间戳:
2020-01-01 01:00:00
(JVM时区(UTC+2)和用户自定义时区(Europe/Berlin
=UTC+1)转换后)服务器内部存储的时间戳值:(
2020-01-01 00:00:00
从 UTC+1 内部转换为 UTC 后)检索到服务器会话中的时间戳值(
Europe/Berlin
连接器/J 将时区修改为 (=UTC+1)):(2020-01-01 01:00:00
从 UTC 内部转换为 UTC+1 后)Connector/J在与之前相同的JVM时区(UTC+2)构造并返回给应用程序的时间戳值:(
2020-01-01 02:00:00
用户自定义时区(UTC+1)和JVM时区(UTC+2)转换后) .注释:通过转换和会话时区根据用户定义的值由 Connector/J 更改时保留时间。
作为此解决方案的替代方案,用户可能需要如上所述在 JVM 时区和用户定义时区之间进行相同的时间戳转换,而无需实际更正服务器上无法识别的时区值。为此,请使用
preserveInstants=true&connectionTimeZone=user-defined-time-zone& forceConnectionTimeZoneToSession=false
. 这实现了保留时间瞬间的相同结果。警告请参阅上面针对解决方案 2b 的警告。