全球化应用程序时区问题探索与实践

所属分类:Csbase | 发布于 2023-12-11 09:28:35

对于面向全球用户的App来说,时区是个绕不开的话题。这篇来探索时区问题以及给出解决方案。

时区

全球化贸易的推动,各地使用各自的当地太阳时间带来了时间不统一的问题,在19世纪催生了统一时间标准的需求,时区由此诞生。

从格林威治本初子午线起,经度每向东或者向西间隔15°,就划分一个时区,在这个区域内,大家使用同样的标准时间。

全球共分为24个标准时区,相邻时区的时间相差一个小时。(东、西各12个时区)。规定英国格林威治天文台为中时区(零时区)、东1-12区,西1-12区。每个时区横跨经度15度,时间正好是1小时。

凡向西走,每过一个时区,时间要慢一个小时,就要把表拨慢1小时(就是说你所在的位置是两点,向西一个时区就减去一个小时,也就是一点);凡向东走,每过一个时区,时间要快一个小时,就要把表拨快1小时(比如1点拨到2点)。

比如北京时间(东八区)19:00,在这个时刻,位于东七区的曼谷的时间就是18:00。

实际上,为了照顾到行政上的方便,常将1个国家或1个省份划在一起。所以时区并不严格按南北直线来划分,而是按自然条件来划分。由于目前国际上并没有一个批准各国更改时区的机构。一些国家会由于特定原因改变自己的时区。比如中国地大物博横跨多个时区,但是都以东八区时间为准,这就是为什么北京早上的八天太阳出来了,但新疆还是在黑夜的缘故。但是也有例外,美国就按照区域不同,分别使用了4个时区时间。

GMT格林尼治标准时间

GMT,即Greenwich Mean Time,格林尼治标准时间(格林尼治所在地的标准时间)。

在英国伦敦,那里有一条世界上著名的线,叫本初子午线,是人类世界计算时间的起点(时区的划分)以及经度的起点。而这条线的划定是由格林尼治天文台确定的,因此格林尼治天文台所在的地方叫零时区。零时区表示为GMT+00,零时区缩写叫z。

以格林尼治天文台所在的时区为中心(GMT+00),向东为正,向西为负;零时区比东时区晚,比西时区早。

北京所在的时区叫东八区,东八区表示形式是:GMT+08。0时区比东八区的时间晚8小时,比西五区的时间早5小时。美国华盛顿比北京慢13小时。

UTC协调世界时

UTC(Coodinated Universal Time),协调世界时,又称世界统一时间、世界标准时间、国际协调时间。

UTC时间是经过平均太阳时(以格林威治时间GMT为准)、地轴运动修正后的新时标以及以秒为单位的国际原子时所综合精算而成,计算过程相当严谨精密。

一般来说,当我们提到UTC时间而不带任何别的修饰时,常指UTC 0点(UTC+0)。

协调世界时(UTC)不与任何地区位置相关,也不代表此刻某地的时间,所以在说明某地时间时要加上时区。比如说UTC + 8 = 北京时间。

也就是说GMT并不等于UTC,而是等于UTC+0,只是格林威治刚好在0时区上,所以GMT = UTC+0。

GMT VS UTC

GMT是前世界标准时,UTC是现世界标准时。

UTC比GMT更精准,以原子时计时,适应现代社会的精确计时。

但在不需要精确到秒的情况下,二者可以视为等同。(UTC有闰秒,而GMT没有)。

Unix 时间戳

这是基于 UTC 1970.01.01 00:00:00 到现在的总秒数/毫秒数,所以这个总秒数/毫秒数全世界都是一样的,也就是说 Unix 时间戳和时区无关,你可以在两个不同时区的服务器执行以下 Java 代码来验证,得出的结果是相同的。

本地时间

在日常生活中所使用的时间我们通常称之为本地时间。这个时间等于我们所在(或者所使用)时区内的当地时间,它由与世界标准时间(UTC)之间的偏移量来定义。这个偏移量可以表示为UTC-UTC+,后面接上偏移的小时和分钟数。

因为时区的问题北京时间和UTC时间有这样的关系 UTC + 8 = 北京时间,理解这个有助于我们后面解决国际化问题。

ISO 8601

ISO 8601一种国际通用的无歧义的日期和时间格式。这个ISO标准能够帮助消除因不同的日期转换、文化差异、时区等的影响导致对日期时间格式理解上的偏差,他给出了一种无论对人还是机器都清晰定义的日期和时间表示形式。

YYYY表示四位数的年份。
MM表示两位数的月份。
DD表示两位数的天(day of the month),从01到31
T是用来指示时间元素的开始字符,日期和时间合并表示时,要在时间前面加大写字母T。
hh表示两位数的小时,从00到23,不包括AM/PM。
mm表示两位数的分钟,从00到59。
ss表示两位数的秒,从00到59。
s表示一或多位数,表示秒的小数部分。
mmm表示三位数的毫秒数,从000到999。
TZD表示时区指示符:Z或+hh:mm或-hh:mm,+或-表示时区距离UTC(世界标准时间)时区多远。

