时间戳、时区、语言环境和服务器
Timestamps, Timezones, Locales, and Servers
我的脑袋开始因时间戳和时区问题而头晕目眩。在我的应用程序中,所有时间戳都保存为 UTC。提交的时区是单独保存的。
环境如下:
- OS:旧金山 Digital Ocean Droplet
上的美分OS
- 应用服务器:Apache Tomcat
- 后端:Java -> 休眠 -> Spring
- 数据库:PostgreSQL
我解决的第一个问题是 PostgreSQL 默认时区在旧金山的服务器上设置为 America/Eastern
。所以我切换到 UTC,但这只是问题的一部分,我不能 100% 确定它是否做了任何事情。仅供参考,PostgreSQL 中的列是 TIMESTAMP WITH TIME ZONE
。
测试的本地化日期,即 MST 或 UTC-07:
- "2016-01-20 13:45:19.894 MST"
所有测试插入中使用的 UTC 日期:
- "2016-01-20 20:45:19.894Z"
第一次测试:---------------------------------------- ----
通过快速 SQL 插入,将输入正确的日期。当我查询时,我得到了本地化日期,因为我的本地系统将它与 MST 相差 -7 小时。
查询后日期:
- "2016-01-20 13:45:19.894"
这确实是我在本地插入的时间
第二次测试:---------------------------------------- ----
这就是奇怪的地方。现在,我使用与 SQL 插入中相同的 UTC 时间戳通过应用程序输入日期。
现在看看我查询出来的结果:
- "2016-01-20 18:45:19.0"
如果使用原始 SQL 插入,日期会减去 7 小时以获得我的本地时间。如果我的本地机器从原始 UTC 时间戳中减去 7,那么应用程序返回的原始 UTC 时间戳将加上 7 小时给我们:
- "2016-01-21 01:45:19.0"
而不是我输入的内容。呵呵。 . . ?
PostgreSQL 似乎已从可疑列表中删除,因为 SQL 插入成功并且时区更改为 UTC。这意味着罪魁祸首可能是以下之一:
- Java
- Tomcat
- 分OS
我的计划是有条不紊地更改服务器上的所有内容以默认为 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 定义的。有时您会看到 Z
(Zulu
的缩写),这也表示 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.Timestamp
。 A 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/Edmonton
或 America/Denver
.
的内容
互动SQL会话
在我看来,将 JDBC 与 Postgres 一起使用可以简化 date-time 处理。往返:
TIMESTAMP WITH TIME ZONE
↔ java.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 因为它的意思是“不做任何调整,忽略任何偏移量或时区,只取这个日期和这个时间,然后盲目地将它填入数据库”。
我的脑袋开始因时间戳和时区问题而头晕目眩。在我的应用程序中,所有时间戳都保存为 UTC。提交的时区是单独保存的。
环境如下:
- OS:旧金山 Digital Ocean Droplet 上的美分OS
- 应用服务器:Apache Tomcat
- 后端:Java -> 休眠 -> Spring
- 数据库:PostgreSQL
我解决的第一个问题是 PostgreSQL 默认时区在旧金山的服务器上设置为 America/Eastern
。所以我切换到 UTC,但这只是问题的一部分,我不能 100% 确定它是否做了任何事情。仅供参考,PostgreSQL 中的列是 TIMESTAMP WITH TIME ZONE
。
测试的本地化日期,即 MST 或 UTC-07:
- "2016-01-20 13:45:19.894 MST"
所有测试插入中使用的 UTC 日期:
- "2016-01-20 20:45:19.894Z"
第一次测试:---------------------------------------- ----
通过快速 SQL 插入,将输入正确的日期。当我查询时,我得到了本地化日期,因为我的本地系统将它与 MST 相差 -7 小时。
查询后日期:
- "2016-01-20 13:45:19.894"
这确实是我在本地插入的时间
第二次测试:---------------------------------------- ----
这就是奇怪的地方。现在,我使用与 SQL 插入中相同的 UTC 时间戳通过应用程序输入日期。
现在看看我查询出来的结果:
- "2016-01-20 18:45:19.0"
如果使用原始 SQL 插入,日期会减去 7 小时以获得我的本地时间。如果我的本地机器从原始 UTC 时间戳中减去 7,那么应用程序返回的原始 UTC 时间戳将加上 7 小时给我们:
- "2016-01-21 01:45:19.0"
而不是我输入的内容。呵呵。 . . ?
PostgreSQL 似乎已从可疑列表中删除,因为 SQL 插入成功并且时区更改为 UTC。这意味着罪魁祸首可能是以下之一:
- Java
- Tomcat
- 分OS
我的计划是有条不紊地更改服务器上的所有内容以默认为 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 定义的。有时您会看到 Z
(Zulu
的缩写),这也表示 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.Timestamp
。 A 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/Edmonton
或 America/Denver
.
互动SQL会话
在我看来,将 JDBC 与 Postgres 一起使用可以简化 date-time 处理。往返:
TIMESTAMP WITH TIME ZONE
↔java.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 因为它的意思是“不做任何调整,忽略任何偏移量或时区,只取这个日期和这个时间,然后盲目地将它填入数据库”。