时间戳、时区、语言环境和服务器

Timestamps, Timezones, Locales, and Servers

我的脑袋开始因时间戳和时区问题而头晕目眩。在我的应用程序中,所有时间戳都保存为 UTC。提交的时区是单独保存的。

环境如下:

我解决的第一个问题是 PostgreSQL 默认时区在旧金山的服务器上设置为 America/Eastern。所以我切换到 UTC,但这只是问题的一部分,我不能 100% 确定它是否做了任何事情。仅供参考,PostgreSQL 中的列是 TIMESTAMP WITH TIME ZONE

测试的本地化日期,即 MST 或 UTC-07:

所有测试插入中使用的 UTC 日期:

第一次测试:---------------------------------------- ----

通过快速 SQL 插入,将输入正确的日期。当我查询时,我得到了本地化日期,因为我的本地系统将它与 MST 相差 -7 小时。

查询后日期:

这确实是我在本地插入的时间

第二次测试:---------------------------------------- ----

这就是奇怪的地方。现在,我使用与 SQL 插入中相同的 UTC 时间戳通过应用程序输入日期。

现在看看我查询出来的结果:

如果使用原始 SQL 插入,日期会减去 7 小时以获得我的本地时间。如果我的本地机器从原始 UTC 时间戳中减去 7,那么应用程序返回的原始 UTC 时间戳将加上 7 小时给我们:

而不是我输入的内容。呵呵。 . . ?

PostgreSQL 似乎已从可疑列表中删除,因为 SQL 插入成功并且时区更改为 UTC。这意味着罪魁祸首可能是以下之一:

我的计划是有条不紊地更改服务器上的所有内容以默认为 UTC 时区。有谁知道这里发生了什么?我计划将所有内容都默认为 UTC 是个好主意吗?

天哪,我找到了解决办法。所以我相信 PostgreSQL JDBC 驱动程序正试图根据 JVM 偏移 UTC 时间戳,JVM 要求 server/OS 作为默认值。如果它不是 JDBC 而不是 Java 本身。由于客户端在一个时区,服务器在另一个时区,日期为 UTC,因此发生了一些非常奇怪的事情,并且输入了错误的时间戳。所以我想我可以将服务器默认时区从 America/NewYork 更改为 UTC .

这篇文章向我展示了如何操作:https://chrisjean.com/change-timezone-in-centos/

只需要运行这两个命令:

备份原本地时间文件:

sudo mv /etc/localtime /etc/localtime.bak

使用正确的时区创建一个新的:

sudo ln -s /usr/share/zoneinfo/UTC /etc/localtime

一周的战斗归结为两个命令:/ 希望这是一个真正的解决方案,其他人可以从中获得一些价值。

让我们一块一块地把它拆开。

我不确定你的问题,但我怀疑你对 Postgres 在控制台会话中呈现 date-time 值的误导行为感到困惑,如下所述。

Postgres 以 UTC 存储

在 Postgres 中,您应该 using the TIMESTAMP WITH TIME ZONE, virtually never TIMESTAMP WITHOUT TIME ZONE. Study the Postgres doc carefully. For the WITH type, Postgres uses any time zone or offset-from-UTC 提供传入数据以将给定的 date-time 调整为 UTC。 Postgres 总是 以 UTC 存储 TIMESTAMP WITH TIME ZONE。传递的时区或偏移量信息在用于调整到 UTC 后被遗忘。此行为可能不同于其他数据库系统;据我所知,SQL 规范没有指定此类细节。

当通过 SQL 控制台查询时,为表示 TIMESTAMP WITH TIME ZONE 中的 date-time 值而生成的文本将应用 SQL 会话的当前时区作为一部分的文本生成。这可能会产生误导,因为它 而不是 意味着存储的数据在那个时区。实际上,TIMESTAMP WITH TIME ZONE 始终以 UTC 格式存储。在您的控制台中进行实验以亲自查看,如下所示。请记住,您在下面看到的所有输出几乎代表同一时刻(我 运行 所有四个快速连续),但它们的 wall-clock time 完全不同。

首先使用为交互式 SQL 会话建立的默认时区。对我来说,在我的开发工作站上 America/Los_Angeles (west coast of United States), with an offset-from-UTC of -08:00.

SELECT now();  -- Or query your `TIMESTAMP WITH TIME ZONE` column.

2016-01-27 12:16:35.681057-08

尝试另一个时区,这个朝加拿大西部的时区,在 Alberta, named America/Edmonton,标准时间偏移 -07:00,夏令时偏移 -06:00

SET TIME ZONE 'America/Edmonton'; -- Offset from UTC: -07:00
SELECT now();  -- Or query your `TIMESTAMP WITH TIME ZONE` column.

2016-01-27 13:17:01.502281-07

尝试在 UTC 之前而不是之后的时区。偏移量是 + 而不是 -。另请注意,印度提前五个 小时。并非所有时区的偏移量都有 full-hour 增量。所以在这个例子中 minute-of-hour 是 47 而不是上面看到的 17

SET TIME ZONE 'Asia/Kolkata';  -- Offset from UTC: +05:30
SELECT now();  -- Or query your `TIMESTAMP WITH TIME ZONE` column.

2016-01-28 01:47:28.95779+05:30

最后,试试UTC。这意味着 +00:00 的偏移量,因为所有其他时区都是根据 UTC 定义的。有时您会看到 ZZulu 的缩写),这也表示 UTC。

SET TIME ZONE 'UTC';
SELECT now();  -- Or query your `TIMESTAMP WITH TIME ZONE` column.

