文档主页
MySQL Connector/J 开发人员指南
相关文档 下载此手册
PDF (US Ltr) - 1.2Mb
PDF (A4) - 1.2Mb


6.6.1 保留时间点

背景

时间点是时间线上一个特定的时刻。当一个时间点在存储到或从数据库中检索时,无论数据库服务器和客户端运行在哪个时区,它都始终指向时间轴上的同一个点,则称这个时间点被保留。

TIMESTAMP 是唯一用于存储时间点的 MySQL 数据类型。为了保留时间点,服务器在需要时对传入或传出的时间值应用时区转换。传入的值将从连接会话的时区转换为协调世界时 (UTC) 以供存储,传出的值将从 UTC 转换为会话时区。从 MySQL 8.0.19 开始,您还可以在存储TIMESTAMP 值时指定时区偏移量(有关详细信息,请参阅DATE、DATETIME 和 TIMESTAMP 类型),在这种情况下,TIMESTAMP 值将从指定的偏移量而不是会话时区转换为 UTC。但是,一旦存储,原始偏移量信息将不再保留。

对于 DATETIME 数据类型,情况并不那么简单:它不代表一个时间点,并且当没有指定时区偏移量时,DATETIME 值没有时区转换,因此它们以原样存储和检索。但是,如果指定了时区偏移量,则输入值将在存储之前转换为会话时区;结果是,当在具有与指定的不同的时区偏移量的其他会话中检索时,DATETIME 值将与原始输入值不同。

由于除了 TIMESTAMP 之外的 MySQL 数据类型(以及这些其他 MySQL 数据类型的 Java 包装类)不代表真正的时区点;在存储和检索值时混合使用表示时区点的和非表示时区点的日期时间类型可能会导致意外的结果。例如

  • 当将 java.sql.Timestamp 存储到例如 DATETIME 列时,您可能无法在检索它到与存储值时客户端所在的时区不同的客户端时获取相同的时间点值。

  • 当将例如 java.time.LocalDateTime 存储到 TIMESTAMP 列时,您可能无法为其存储正确基于 UTC 的值,因为值的时区实际上是未定义的。

因此,在使用服务器时,请勿将表示时间点的日期时间类型(java.util.Calendarjava.util.Datejava.time.OffsetDateTimejava.sql.Timestamp)传递给非表示时间点的日期时间类型(例如 java.sql.DATEjava.time.LocalDatejava.time.LocalTimejava.time.OffsetTime),反之亦然。

本节的其余部分讨论了在使用 Connector/J 时如何保留时间点。

使用 Connector/J 保留时间点

场景: 假设一个应用程序正在某个应用程序服务器上运行,并使用 Connector/J 连接到 MySQL 服务器。在连接会话中发生某些事件,为此生成时间戳,并且事件时间戳与应用程序服务器的 JVM 时区相关联。这些时间戳将存储到 MySQL 服务器上,并且稍后也将在其中检索。

挑战: 使用 Connector/J 将时间戳保存到服务器或从服务器检索时,需要保留时间戳的时间点值。由于 MySQL 服务器始终隐式地假定时间点值引用连接会话时区(由会话 time_zone 变量设置),仅在以下情况下才能正确保留时间点值

  1. 当 Connector/J 在与 MySQL 服务器相同的时区运行时(即服务器的会话时区与 JVM 的时区相同),时间点自然会保留,不需要时区转换。请注意,在这种情况下,只有在服务器和 JVM 在将来始终在同一个时区运行时,时间点才会真正保留。

  2. 当 Connector/J 在与 MySQL 服务器不同的时区运行时(即 JVM 的时区与服务器的会话时区不同),Connector/.J 会执行以下操作之一

    1. 从服务器查询会话时区的值,并在会话时区和 JVM 时区之间转换事件时间戳。

    2. 将服务器的会话时区更改为 JVM 的时区,之后将不再需要时区转换。

    3. 将服务器会话时区更改为用户指定的所需时区,然后在 JVM 时区和用户指定的时区之间转换时间戳。