Z表示UTC时间。
+hh:mm表示比UTC时间快的本地时区。
-hh:mm表示比UTC时间慢的本地时区。

星期

星期 全称 简写
周一 Monday Mon
周二 Tuesday Tue
周三 Wednesday Wed
周四 Thursday Thur
周五 Friday Fri
周六 Saturday Sat
周日 Sunday Sun

月份

月份 全称 简写
一月 January Jan
二月 February Feb
三月 March Mar
四月 April Apr
五月 May May
六月 June Jun
七月 July Jul
八月 Augus Aug
九月 September Sept
十月 October Oct
十一月 November Nov
十二月 December Dec

时间本地化

因为不同地区的人对时间展示有不同的习惯,所以存在时间本地化的问题,比如2023年12月1日,中国人接受的显示方式是2023-12-01,而在美国以及大多数西方国家接受的显示方式是Dec 1 2023。

这种针对同一时间,在不同地区使用不同显示方式的过程,就是时间本地化。

时区问题分析

应用程序处理请求全过程

从图中可以看到,一个应用主要分为三个部分,客户端、WebServer、DBServer。

客户端分布在全球各地,时区、时间本地化都不一样。

WebServer:处理全球各地的请求,同一时刻,东八区用户的实际时间是上午8点,东九区的用户的实际时间是上午9点。

DBServer:存储时间数据的地方。

时区问题实践

为了解决时区问题,我们统一约定以下原则:

  • DB数据库存储的总是UTC时间。
  • 客户端使用本地时间,与WebServer交互时必须带上时区。
  • WebServer负责将不同时区的时间转换成UTC时间,再与数据库交互。

DB MySQL时间存储

Mysql中存储时间主要有三种类型

  • bigint:直接将 utc 时间戳存到 int 类型的字段中。后期根据用户本地时区进行转换。
  • timestamp:MySQL 官方定义的时间戳,内部使用 utc 时间戳存储,但查询时返回的结果会随着 session time_zone 的变化而变化。
  • datetime:只存日期时间的值,不包含时区信息。

网上有的说timestamp好,有的说datetime好,我觉得不管哪种最终都能实现,个人习惯选择datetime类型存储时间。

datetime只存储时间的值,不包含时区信息,值由WebServer传入。不使用mysql自带的时间函数。

为了标准清楚,我们将字段名称做一下调整

created_time 改为 created_time_gmt

updated_time 改为 updated_time_gmt

 

WebServer时间处理

WebServer将不同时区的客户端的请求转换成统一的UTC时间,然后与DB进行交互;当从DB获取到时间数据后,再把时间转换成客户端的时区时间发送回客户端。

由于应用程序开发语言的时间相关函数与时区有关,所以我们必须约定一个标准,以0时区为标准,当然也可以以东八区为标准,区别是时间转换的时候值不一样。

约定统一时区的好处是,当应用程序部署在上海的服务器和部署在洛杉矶的服务器上的时间是一样的,因为都是记录的0时区的时间。

对于PHP来说,可以在php.ini文件中将时区设置为0市区,date.timezone = UTC;  这样有一个坏处是这台服务器上所有的程序都是使用了UTC时区,最好的方式是在应用程序内部设置时区。

不同框架设置时区的方式大同小异,如thinkphp设置时区的位置在config/app.php

// 默认时区
//'default_timezone' => 'Asia/Shanghai',
 'default_timezone' => 'UTC',

php获取UTC时间

$date = gmdate('Y-m-d H:i:s');

两个时间转换函数

/**
 * TimeZone 时间转换成UTC时间
 * @param $datetime
 * @return string
 */
function getGmtDateTime($datetime): string
{
    return getTimeZoneDateTime($datetime, "UTC");
}

/**
 * UTC时间转换成TimeZone时间
 * @param $datetime
 * @param $timezone
 * @return string
 */
function getTimeZoneDateTime($datetime, $timezone): string
{
    $dt = new DateTime($datetime);
    $dt->setTimezone(new DateTimeZone($timezone));
    return $dt->format("Y-m-d H:i:s");
}

iOS端

客户端在请求头增加app_timezone字段,将当前时区传给后端

获取当前时区

var appTimezone: String {
        return TimeZone.current.identifier
    }

Android端

还没开发,开发的时候再补上

Web端

还没开发,开发的时候再补上。从查的资料上来看,可以使用day.js获取时区。

 

大总结

没做之前觉得时区处理问题挺难得,有了原理的支撑,再去做的时候发现,居然三下五去二就开发完了,虽然也是这个应用比较简单的缘故。

这真应了那句话“为之则难者亦易矣,不为则易者亦难矣”。

文哥博客(https://wenge365.com)属于文野个人博客,欢迎浏览使用

联系方式:qq:52292959 邮箱:52292959@qq.com

备案号:粤ICP备18108585号 友情链接