2016-01-27 20:17:50.86757+00

JDBC

在 JDBC 中,您应该在 ResultSet 上调用 getTimestamp 以获得 java.sql.TimestampA java.sql.Timestamp 在 UTC总是

令人恼火且令人困惑的是,它的 toString 方法在生成作为 date-time 值的文本表示的字符串时静默应用 JVM 当前的默认时区。所以它可能 看起来 好像它有时区,但实际上它没有。这些旧的 date-time 类 是 well-intentioned 但做出了一些不幸的设计选择,例如这种行为。尽快转换为 java.time 类型的另一个原因。

java.time

您应该从 java.sql 类型转换为 Java 8 及更高版本中内置的 java.time 类型。然后继续您的业务逻辑。

基本的 java.time 类型是…… Instant is a moment on the timeline in UTC. Get an Instant by calling toInstant on your java.sql.Timestamp. Most of your work should be in UTC, so work often with Instant. Apply a time zone (ZoneId) to get a ZonedDateTime 当您需要在他们的 expected/desired 时区向用户展示时。

明确指定时区

在所有 java.time 操作中,始终指定 desired/expected 时区。如果省略,则静默应用 JVM 当前的默认时区。为什么我说“当前”?因为 JVM 的默认时区可以随时更改,即使在 运行time(!) 期间也是如此。不要隐含地依赖默认值,要明确。 Locale 同上,但那是另一个讨论。

避旧Date-Time类

避免与 Java 的早期版本捆绑在一起的旧 java.util.Date/.Calendar 类。他们是出了名的麻烦。它们已被 java.time 类.

取代

每一块都是 UTC

请注意,从 Postgres 中的 TIMESTAMP WITH TIME ZONE 到 JDBC 到 java.sql.Timestamp 再到 java.time Instant 意味着一切都在 UTC 中。数据库服务器的时区 hardware/OS 无关紧要。 SQL 会话的时区无关紧要。 Java 虚拟机主机服务器 hardware/OS 上的时区无关紧要。 JVM 中的当前默认时区无关紧要。

验证时钟设置是否正确

重要的是您验证数据库服务器和 JVM 主机上的硬件时钟都是正确的,设置为适合其指定时区的正确时间。虽然通常最佳做法是将所有服务器的 OS(以及 Postgres 和服务器 JVM)设置为 UTC,但考虑到上述情况,这不是必需的。

不,JDBC驱动程序应用区域

I believe the PostgreSQL JDBC driver is trying to offset a UTC timestamp based on the JVM which asks the server/OS for it's default.

不,你的信念不正确。 Postgres-provided JDBC driver 不应用 java.sql.Timestamp 的 JVM 当前默认时区。当从存储的数据库 date-time 值中的 UTC 转到同样采用 UTC 的 java.sql.Timestamp 值时,此类应用程序将无关紧要。

使用正确的时区名称

使用proper time zone names。它们以 Continent/Region 的格式出现。切勿使用像 MST 这样的 3-4 字母代码,因为它们既不标准化也不唯一。如果您指的是山地标准时间,请使用类似 America/EdmontonAmerica/Denver.

的内容

互动SQL会话

在我看来,将 JDBC 与 Postgres 一起使用可以简化 date-time 处理。往返:

TIMESTAMP WITH TIME ZONEjava.sql.Timestamp ↔ java.time.Instant

…让一切变得简单、清晰、整洁。每一步都使用 UTC。稍后在您的应用程序中,您可以根据需要调整到时区。

但是来自 Postgres 的其他 date-time 字符串 input/output 呢,例如 pgAdmin 中的交互式 SQL 会话?

如果 date-time 字符串中缺少任何 offset-from-UTC,则 Postgres 在将 SQL 会话的当前默认时区转换为 UTC 进行内部存储的过程中应用。我认为在没有任何偏移量或时区的情况下传递 date-time 字符串是一种不好的做法。

如果您的 date-string 包含一个 offset-from-UTC,那么 Postgres 会使用该信息调整到 UTC 以进行内部存储。

以下示例显示了这两种行为。我调用 SET TIME ZONE 来明确 current default time zone in this SQL session。时区 America/Los_Angles 在一月份的偏移量为 -08:00。您可以方便地在您自己的数据库中 运行 此代码,因为 CREATE TEMP TABLE 中的 TEMP 意味着 table 在 SQL 会话关闭时自动删除。

请注意在第二种情况下,小时是 04 而不是 12。这两个值 而不是 代表时间轴上的同一时刻;这是两个不同的时间点

SET TIME ZONE 'America/Los_Angeles';

DROP TABLE IF EXISTS some_table_ ;

CREATE TEMP TABLE IF NOT EXISTS some_table_  (
    "some_datetime_" TIMESTAMP WITH TIME ZONE NOT NULL
) ;

INSERT INTO some_table_
VALUES ( '2016-01-23 12:34:01' ); -- Lacking offset-from-UTC. *Not recommended*!

INSERT INTO some_table_
VALUES ( '2016-01-23 12:34:02Z' ); -- 'Z' means Zulu = UTC.

TABLE some_table_ ;

2016-01-23 12:34:01-08

2016-01-23 04:34:02-08

非常重要: 请注意,上面的行为是在您的列数据类型为 TIMESTAMP WITH TIME ZONE 时发生的。另一种类型 TIMESTAMP WITHOUT TIME ZONE 应该几乎 never be used 因为它的意思是“不做任何调整,忽略任何偏移量或时区,只取这个日期和这个时间,然后盲目地将它填入数据库”。