LocalDateTime与timestamptz

这份笔记来自对另一份笔记的拆分,这篇笔记偏重于LocalDateTime,为了方便查阅,所以我移动到了另一篇笔记中。

项目目前对对timestamptz的应用

首先对着我们库执行show timezone得到的结果为GMT。GMT表明了我们数据库服务设计的初心:我们中美服务器的数据库都统一使用一个时区。结合我们的实践,现在我产生了如下几个问题:

  1. 向表中插入一条当前系统的时间,然后再去查表中的记录,这个时候的时间是什么呢(我期待的是做过转换的GMT时间)

  2. 当我们的项目链接到数据库时,此时的链接的时区是什么,也是GMT么?

  3. 我们用于接受timestamptz的LocalDateTime类型,有什么特殊的地方么,在插入和查询结果时。

  4. 我们的Java服务实例的时区对整个时间戳有什么影响么,要求我们的Java实例必须设置正确的时区吗?

  5. 到底如何和前端配置,前端的意思是我们可以给一个+00的时间戳,但是我们又使用了LocalDateTime,LocalDateTime在反序列化时貌似自动完成了+08运算。

第一个问题

如下实验:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17

CREATE TABLE timestamp_demo (ts TIMESTAMP, tstz TIMESTAMPTZ);

INSERT INTO timestamp_demo (ts, tstz)
VALUES
 (
 '2021-07-19 11:00:00-00',
 '2021-07-19 11:00:00-00'
 );

INSERT INTO timestamp_demo (ts, tstz)
VALUES
 (
 now(),
 now()
 );

最后查看数据库中的数据:

2021-07-19-11-01-12

我发现当我使用字符串的进行timestamptz类型插入的时候,timestamptz类型的字段没有按照我的现象根据我操作系统当前所在的时区(链接、用户所在的时区),将时间转换成GMT时间。按照MySQL的经验,show timezone或许得到的就是当前链接(即session)的时区,那么这个现象是可以理解的:我在GMT的时区里插入任何数据,都不需要进行转换。

我现在手动切换当前session时区到PRC,我先执行了一次select语句,我发现timestamp的数据没有任何变化,timestamptz类型的数据按照设计完成了+8运算。

2021-07-19-11-18-14

实验到这儿,我才发现了一些问题,我在插入数据的时候,已经带上了时区~~~,秒后面的+00和-00其实就是时区信息,我一直没有意识到这个问题,不过好在该细节对前面我作出来的分析并没有任何影响,我前面的分析都是正确的。此时我将当前链接的时区设置为PRC,然后用如下代码进行插入实验,最终插入数据库的数据确实完成了PRC时区到GMT时区(即UTC时间)转换,非常的开心。

1
2
3
4
5
6
7
8

INSERT INTO timestamp_demo (ts, tstz)
VALUES
 (
 '2021-07-19 11:00:00',
 '2021-07-19 11:00:00'
 );

2021-07-19-11-24-48

发现数据没有像我想象的一样转换成GMT时间。看了上面的资料,我认为可能是因为我用户或者我链接的所在的时区影响了数据库出数据(自动帮我完成了时区的缓存)。

第二个问题

我用如下代码,查看了当前数据库的链接,发现数据库的链接的时区为:Asia/Shanghai。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

List<String> timezones = jdbcTemplate.query(
        "show time zone",
        new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                return rs.getString(1);
            }
        });

System.out.println(timezones);

这个结果是比较出乎我的意料的,因为我们数据库的请求url上并没有配置时区,我现在不知道这个时区数据是从何而来的。最后我查找资料,成功的将SpringBoot实例的时区更换为了GMT,相关笔记我整理在了SpringBoot分类下。

我仅仅只是处于技术探索的目的进行SpringBoot时区的调整,实践中该技术貌似没有太大的需求。

第三个问题

在写入的时候,在我们自定义的typeHandler中,LocalDateTime类型的参数被转换成了Timestamp类型的对象。这个然后这个timestamp对象由pg自己的驱动器了进行处理(我想pg自己的驱动器类应该很清楚知道pg数据库支持的类型,所以可以很好的根据数据库中的类型处理这个timestamp对象,我之所以作出这个判断,是因为我在开发库上做实验,而开发库没有开启sql日志)

在读出数据的时候,在我们自定义的typeHandler中,pg的数据库交给我们一个Timestamp类型的对象,然后我们将这个timestamp对象转换成LocalDateTime对象。整个过程平平无奇,并没有涉及到过于底层的东西(最底层的东西都被PG的驱动器类自己处理了)。

第四个问题

SpringBoot实例所设置的时区,对时间戳影响很大。我们在程序内部是很少使用时间戳概念的,我们使用的都是Date、Time、DateTime之类的概念(我们不会拿时间戳做运算,但是我们会拿DateTime等做运算)。我之前没有意识到这个问题,其实Date、Time、DateTime之类的概念都蕴含了时区的概念,也就是说,我们只能说某某时区的Date、Time和DateTime,而不能脱离时区谈论这些概念。

因此,从这些对象转换成时间戳的时候,运算过程一定是带上了时区的。举个例子,一个时间2021-07-19 16:00:00转换成时间戳,如果单纯的计算秒数,我们实际上是假设了这个时间是零时区的。但是我们目前处于+8时区,所以在计算秒数的时候,需要先将这个时间减去8小时,然后再计算秒数。

SpringBoot实例应该会根据系统当前的时区,来设置整个实例的时区,同时,这个时区会用于与pg数据建立链接(即我们的链接的session和SpringBoot的时区是一致的)。

第五个问题

综上,产生这个问题的原因在我们项目中在讲LocalDateTime转换成时间戳的时传递了错误的ZoneOffset,我们硬编码了一个+8的值。当项目跑在国内的环境中时,这个问题的印象是比较小的,当我们的项目跑在国外的环境中时,这个问题时灾难的,因为此时的LocalDateTime中记录的时区并不是+8时区,但是强行按照了+8时区进行时间戳转换,所以算出来的数据是错误的。

此时如下编码,即可解决这个问题:

1
2
3
4
5
6
7
8

LocalDateTime localDateTime = LocalDateTime.of(2021, 7, 19, 15, 0, 0);

ZoneId systemDefaultZoneId = ZoneId.systemDefault();
ZoneOffset offset = systemDefaultZoneId.getRules().getOffset(localDateTime);

localDateTime.toInstant(offset).toEpochMilli();

这段代码中,我们应用当前系统所在的时区来转换LocalDateTime对象,是符合需求的。