转载一篇:一文读懂全球化系统中的日期时间处理问题
太长不看:
- 大多数应用中,只需要用 "绝对时间 DateTime" 一种技术实现即可
- 后端应统一用 UTC 时间(包括 DB 落盘、接口定义),不应当受用户时区或服务器时区的影响
- 前端输入、展示的时间,根据具体业务场景进行时区调整,以及精度调整
- 面对不带时间的日期,要明确区分「纪念日」与「精度不高的绝对时间」两种用途,大部分时候你看到的日期是后者,它也应当用“确定时区的 DateTime”来实现
重要性
日期时间的处理,一直是计算机系统中看似简单,实则经常爆雷的问题。
例如,每隔几年,都会爆出的「千年虫问题」的各种变种,通常因为系统在设计之初,没有设计好日期时间的数据存储方式,或者低估了产品设计的生命周期,导致最初选型的数据结构不够用了。
千年虫问题:
年纪大的程序员,都知道千年虫问题。在 2000 年之前,很多系统用 2 位数字表示年份,这样 99 年是它能表达的最大数值。因此 1999 年之后的一年,在这些系统中是没有定义的,甚至可能出现多种奇怪的情况,例如“1900”、“1:00”、“19:0”(为什么?感兴趣的读者可以自己推测)。
如果说,「千年虫」是在时间维度上缺乏前瞻性的设计导致的,那么另一种缺乏前瞻性的问题,是空间维度的,即产品全球化、跨时区带来的问题。
全球化的产品中,如果时间的处理没有遵循统一的标准,会让整个系统充斥着难以理解和维护的时间转换。各种接口的对接文档,都不得不明确说明「这个接口的时间是什么时区的?需要如何处理?」后端服务如果需要跨国部署在多个大洲的机房时,因为服务器的时区不同,需要做大量的改造。
遗憾的是,大多情况下,产品不会一开始就有「全球化」属性。所以在一开始,产研团队都不会重视全球化的设计问题,很容易留下缺乏前瞻性的设计问题。
通常情况下,我们都不鼓励「过度设计」。然而,日期时间的设计,是最不怕「过度」的。这时因为,在技术上实现一个前瞻的时间日期方案,成本并不高;但如果一开始的设计不够,后期的升级和数据迁移工作,却是伤筋动骨的。
如何表达时间和日期?
-
时间日期的传递:用字符串
在微服务之间,以及在前后端之间,建议用字符串传递日期时间。字符串清晰易读,易于人工调试,带来的开销通常也完全可以接受。(带大量时间数据的接口,建议考虑用 Unix Timestamp)如果用字符串,格式就不要自己发明了。有个非常明确的国际标准:ISO 8601(wikipedia: https://en.wikipedia.org/wiki/ISO_8601)
下面举例是符合规范的常用格式:
- 仅日期:2022-02-09
- UTC 日期时间:2022-02-09T12:36:42Z
- 特定时区的日期时间:2022-02-09T20:36:42+08:00
- 精度更高的时间:2022-02-09T12:36:42.123456789Z
注意,MySQL 中使用的字符串格式(如 2022-02-09 12:36:42)并不符合规范,不建议使用。
-
时间日期的存储:关注 MySQL 中的 DateTime
不同数据库在时间日期相关对象的处理差异很大。这里单说 MySQL,因为坑不小。MySQL 的 DateTime 数据在存储时并不包含时区信息,因此,在读取时也不会做任何时区的转换。
同时,每个 MySQL 连接会话,都有「会话时区」的概念,但这个概念只影响 MySQL 的 NOW() 等有关当前时间的函数的行为,对数据中已经保存的 DateTime 没有任何影响。
SET time_zone = '+00:00' ; UPDATE tab SET datetime_colume = '2020-01-01 00:00:00'; SET time_zone = '+08:00' ; -- 换一个会话时区 SELECT datetime_colume FROM tab; -- 返回值仍然是 '2020-01-01 00:00:00',和写入的数据一致,和会话时间无关 --------- SET time_zone = '+00:00' ; SELECT NOW(); -- 假设返回 '2022-01-01 00:00:00' UPDATE tab SET datetime_colume = NOW(); -- 存入的是 '2022-01-01 00:00:00' SET time_zone = '+08:00' ; -- 换一个会话时区 SELECT NOW(); -- '2022-01-01 08:00:00' 根据时区变化了 SELECT datetime_colume FROM tab; -- '2022-01-01 00:00:00' 已经写入的不会变
-
时间日期 的计算:语言原生的 DateTime 类型
各语言一般都提供了原生的 DateTime 数据类型,以表达绝对的日期时间,并且都支持上面 ISO 8601 规范的解析和格式化。
处理相对时区时,各种语言通常都是使用操作系统的时区数据库,来转化为绝对时区。时区数据库需要在联网情况下,由操作系统负责定时更新。
-
万能的 Unix Timestamp
Unix Timestamp 在存储、计算、传递环节都可以使用,可谓万能。它唯独不适合表达纪念日日期。它通过一个数值表示了一个绝对时间与 Unix Epoch 时间(定义为 1970-01-01T00:00:00Z)的差值秒数。Unix Timestamp 本身已经表达了绝对时间,并不需要时区信息。
使用 Unix Timestamp 时,应特别注意选用合适的数值类型,它会影响时间表示的范围。稍不留神,你就可能种下一个新的千年虫。
- 用有符号int32,最多表示到 2038 年。MySQL 的 TIMESTAMP 类型也是它,一个千年虫变种
- 用有符号int64,并使用 9 位 10 进制定点小数位时,就是 Golang 的UnixNano(),可以表示 1678 年至 2262 年
- 一般不会用浮点数表示,因为浮点数的精度不固定
网友评论