欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

如何在服务中正确设置时区?

最编程 2024-01-23 07:03:28
...

遇到的问题:

A服务请求B服务的接口,接口参数包含一个日期参数time,time为时间戳,在java中为long类型。至于这个time参数,A服务是读库拿到一个datetime类型的字段,然后转成时间戳再请求B接口。

B服务把这个时间戳转为Date对象再格式化为yyyy-MM-dd HH:mm:ss后,却发现这个时间和数据库中对应时间差了8个小时。

背景知识:

要解决这个问题,我们需要对时区的概念有一些了解,同时知道时间戳和格式化的字符串的区别。

时区:

因为地球是自西向东转,不同地区的人看到日出的时刻是不一样的,为了沟通方便,如大家说吃早饭的时间,那都指代的是9点左右这个时候(虽然从宇宙的角度来说,这些9点不是同一时刻),地球被分为24个时区,相邻两个时区之间差一个小时。以英国的格林威治天文台为原点,划分了东12区和西12区,我国横跨了5个时区,但这5个时区都是用北京时间,而北京处于东8区。

关于时区的更多信息,可以参考李永乐老师的科普视频,或者某位仁兄的以下两篇文章:

各种时区概念

java中的时间处理

时间戳:

  • 时间戳是一个数值,在java中可以通过Date对象的getTime方法来获得,类型为long, 它表示的是自格林威治时间( GMT)1970年1月1日0点到现在所经过的毫秒数。

  • 对一个特定时刻来说,全球的任何地方的时间戳都是一样的。

时间字符串:

  1. 不带时区信息的字符串,比如2021-07-25 10:00:00,这个时间其实不是个绝对时间,它不能指定一个特定时刻。想象自己站在高于地球的宇宙上空,在一个时刻看向地球,这时候北京的某位同学看到自己的表是2021-07-25 10:00:00,但其实日本东京的同学看到自己的表已经是2021-07-25 11:00:00了。

  2. 带有时区信息的字符串,如Fri Jul 23 00:26:54 GMT+08:00 2021, 其中的GMT+8:00就表明了这个东八区的2021-07-23 00:26:54,这个时间字符串就可以定位到一个确定的时刻,虽然这个时刻在其他时区的表示方式可能就不是2021-07-23 00:26:54了。

服务链路中的时间处理:

在分析中,关于数据库中的datetime和java中的Date,我们可以简单化理解为:

  • 数据库datetime字段,它存储的是一个格式为yyyy-MM-dd HH:mm:ss的字符串。
  • Date对象内部是用fastTime字段存了一个long类型的时间戳

所以,归根结底,我们需要分析的是在不同的节点,时间戳和时间字符串是如何转换的。

当出现时间差值的问题时,比如我们开头提到的例子时,我们该从何排查起呢?

首先我们需要了解,在我们的服务链路中,都有哪些地方可以设置时区:

  1. 数据库可设置时区
  2. 服务本身可设置时区,如不设置,取服务器时区(这个时间会影响服务中时间戳如何转为时间字符串)
  3. 服务与数据库连接可设置时区,如不设置,取数据库时区(这个时间会影响数据库中的datetime时间字符串如何转为时间戳)

下图是服务链路时间设置的梳理,这是一个非常典型的场景:

ABC.png

数据库时区设置为东8区,C服务负责写入数据,C本身时区设置为东8区,并且C的数据库连接也为东8区,C写入一个时间到数据库datetime字段,如2021-07-25 10:00:00,这个10点表示的是东8区的10点(可以理解为和时区相关的相对时间),它对应的时间戳(可以理解为绝对时间)为1627178400000。

A从库里取数据时,和java服务不同,它从数据库中拿出的datetime字段是一个字符串2021-07-25 10:00:00(没有mybatis这样的ORM框架去转化),然后把这个字符串再转为时间戳,但因为它的时区为GMT, 所以它把2021-07-25 10:00:00当做GMT时间去转化的,最终的时间戳就是1627178400000+8个小时的毫秒数。

然后A再用这个错误的时间戳去请求B服务,就出现了8个小时的差值。

补充实验:

  1. 数据库连接设置serverTimezone和服务本身JVM参数-Duser.timezone对服务从数据库读的时间的影响

    实验设计,分别改变这两个值,观察从数据库中datetime字段解析出的Date对象,结果如下:

Date转换.png

从上图中可以看出,在数据库连接serverTimezone时区设置不变的情况下,解析出的时间戳都是一样的,所以serverTimezone反映的是从datetime怎样转化为毫秒数的规则。

在同一个时间戳,-Duser.timezone设置不同的情况下,Date格式后的时间字符串会不同,表中的字符串带了时区信息,但如果我们用yyyy-MM-dd HH:mm:ss来解析的话,同一时间戳格式化的字符串会不同,相当于图中的时间字符串去掉时区信息。-Duser.timezone反应的是服务中时间戳如何转换为时间字符串的规则。

  1. 数据库timezone设置的影响

在数据库中,我们最常使用的时间格式就是datetime和timestamp了,除了占用空间和表示范围的不同,他们还有一点不同是datetime的显示和时区无关,timestamp的显示时区有关,这是什么意思呢?

在数据库中datetime就是一个字符串2021-07-25 10:00:00,是没有时区信息的,就说了是这天的10点,没说哪个时区,所以到哪都一样,但timestamp在存储的时候,mysql会把它转化为UTC时间(可以理解为绝对时间,同时间戳的概念),在展示的时候在根据数据库的时区设置,来展示具体的时区相关的年月日时分秒。

假如一个数据库,最开始的时区设置是time_zone=+8:00,我们建一个测试表结构如下:

CREATE TABLE `test_time`
(
    `id`              bigint(20)                              NOT NULL AUTO_INCREMENT COMMENT '自增id',
    `createTime`      datetime                                NOT NULL COMMENT '创建时间',
    `updateTime`      timestamp                               NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='时间测试';

creatTime为datetime, updateTime为timestamp,插入一条记录后,我们看到的是:

记录2.jpeg

接下来,我们把数据的时区设置为东9区

set time_zone = '+9:00';

我们看到这条记录的展示如下,其中creatTIime没有变,但updateTime展示为21:39:04,这是按照东9区来展示的

记录1.jpeg

结论和建议:

1.正确设置自己服务的时区,以及数据库连接的时区

2.在和其他服务交互时,尽量不要使用yyyy-MM-dd HH:mm:ss这种字符串,因为不确定其他服务是不是和自己服务设置的时区一样,最好使用绝对时间:long类型的时间戳

推荐阅读