之前遇到过一个由于标准时间/夏令时时间转换引起的问题,这里记录下来分享给大家。

大家都知道,地球上按照经度分成24个时区,每个时区相差一个小时。一般来说每个国家法定的时间都对应一个时区,比如中国用的东八区时间,韩国用的东九区时间,韩国时间比中国快一个小时。同时,很多高纬度国家都实行夏令时,即每到夏天把时钟拨快一个小时,每到冬天再把时钟拨慢一个小时,比如德国。

下图中可以看出很多国家现在或者曾经都实行过夏令时,蓝色表示正在实行,灰色表示曾经实行过,具体可查看wikipedia。

问题的原型是:在Postgres数据库中有一张表,表的定义如下:

CREATE TABLE public.datetimetest (
  createdat timestamp NULL,
  id varchar NULL
);

首先通过DBeaver执行如下语句向表中插入三条记录:

--001--
INSERT INTO public.datetimetest (id, createdat) VALUES('001', '1989-04-16 00:02:00.78');
--002--
INSERT INTO public.datetimetest (id, createdat) VALUES('002', '1989-04-16 00:50:00.450');
--003--
INSERT INTO public.datetimetest (id, createdat) VALUES('003', '1989-04-16 01:00:00.32');

然后执行如下语句查询:

--query--
select id, createdat, createdat::text as createdatstr from public.datetimetest;

得到的结果如下:

可以看到,记录001和002的createdat字段从00:02和00:50变成了01:02和01:50,这两条记录时间往后加了一个小时。但是,记录003却保持没变,和插入的时间是一致的。同时,可以看到,把createdat字段转成text类型,输出的值和插入的时间是一致的。

这是为什么呢?

开始还以为跟电脑或者数据库客户端IDE有关,后面发现在其他人电脑上有同样的问题。并且,用下面这段Java程序处理这个时间,出现了同样的问题,打印出来的时间也往后加了一个小时:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
Date date = sdf.parse("1989-04-16 00:02:00.78");
System.out.println(date);


Sun Apr 16 01:02:00 CDT 1989 //打印结果,加了一个小时,timezone变成了CDT,中国的标准时间是CST

后来mina同学发现,这个问题是标准时间/夏令时转换引起的。

那么什么是夏令时呢?夏令时英文全称Daylight Saving Time,缩写为dst,在上世纪初,为了节约能源而出现的计时方式,即每到夏天,把时钟拨快一个小时,每到冬天再把时钟拨慢一个小时,这种夏令时制在很多高纬度国家都在使用,比如德国,他们的timezone是Europe/Berlin,每到夏天他们会把时钟拨快一个小时,采用东二区的时间(CEST:Central European Summer Time ),夏令时结束,又把时钟拨慢一个小时,采用东一区时间(CET:Central European Time)。一个实际的例子就是:喜欢看球赛的同学可能有注意到,欧冠比赛的比赛时间在夏天是北京时间凌晨2:45am,冬天则是3:45am,其实都是当地时间8:45pm开始。

现在很多人可能不知道,其实中国曾经也实行过夏令时(86年至91年),后来由于各种原因就取消了,全年采用统一的时间。关于为什么取消,网上有很多此方面的讨论。

现在计算机里面已经能够自动处理标准时间/夏令时的转换。比如用下面这段Java程序打印出当前时间:

System.out.println(new Date());

如果当前时间正处于标准时间/夏令时转换,程序会自动把当前时间加一个小时。

回到上面问题,我们把这个时间转成Java Date对象:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
Date date = sdf.parse("1989-04-16 00:02:00.78");
System.out.println(date);

Sun Apr 16 01:02:00 CDT 1989 //打印结果,加了一个小时,timezone变成了CDT,中国的标准时间是CST

由于这个时间刚好是执行标准时间/夏令时转换的那一个小时,在转成Java Date对象时,jdk认为这是一个标准时间,需要自动加一个小时,变成夏令时(类似于人为拨快一个小时动作)。

而其他时间没有这个问题,是因为jdk认为传入的时间就是一个夏令时时间,直接应用夏令时。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
Date date = sdf.parse("1989-04-16 01:00:00.32");
System.out.println(date);

Sun Apr 16 01:00:00 CDT 1989 //打印结果

这就是为什么发现有问题的时间都是在标准时间/夏令时转换的这一个小时。尽管中国已经不实行夏令时了,jdk还是能够自动处理曾经实行过的这一段时间。

**最后,当我们在处理时间时,如有必要,一定要把timezone信息存上,之前就遇到过由于压缩文件的时间戳不带timezone引起的问题,参见另一篇文章关于时间的那些事 - 文件的时间戳。 ** **另外,由于夏令时的存在,程序在处理某些时间时,可能会把标准时间转成夏令时时间(反之亦然),导致意想不到的结果。 **

在研究过程中发现一个奇怪的现象:目前从各种资料上看到,中国实行夏令时的那几年(86-91年),标准时间到夏令时时间转换发生在凌晨2am,但是程序测试结果来看,转换是在0am,无论是java还是javascript,发现都是0am。

Java代码:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
Date date = sdf.parse("1989-04-16 00:02:00.78");
System.out.println(date);

Sun Apr 16 01:02:00 CDT 1989 //打印结果可以看到,在夏令时转换的这一个小时,凌晨0点过的时间自动加了一个小时,变成了一点过.

Javascript代码:

new Date("1989-04-16 00:02:00.78");
Sun Apr 16 1989 01:02:00 GMT+0900 (China Daylight Time)

同理,德国的夏令时转换也是凌晨2am,但是程序处理这个转换就是两点,和网上资料显示时一致的。

TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("Europe/Berlin")));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
Date date = sdf.parse("2019-03-31 02:02:00.78");
System.out.println(date);

Sun Mar 31 03:02:00 CEST 2019 //打印结果可以看到,在夏令时转换的这一个小时,凌晨两点过的时间自动加了一个小时,变成了三点过.

有童鞋知道这个现象吗?欢迎分享。

Rerefences

  • https://www.timeanddate.com/time/change/china/beijing?year=1986

  • 封面图片:https://www.starmilling.com/blog/whats-up-with-daylight-saving-time/