1. 大多数运用中,只须要用“绝对韶光 DateTime”一种技能实现即可
2. 后端应统一用 UTC 韶光(包括 DB 落盘、接口定义),不应当受用户时区或做事器时区的影响
3. 前端输入、展示的韶光,根据详细业务场景进行时区调度,以及精度调度

4. 面对不带韶光的日期,要明确区分「纪念日」与「精度不高的绝对韶光」两种用场,大部分时候你看到的日期是后者,它也应该用“确定时区的 DateTime”来实现
1. 主要性日期韶光的处理,一贯是打算机系统中看似大略,实则常常爆雷的问题。
例如,每隔几年,都会爆出的「千年虫问题」的各种变种,常日由于系统在设计之初,没有设计好日期韶光的数据存储办法,或者低估了产品设计的生命周期,导致最初选型的数据构造不足用了。
千年虫问题:
年纪大的程序员,都知道千年虫问题。在 2000 年之前,很多系统用 2 位数字表示年份,这样 99 年是它能表达的最大数值。因此 1999 年之后的一年,在这些系统中是没有定义的,乃至可能涌现多种奇怪的情形,例如“1900”、“1:00”、“19:0”(为什么?感兴趣的读者可以自己推测)。
如果说,「千年虫」是在韶光维度上缺少前瞻性的设计导致的,那么另一种缺少前瞻性的问题,是空间维度的,即产品环球化、跨时区带来的问题。
环球化的产品中,如果韶光的处理没有遵照统一的标准,会让全体系统充斥着难以理解和掩护的韶光转换。各种接口的对接文档,都不得不明确解释「这个接口的韶光是什么时区的?须要如何处理?」后端做事如果须要跨国支配在多个大洲的机房时,由于做事器的时区不同,须要做大量的改造。
遗憾的是,大多情形下,产品不会一开始就有「环球化」属性。以是在一开始,产研团队都不会重视环球化的设计问题,很随意马虎留下缺少前瞻性的设计问题。
常日情形下,我们都不鼓励「过度设计」。然而,日期韶光的设计,是最不怕「过度」的。这时由于,在技能上实现一个前瞻的韶光日期方案,本钱并不高;但如果一开始的设计不足,后期的升级和数据迁移事情,却是伤筋动骨的。
2. 如何表达韶光和日期?2.1 韶光日期的通报:用字符串在微做事之间,以及在前后端之间,建议用字符串通报日期韶光。字符串清晰易读,易于人工调试,带来的开销常日也完备可以接管。(带大量韶光数据的接口,建议考虑用 Unix Timestamp。)
如果用字符串,格式就不要自己发明了。有个非常明确的国际标准:ISO 8601(wikipedia: https://en.wikipedia.org/wiki/ISO_8601)
下面举例是符合规范的常用格式:
仅日期:2022-02-09UTC 日期韶光: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)并不符合规范,不建议利用。
2.2 韶光日期的存储:关注 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' 已经写入的不会变
2.3 韶光日期的打算:措辞原生的 DateTime 类型各措辞一样平常都供应了原生的 DateTime 数据类型,以表达绝对的日期韶光,并且都支持上面 ISO 8601 规范的解析和格式化。
处理相对时区时,各种措辞常日都是利用操作系统的时区数据库,来转化为绝对时区。时区数据库须要在联网情形下,由操作系统卖力定时更新。
2.4 万能的 Unix TimestampUnix Timestamp 在存储、打算、通报环节都可以利用,可谓万能。它唯独不适宜表达纪念日日期。
它通过一个数值表示了一个绝对韶光与 Unix Epoch 韶光(定义为 1970-01-01T00:00:00Z)的差值秒数。Unix Timestamp 本身已经表达了绝对韶光,并不须要时区信息。
利用 Unix Timestamp 时,应特殊把稳选用得当的数值类型,它会影响韶光表示的范围。稍不留神,你就可能种下一个新的千年虫。
用有符号int32,最多表示到 2038 年。MySQL 的 TIMESTAMP 类型也是它,一个千年虫变种用有符号int64,并利用 9 位 10 进制订点小数位时,便是 Golang 的UnixNano(),可以表示 1678 年至 2262 年一样平常不会用浮点数表示,由于浮点数的精度不固定3. 产品视角的日期韶光设计本着不重不漏的原则,我们可以按如下表格划分产品中的所有日期韶光工具:
日期+韶光
仅韶光
仅日期
不指明时区,无需根据用户所在时区做转换
① 表示本地的确定时间点
③ 表示本地重复性韶光
⑤ 表示纪念日、节日
指明时区,需根据用户所在时区做转换
② 表示环球唯一确定韶光点
④ 表示环球可理解的重复性韶光
❌ 不存在的场景
下面逐一阐明这五种场景。
3.1 表示环球唯一确定韶光点(表中的 ②)信息量包含「年月日-时分秒-时区」。这样,就可以完备确定历史长河中的一个无歧义的韶光点。这个韶光点是完备客不雅观的,和访问的用户地理位置无关,和做事器的地理位置无关,和什么都无关。
产品表现上,常日会根据查看者所在的时区来重新调度韶光的显示。
用场举例:
单个事宜发生的韶光。如 2022 年冬奥会开幕式的韶光:2022 年 2 月 4 日,20 点整,+0800 时区。一个英国人看电视转播预报时,会看到开幕式的转播韶光是:2022 年 2 月 4 日中午 12 点整。这表示了根据查看者做韶光的转换。3.2 表示本地的确定时间点(表中的 ①)包含「年月日-时分秒」,由于没有时区信息,以是它本身并不能确定一个精确的韶光点,而是只在特定的情境下才故意义。
所谓特定的情境,是由于业务场景中蕴含了时区的信息,并且是大家公认的共识。因此,实质上它仍旧表示了一个绝对韶光。在产品表现上,由于对时区的共识,以是不须要根据查看者的时区来调度韶光的展示。
用场举例:
在非国际化的产品中,明确知道用户所在的时区,那么去掉时区是最大略的处理办法,可以省去很多麻烦。对付时区有其他约定俗成的理解。例如:飞机的起飞降落韶光,酒店的入住离店韶光,一定是按照飞机起落地、酒店坐落地当地时区来表达的。在所有订票网站上,都会按照这个规则显示韶光,不论访问的用户身处哪个时区。3.3 表示重复性韶光(表中的 ③ 和 ④)和前两类比较,去掉了「日期」这个信息,是为了描述重复性的日程。它可以是指明了时区的,也可以不指明时区,而基于人们对时区的共识去理解。
用场举例:
每周三 8:00+0800 开会,如果这可以是个跨国的会议,大家都能理解精确的韶光。这时,产品表现上该当把稳根据查看者来调度显示。每周三 8:00 起飞的航班,航班起飞地的时区是蕴含的共识。产品表现中不必根据查看者的时区调度显示。3.4 纪念日日期(表中的 ⑤)日期工具险些只有一个故意义的用场:表示纪念日/节日。它不会包含时区信息。
认为「日期」只能用于「纪念日」,有些绝对了。但我确实查阅了很多资料,也没有看到任何非「纪念日」用场的日期。
例如:
小吴的生日是 3 月 11 日,那么不管他在中国还是美国,都会在 3 月 11 日这一天过生日。每年 12 月 25 日是西方的圣诞节,各个国家都在 12 月 25 日这一天庆祝,虽然它们并不在同一个时区。产品表示上,不须要根据时区调度日期的显示。实质上,「纪念日」的逻辑,实在是人脑的不严谨导致的一种习气,是不严谨、不客不雅观的习气。不包含时区信息,便是为了知足这种不严谨的习气。
3.5 区分「纪念日日期」与「精度不高的绝对韶光」上面说过,日期工具不能包含时区。你可能会问,我须要表示“北京韶光 2022 年 3 月 22 日”呢?答案是:这不是一个日期,而是一个「精度不高的绝对韶光」。
很多情形下,当你想用日期时,实在很可能须要的是个「精度不高的绝对韶光」。在飞书人力套件的业务中,常常会碰着这种场景。
例如,一个在美国的同学与一个在日本的同学,都在 2022 年 3 月 22 日这天从公司离职了,由同一个在北京的 HR 办理离职事变。
可见,从我们用户视角理解的「一个事宜发生的日期」,实在是我们忽略了韶光的精度。在产品环球化之前,我们通过一些默认的简化,忽略了韶光精度的问题(例如把韶光都填成 00:00:00)。一旦面临产品的环球化,就须要补齐韶光、提高精度。
而补齐韶光、提高精度的办法,须要根据详细的产品形态详细考虑、明确定义。
例如,在上述离职场景下,就须要按照这个公司对离职的定义来补充,可以是当地韶光当天的 23:59:59,也可以是当天下班韶光,如 17:00:00。
又比如,对付跨团队的业务,例如一个同学的上级申报请示线从一个美国 Leader 转到一个日本 Leader,那么为了避免歧义,常日谈判定一个确定的生效时区,犹如一按照公司的总部所在地的韶光来打算。
4. 日期韶光的技能实现4.1 确定时区的 DateTime
适用于上面的 ①②③④ 四种场景。
所有后端暴露的接口中的韶光工具,全部以 UTC 韶光表示。
同时,所有后端在存储、打算、传输韶光时,也统一利用 UTC 韶光。由于 DB 存储韶光时,时区信息会被丧失落,因此应担保丧失落的时区,是大家明确约定清楚的无歧义的,即 UTC。这样一来,DB 中的所有韶光字段也都没有歧义。
接口内部产生的韶光,例如 CreatedAt、UpdatedAt韶光,都该当转换为 UTC 再落盘。如果直策应用了 MySQL 的NOW()函数,应确保 MySQL Session 的时区设置精确。
在前端或 BFF 卖力处理用户输入的韶光,以及展示给客户看到的韶光。包括两个步骤:
处理“精度不高的韶光”问题。 比如:员工异动的生效韶光,用户只设置到“天”的精度。那么如果不跨国,可以补全用户会话时区的 00:00:00 为精确生效韶光;如果跨国,那就看客户如何定义,以及产品给客户若何的灵巧性:例如,可以以客户公司总部所在地的时区的 00:00:00 为精确生效的韶光。时区转换。 把稳,这里不一定是利用用户的会话时区来转换。如前面先容的飞机火车宾馆的预定时间,就要以预定当地的时区来转换。上述两点,是一定须要在产品设计中定义清晰的,切忌暗昧不清。
不要较真儿抬杠的几点:
由于历史缘故原由,DB 里已经采取北京韶光保存了,那么我们可以约定+0800 时区是我们所有后端接口的韶光。只要用一个确定的绝对时区,就不会有歧义,不必非要时 UTC。
也可以在后端接口的网关层处理韶光转换。不要较真那算不算 BFF,我们须要的是,时区转换逻辑应严禁深入到后真个下层去。
4.2 不带时区的 Date适用于上面的 ⑤,即纪念日场景。
输入或展示时,都不对日期做任何处理。日期工具直接保存在 DB 中。
只有真正的纪念日有必要用这种办法,应该非常谨慎。例如保存一个联系人的生日时。
5. 关于时区的分外处理5.1 时区的不愿定性利用绝对的时差来表示时区,例如:“东 8 区”表示比天下折衷韶光(UTC)早 8 个小时的时区。这是个客不雅观的时区。
很多时候,我们关注的是一个城市或地区的时区。例如:Asia/Shanghai 表示中国韶光;三字母的缩写 EST 表示美国东部标准韶光。把稳,这些根据地理位置定义的时区的时差是会发生变革的,变革成分包括:可能受到当地政策的影响,或夏令时影响。
对付历史的韶光,地理时区是可以确定客不雅观时区的,由于没有人会重新定义已经由去的韶光。
对付未来的韶光,地理时区并不能确定客不雅观时区。因此,如果一个未来的事宜是按照非绝对时区约定的,那么它很可能变革。并且,我们的产品须要考虑到处理这种变革。
例如,中国员工发起一个“每天早 8 点”的跨国会议,那么在美国,由于夏令时的改变,冬天开会的韶光和夏天是不一样的。反之,美国员工发起的一个“每天早 8 点”的跨国会议,由于美国夏令时的变革,对中国员工的韶光也是夏天和冬天不一样的。
5.2 夏令时某些国家在夏天,会把韶光调快一小时(提前一小时)。这表现为,同一个地区,在冬天和夏天用不同的绝对时区。
这样做,是由于夏天白天很长,调度后会在白天的更早的时段上班,从而放工后有更长的天亮的韶光。把稳,并不是把 10 点上班调度到 9 点上班,而是全社会重新定义了 10 点提前了一小时。
一个详细的例子,在美国:
在 2021 年 3 月 14 日凌晨 1:59:59 后,下一秒便是凌晨 3:00:00。因此,美国的 2021 年 3 月 14 日凌晨 2:10:00 这个韶光实际上不存在。为了兼容,根据 RFC5545,如果日程约在了这个不存在的韶光,会认为是 3:10:00。
在 2021 年 11 月 7 日凌晨 1:59:59 后,下一秒是凌晨 1:00:00。因此,美国的 2021 年 11 月 7 日凌晨 1:10:00 这个韶光实际上会涌现两次。为了避免歧义,根据 RFC5545,看到这个韶光时,会认为是靠前的韶光点。因此,除非用别国的时区来约日程,否则,美国老板是不可能约你在重叠的第二个小时内开会的。
阅读更多及参考文献Wikipedia: ISO8601 - 用字符串表达各种韶光工具的标准https://en.wikipedia.org/wiki/ISO_8601
RFC3339 - 互联网上关于韶光和日期实现的通用建议https://www.rfc-editor.org/rfc/rfc3339
RFC5545 - iCalendar 互联网日历运用的规范https://datatracker.ietf.org/doc/html/rfc5545
Stackoverflow: Daylight saving time and time zone best practices [closed] - 技能实现建议https://stackoverflow.com/questions/2532729/daylight-saving-time-and-time-zone-best-practices
Stackoverflow: How to store repeating dates keeping in mind daylight saving time - 技能实现建议https://medium.com/@vivekmadurai/how-to-deal-with-date-and-time-across-time-zones-39b1bd747f35
Medium: How to Deal with Date and Time across Time Zones - 技能实现建议https://medium.com/@vivekmadurai/how-to-deal-with-date-and-time-across-time-zones-39b1bd747f35
Microsoft365: Behavior and format options of the Date and Time field - 微软的韶光和日期字段的文档https://docs.microsoft.com/en-us/dynamics365/customerengagement/on-premises/customize/behavior-format-date-time-field?view=op-9-1
Time Change 2021 in the United States - 美国 2021 年夏令时的调度办法https://www.timeanddate.com/time/change/usa?year=2021