当前位置: 首页 > Linux

别再踩时区的坑了!

时间:2023-04-06 11:40:41 Linux

原创:编码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。简介最近在使用date命令的时候,发现东8区(中国时区)应该使用GMT-8,但是在Java中应该使用GMT+8,如下:$TZ='GMT-8'date-d@1647658144+'%F%T%:z'2022-03-1910:49:04+08:00#如果使用GMT+8,会慢16小时。$TZ='GMT+8'date-d@1647658144+'%F%T%:z'2022-03-1818:49:04-08:00在Java中,如下:DateTimeFormatterdtf=DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ssXXX");StringdateStr=dtf.format(Instant.ofEpochSecond(1647658144).atZone(ZoneId.of("GMT+8")));System.out.println(dateStr);//输出2022-03-1910:49:04+08:00这让人有点疑惑。找了一会儿,发现时区的表达形式还是有很多知识点的!时区的偏移表示是众所周知的。为了方便各地区当地时间之间的转换,人们将世界划分为24个时区。以格林威治天文台(GMT)为零时区,东西方向共有12个时区。所以自然而然就有了以GMT为前缀的时区表示法,如下:GMT+8表示东8区,中国使用这个时区,GMT-8表示西8区,如果格林威治天文台的当地时间是2022年-03-190:00,则GMT+8地区当地时间为2022-03-198:00,GMT-8地区时间提前8小时,即2022年16:00-03-18。注意,虽然上面各个地区当地时间的表达方式不同,但实际上是同一时刻(绝对时间),需要理解当地时间和绝对时间的区别。GMT+8是Java支持的时区表示法,那么Linux为什么是GMT-8呢?其实Linux中的GMT-8也可以写成Etc/GMT-8,这是它的标准名称,如下:$TZ='Etc/GMT-8'date-d@1647658144-Is2022-03-19T10:49:04+08:00DateTimeFormatterdtf=DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ssXXX");StringdateStr=dtf.format(Instant.ofEpochSecond(1647658144).atZone(ZoneId.of("等/GMT-8")));System.out.println(dateStr);//输出2022-03-1910:49:04+08:00。可以发现,如果使用Etc/GMT-8,Linux和Java的输出都是一样的,没错,Etc/GMT-8也是一种类似于GMT+8的时区表示机制,只不过它的+-符号相反。好了,上面的区别虽然说清楚了,但是时区的表示还没有介绍,再往下看……除了GMT+8的表示,我们还经常看到UTC+8的表示。是UTC时区表示法。瞬间生成GMT并生成UTC?这是因为GMT是以格林威治天文台为时间基准,但地球并不是一个完美的球体,自转速度在变慢,所以地球的自转速度不均匀,导致使用格林威治时间不准确天文台作为时间参考。为了更准确地测量时间,科学家们发明了UTC时间,通过铯原子的跃迁次数来测量时间,比GMT时间更准确。为了确保GMT的准确性,GMT时间每隔几年就会调整一次,以与UTC时间对齐。所以,既然有更准确的UTC,就有了以UTC为前缀的时区表示法,比如UTC+8代表中国时区。各时区的偏移量表示法列表如下:偏移量表示法描述GMT+8比GMT长8小时Etc/GMT-8与GMT+8相同,+-号相反为UTC+8同GMT+8GMT+08:00精确到分钟级GMT+08:00:00精确到秒级GMT+0800精确到分钟级,省略冒号GMT+080000精确到秒级别,省略冒号+08:00精确到分钟级别,省略前缀+08:00:00精确到秒级别,省略前缀+0800精确到分钟级别,省略前缀和冒号+080000精确到秒级,省略前缀和冒号Z表示零时区,相当于GMT、UTC、GMT+0、UTC+0除了用偏移量表示时区外,为方便起见,时区还按地区/城市定义了时区。比如Asia/Shanghai和Asia/Hong_Kong都代表东8区。哪些城市以时区命名?,可以在时区数据库中查看。此外,为了简化区域时区的表示,定义了一套时区缩写。例如,CST是中国时区ChinaStandardTime的缩写。您可以在时区缩写中查看各种缩写的定义。注意,一般不建议使用时区缩写,因为时区缩写的名称经常重复。例如,CST为中部标准时间(UTC-6,北美中部标准时间)、中国标准时间(UTC+8,中国标准时间)、古巴标准时间(CubaStandardTimeUTC-5)。因为不同的软件对CST的解读可能不同,可能会有13或14小时的时差。当Java与MySQL一起使用时,通常会发生这种情况。我还专门写了一篇文章。mysqltimestamp有没有时区问题?,对于必须使用时区缩写的场景,可以使用香港时区缩写HKT,不重复,与上海处于同一时区。区域符号描述Asia/Shanghai上海时区,是东8区CST时区的简称。谨慎使用Java来表示时区。Java中与时区相关的类有TimeZone和ZoneId。TimeZone是旧的时区类,而ZoneId是新的。时区类,它有两个子类ZoneOffset和ZoneRegion,分别表示偏移量表示法和时区表示法。上面提到的时区写法都支持哪几种?写个demo验证一下,如下:publicstaticvoidmain(String[]args){printZoneId("+08:00");printZoneId("+0800");printZoneId("GMT+8");printZoneId("等/GMT-8");printZoneId("UTC+8");printZoneId("亚洲/上海");printZoneId("CST");printZoneId("Z");}publicstaticvoidprintZoneId(Stringzone){ZoneIdzoneId;if(!ZoneId.SHORT_IDS.containsKey(zone)){zoneId=ZoneId.of(zone);}else{zoneId=ZoneId.of(ZoneId.SHORT_IDS.get(zone));}TimeZonetimeZone=TimeZone.getTimeZone(zone);ZoneOffsetzoneOffset=zoneId.getRules().getOffset(Instant.now());DateTimeFormatterdtf=DateTimeFormatter.ofPattern("xxxZZZOOOOO");System.out.printf("%-14s->%-28s->class:%s->TimeZone.offset:%d\n",zone,dtf.format(zoneOffset),zoneId.getClass().getSimpleName(),timeZone.getRawOffset());}输出如下:+08:00->+08:00+0800GMT+8GMT+08:00->class:ZoneOffset->TimeZone.offset:0+0800->+08:00+0800GMT+8GMT+08:00->class:ZoneOffset->TimeZone.offset:0GMT+8->+08:00+0800GMT+8GMT+08:00->class:ZoneRegion->TimeZone.offset:28800000Etc/GMT-8->+08:00+0800GMT+8GMT+08:00->class:ZoneRegion->TimeZone.offset:28800000UTC+8->+08:00+0800GMT+8GMT+08:00->class:ZoneRegion->TimeZone.offset:0Asia/Shanghai->+08:00+0800GMT+8GMT+08:00->class:ZoneRegion->TimeZone.offset:28800000CST->-05:00-0500GMT-5GMT-05:00->class:ZoneRegion->TimeZone.offset:-21600000Z->+00:00+0000GMTGMT->class:ZoneOffset->TimeZone。offset:0TimezonewritingZoneIdTimeZone+08:00supportsnotsupports+0800supports不支持GMT+8supportsEtc/GMT-8supportssupportssupportsUTC+8supportsnotsupportAsia/ShanghaisupportssupportssupportsCSTsupports,代表北美西部Time,notChinaStandardTime支持,代表北美西部时间,非中国标准时间Z支持偏移数量表示法和地区表示法的区别虽然偏移量表示法和地区表示法都可以表示时区,但由于夏令时的存在,它们并不完全等同于夏令时(DaylightSavingTime:DST),也叫夏令时时间,是指为了节约能源,在黎明早的夏季,人为将时间调整一小时,以充分利用光资源,节约照明用电。而中国也从1986年到1991年实行夏令时。从1986年到1991年,每年4月中旬的第一个星期日凌晨2点(北京时间)起,时钟拨快一小时,即指针从凌晨2:00移动到凌晨2:00。3:00,夏令时开始;9月中旬第一个星期日凌晨2:00(北京夏令时),将时钟拨回一小时,即指针从2:00拨回1:00,夏令时结束。1986年至1991年这6年中,除1986年是实行夏令时的第一年,即5月4日至9月14日外,其他年份均按规定时间段执行。因此,下面的现象看起来有点奇怪:DateTimeFormatterdtf=DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ssVV");Instantinstant=Instant.ofEpochSecond(515527200);System.out.println(dtf.format(instant.atZone(ZoneId.of("Asia/Shanghai"))));//输出1986-05-0403:00:00Asia/ShanghaiSystem.out.println(dtf.format(instant.atZone(ZoneId.of("GMT+8"))));//输出1986-05-0402:00:00GMT+08:00为什么Asia/Shanghai的输出是3点,而GMT+8的输出是2点??原因是1986-05-0402:00:00,中国开始实行夏令时,时钟向前拨了1小时。为什么GMT+8输出2点?因为中国、马来西亚、菲律宾、新加坡的时区都是GMT+8,而只有中国在执行夏令时,而GMT+8无法感知区域信息,那么java只能计算本地时间而不执行夏令时是时候了。夏令时引起的奇怪现象正是由于夏令时的存在,可能会导致程序出现奇怪现象甚至bug,如下:由于夏令时会在2点钟变为3点钟,2点会消失,所以date命令报错$TZ='Asia/Shanghai'date-d1986-05-04T02:00:00+%sdate:invaliddate'1986-05-04T02:00:00'$TZ='Asia/Shanghai'date-d1986-05-04T03:00:00+%s515527200解析时间后格式化输出,发现不一样DateTimeFormatterdtf=DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ssVV");ZonedDateTimetime1=ZonedDateTime.parse("1986-05-0402:00:00Asia/Shanghai",dtf);System.out.println(time1.format(dtf));//输出1986-05-0403:00:00Asia/Shanghai时间加1小时,发现2小时还是完全没有变化publicstaticvoidmain(String[]args){DateTimeFormatterdtf=DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ssVV");//加1夏令时开始时刚开始的小时ZonedDateTimetime1=ZonedDateTime.parse("1986-05-0401:00:00Asia/Shanghai",dtf);printZonedDateTime(time1);printZonedDateTime(time1.plusHours(1));//加1夏令时结束时刚好结束ZonedDateTimetime2=ZonedDateTime.parse("1986-09-1401:00:00Asia/Shanghai",dtf);printZonedDateTime(time2);printZonedDateTime(time2.plusHours(1));}privatestaticvoidprintZonedDateTime(ZonedDateTimetime){DateTimeFormatterdtf=DateTimeFormatter.ofPattern("yyyy-MM-ddHH:mm:ssVV");System.out.println(time.format(dtf));}输出如下:1986-05-0401:00:00Asia/Shanghai1986-05-0403:00:00Asia/Shanghai//Add1hour,结果好像加了2Hour1986-09-1401:00:00Asia/Shanghai1986-09-1401:00:00Asia/Shanghai//加了1小时,但是时间好像没有变化。为什么会这样?原因是虽然本地时间好像没变,但是Asia/Shanghai代表的时区变了我们可以把上面printZonedDateTime中的时间格式从yyyy-MM-ddHH:mm:ssVV改成yyyy-MM-ddHH:mm:ssVVxxx再执行一次。输出如下:1986-05-0401:00:00Asia/Shanghai+08:001986-05-0403:00:00Asia/Shanghai+09:001986-09-1401:00:00Asia/Shanghai+09:001986-09-1401:00:00Asia/Shanghai+08:00如上,Asia/Shanghai的时区不一定是东八区,也不一定是东九区,因为夏令时。因此,在Java中,如果要将ZoneRegion转换为ZoneOffset,需要传递一个即时时间参数,如下:://output+08:00Instantinstant=Instant.now();System.out.println(ZoneId.of("Asia/Shanghai").getRules().getOffset(instant));//输出+09:00,夏令时1986-05-0402:00:00+08:00,增加1小时Instantinstant=Instant.ofEpochSecond(515527200);System.out.println(ZoneId.of("亚洲/上海").getRules().getOffset(instant));夏令时真是一种自欺欺人的做法。幸运的是,中国自1991年以来就没有实施过!