我们将上述时间点保留解决方案标识为解决方案 1、2a、2b 和 2c。为了实现这些解决方案,自 Connector/J 8.0.23 版本以来引入了以下连接属性

  • preserveInstants={true|false}:是否尝试通过调整时间戳来保留时间点值。

    • 当它是 false 时,不会尝试进行转换;时间戳将按原样发送到服务器以供存储,并且会保留其可视表示,而不是实际时间点。当它从服务器被 Connector/J 检索时,它可能与不同的时区相关联,因为检索可能在不同的 JVM 时区中发生。例如:

      • 时区:UTC 用于 JVM,UTC+1 用于服务器会话

      • 来自客户端的原始时间戳(以 UTC 表示):2020-01-01 01:00:00

      • Connector/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 表示):2020-01-01 01:00:00(在将 2020-01-01 00:00:00 从 UTC 内部转换为 UTC+1 后)

      • 然后由 Connector/J 在其他 JVM 时区中构建的时间戳值(例如,在 UTC+3 中):2020-01-01 01:00:00

      • 注释:时间点未保留

    • 当它是 true 时,Connector/J 会尝试通过以连接属性 connectionTimeZoneforceConnectionTimeZoneToSession 定义的方式进行转换来保留时间点。

      存储值时,仅当目标数据类型(显式数据类型或默认数据类型)是 TIMESTAMP 时,才会执行转换。检索值时,仅当源列具有 TIMESTAMPDATETIME 或字符数据类型,并且目标类是保留时间点的类(例如 java.sql.Timestampjava.time.OffsetDateTime)时,才会执行转换。

  • connectionTimeZone={LOCAL|SERVER|user-defined-time-zone}: 指定 Connector/J 如何确定服务器会话时区(以该时区为参考保存时间戳到服务器)。它可以取以下值之一。

    • LOCAL: Connector/J 假设服务器的会话时区要么 (a) 与 Connector/J 的 JVM 时区相同,要么 (b) 应该设置为与 Connector/J 的 JVM 时区相同。Connector/J 根据连接属性 forceConnectionTimeZoneToSession 的值,将情况视为 (a) 或 (b)。

    • SERVER: Connector/J 应该从服务器查询会话时区,而不是对其进行任何假设。如果会话时区实际上与 Connector/J 的 JVM 时区不同,并且 preserveInstants=true,Connector/J 会在会话时区和 JVM 时区之间执行时区转换。

    • user-defined-time-zone: Connector/J 假设服务器的会话时区要么 (a) 与用户定义的时区相同,要么 (b) 应该设置为用户定义的时区。Connector/J 根据连接属性 forceConnectionTimeZoneToSession 的值,将情况视为 (a) 或 (b)。

    注意

    对于 Connector/J 8.0.23 及更高版本,serverTimezoneconnectionTimeZone 的别名。对于 Connector/J 8.0.22 及更早版本,serverTimezone 用于覆盖服务器上的会话时区设置。

  • forceConnectionTimeZoneToSession={true|false}: 控制是否将会话 time_zone 变量设置为 connectionTimeZone 中指定的值。

现在,以下是用于实现上述保留时间瞬间解决方案的连接属性值。

  • 解决方案 1:使用 preserveInstants=falseconnectionTimeZone=LOCAL& forceConnectionTimeZoneToSession=false。因为可以安全地假设服务器会话时区与 Connector/J 的 JVM 时区相同,所以不会发生服务器会话时区的查询,也不会发生时区转换。例如

    • 时区:JVM 和服务器会话均为 UTC+1

    • 来自客户端的原始时间戳(UTC+1):2020-01-01 01:00:00

    • Connector/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

    • Connector/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 时区,之后在存储或获取时间戳时无需进行时区转换。例如

    • 时区:JVM 为 UTC+1,服务器会话最初为 UTC+2,但现在由 Connector/J 修改为 UTC+1

    • 来自客户端的原始时间戳(UTC+1):2020-01-01 01:00:00

    • Connector/J 发送到服务器的时间戳:2020-01-01 01:00:00(不转换)

    • 服务器内部存储的时间戳值:2020-01-01 00:00:00(在将 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 修改为 UTC+3):2020-01-01 03:00:00(在将 UTC 内部转换为 UTC+3 后)

    • Connector/J 在 JVM 时区 UTC+3 中构造的时间戳值: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 识别为不可识别时(例如 CSTCEST)。例如

    • 时区:JVM 为 UTC+2,服务器会话最初为 CET,但现在由 Connector/J 修改为用户指定的 Europe/Berlin

    • 来自客户端的原始时间戳(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 后)

    • 检索到服务器会话中的时间戳值(由 Connector/J 修改为 Europe/Berlin(=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 的警告。