前言
在和前端同学联调的过程中,我们通常会遇到与时间有关的场景:为什么传过来的时间落库的时候总是1970年?为什么相同的时间戳在不同的区域里转换成的时间字符串不一样?为什么不同的转换方法换成的结果一会是GMT,一会是UTC?
时间,这个在开发中不起眼的一环,却又往往会让很多开发者在遇到时犹豫不决。
在本文开始之前,先抛出一个问题:在日常的业务场景中,什么场景使用北京时间?什么场景使用本地时间?
GMT与UTC
我们先来复习一下以前的地理知识:
我们知道,地球分为24 个时区,一个时区的范围是十五个经度,地球又分东西半球,东西半球各占十二个时区;每个时区相差一个小时,最多相差24小时,也就是一天。
凡向西走,每过一个时区,时间要慢一个小时,就要把表拨慢1小时(就是说你所在的位置是两点,向西一个时区就减去一个小时,也就是一点);凡向东走,每过一个时区,时间要快一个小时,就要把表拨快1小时(比如1点拨到2点)。
而这一切的起点,就在英国伦敦,那里有一条世界上著名的线,叫本初子午线,是人类世界计算时间的起点(时区的划分)以及经度的起点。而这条线的划定是由格林尼治天文台确定的,因此格林尼治天文台所在的地方叫零时区。零时区表示为GMT+00,零时区缩写叫z。
所以GMT,即Greenwich Mean Time,格林尼治标准时间(格林尼治所在地的标准时间)。
以格林尼治天文台所在的时区为中心(GMT+00),向东为正,向西为负;零时区比东时区晚,比西时区早。
北京所在的时区叫东八区,东八区表示形式是:GMT+08。0时区比东八区的时间晚8小时,比西五区的时间早5小时。美国华盛顿比北京慢13小时。
UTC全称Universal Time Coordinated,是国际无线电咨询委员会制定和推荐的,UTC相当于本初子午线(即经度0度)上的平均太阳时。它是经过平均太阳时(以格林威治标准时间GMT)、地轴运动修正后的新时标以及以「秒」为单位的国际原子时所综合精算而成的时间,计算过程相当严谨精密,因此若以「世界标准时间」的角度来说,UTC比GMT来得更加精准。
所以,简单来说,UTC就是一个比GMT更加精确的时间表述。
说完了人类世界的时间表示,我们来看看计算机世界的时间表述。
认识二进制世界中的时间
这种表述方式被称为Unix时间(Unix Time),也叫做POSIX时间或,是用来记录时间的流逝,定义为从UTC时间1970年1月1日0时0分开始流逝的秒数,不考虑闰秒。
上述图中,有一个很不起眼但很关键的一个点:t=0时刻。这个时刻为1970年1月1日0时0分,被称为纪元时间(epoch time)。从定义可以看到它只代表了从Unix纪元开始流逝的秒数,所以你身处地球上何处,这个时间都是一样的。
有一点值得注意的是,Unix纪元是Unix或类Unix系统,一些C/C++,Java等编程语言使用的纪元。而其他的操作系统或者编程语言,使用的就是不一样的纪元起始日期了。
例如:Microsoft C/C++ 7.0 使用的是 1899年十二月31号
时区转换
既然地球上的不同时区所处的时间是不一样的,那么计算机在处理跨时区问题时就需要做时区的转换。在我们对时区问题抽象之前,我们可以先看看跨时区可能会出现什么异常?
跨时区可能会出现什么异常?
问题1:跨时区引发的展示异常
如果在日本(比北京时间快1小时)使用淘宝下单,看到的时间是日本时间还是北京时间?支付服务是按照北京时间还是日本时间执行?
问题2:时间错乱引发的处理异常
在旧金山(比北京时间慢16小时)的用户A计划在2023-01-01给北京的用户B预约一笔转账计划,但后端收到之后当做北京时间处理,于是在北京时间"2023-01-01"(对应旧金山时间 2022-12-31)进行了转账,于是用户B提前了1天2022-12-31就收到了转账。
对跨时区问题的抽象处理
在上述场景中,有几个不同的时间,接下来我们一一阐述
- 用户的时间(客户端)与服务器的时间(展示与计算)
- 服务器与服务器的时间(跨时区计算)
- 服务器与数据库的时间(计算与存储)
Client <=> Web Server
Web Server => Client
- 若二者时区相同(已将server服务器设置为当地时区),那么可以在server端将时间格式化为字符串字面量直接传输到client端展示。其中,若server端时间为其他时区的字符串字面量时,需转为当前时区;若为Date对象,直接format,Java默认取当前系统时区;若为毫秒long,转为Date再格式化
- 若时区不同,server端将时间转换成毫秒数long或者字面量+时区,传输到client,由client所处的时区进行转换处理,最终展示。
Client => Web Server
如用户通过前端时间控件选择的时间,需要转化为毫秒数long或者字面量+时区,传输到server端。
Server-n <=>Server-k
取决于时间的序列化和反序列方式,如dubbo所使用的hession序列化方式会将Date对象序列化为毫秒数、json将时间序列化为字符串(需要指定时区)
Server-n <=> DB
数据库时间最常用的字段类型有bigint、datetime和timestamp
- bigint用于自己维护一个时间戳,8字节的长度让其使用几乎无上限,因此使用bigint不存在时区问题,可以使用;
- datetime字段以字符串格式存储,对应应用层String,无时区属性,在多时区场景下传输与转换不建议使用;
- timestamp字段以时间戳格式存储,对应应用层Date类型,与系统时区无关,但需要注意的是,由于4字节的长度导致存储的时间上限为2038-01-19 03:14:07,需做好对应的处理。
有关2038-01-19 03:14:07这个著名的时间点,详情可参考我之前写过的一篇《聊一聊2038年问题》
时间序列化和反序列化
上文提到使用bigint不存在时区问题,因此这里我们来重点讨论下datetime和timestamp在时间存储与读取时所遇到的序列化与反序列化的情况
- DateTime类型字段,MySQL存储时不存时区信息,并且怎么存就怎么取,不做任何处理和转换。所以时区timeZone1的server1插入MySQL一条记录后,时区timeZone2的server2读取出来的时间就不对了。这里只能将所有的server的时区设置为一样的,或者在数据库表中添加一个字段存储时区信息
- TimeStamp类型字段,这个比较特殊。当server创建connection时,可以在数据库URL中手动指定时区信息,即不同时区的server连接MySQL时,指定connection时区使用自己所在时区。当MySQL处理不同的connection时,就有了时间字符串和发出请求的时区,然后转换为UTC时间进行存储。从MySQL中读取时也是基于connection的时区设置进行转换。但是如果不指定connection时区,那么MySQL就将存储的UTC时间,按MySQL服务器所在时区进行转换和展示或者传输,此时若MySQL服务器和server的时区不一致,就会出现时区问题
由上可知,产生时区问题的根本原因在于不同时区的机器对时间进行序列化和反序列化时,Date对象或者毫秒数long与字符串之间的转换,丢失了时区信息,最终导致问题。
跨时区场景应用调研
了解了跨时区场景的处理后,我们来看看业内产品是如何处理跨时区业务的。
行业 |
业内App |
结论 |
通讯类 |
微信、QQ |
|
电商类 |
抖音、淘宝、拼多多、亚马逊 |
|
金融类 |
招银、中银、工银、paypal |
|
分析
1. 非金融类的app不太关心跨时区场景?
常规业务对于时间点信息的感知偏弱,更倾向于感知距离“现在”的时间差(消息、评论等),不同的服务有各自对应的最佳“时区”搭配。
2. 银行类App的时间是统一使用“北京时间”?
国内银行类服务、金融类服务(基金、股票)有强制使用北京时间的诉求。
3. 国际化业务有时区设置能力?
可选,国际化业务中一般在时间信息之后回携带上时区信息(如:时区偏移量、城市信息), 移动端服务 常见的是跟随设备的时区,部分服务商会在业务中提供出选择时区的能力。
4. 衡量使用北京时间和设备本地时间的标准是什么?
业内暂无具体的标准,合规、安全也没有详细要求,最佳的是站在用户体验角度case by case去分析。
总结
国际化背景下多时区问题日益严重,多时区引发的问题一般不易发现,发现时已经灾难性问题,应对及改造成本也较高。架构层面尽量规避时区问题,在系统设计阶段需要考虑并遵循相应的原则。
DB层面:
- 新增表或字段时需要对时间类型慎重选择,非跨时区场景建议选择datetime,跨时区场景可以选择timestamp或bigint。
- 对于VARCHAR类型时间字符串都必须使用iso标准时间格式。
- 新建库的DB时区需为UTC时区,不能使用其他时区(如LA时区)。
应用层面:
- 新建应用的服务器时区需为UTC时区。
- 应用内部以及应用与应该之间,统一使用Date对象或iso标准时间格式字符串来传递属性。
- 只需要年月日信息的日期属性, 请定义为String类型,不要定义为Date类型。
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:dandanxi6@qq.com