当前位置: 首页 > 科技观察

您知道该程序如何处理时区问题吗?

时间:2023-03-20 14:04:58 科技观察

本文转载自微信公众号《三太子敖丙》,作者三太子敖丙。转载本文请联系三太子敖丙公众号。前言在实际业务开发中,您会遇到夏令时、闰秒、时区转换等问题。这些问题都需要从业务的角度来考虑,保证用户在任何区域看到的数据都是一致的。这就需要MySQL数据库,后端服务和前端服务需要做相应的处理才能完成。最近刚好在开发的时候碰到了。幸好写了这篇比较冷门的文章,跟大家聊聊实际开发过程中夏令时、闰秒、时区转换的解决方法。DaylightSavingTime夏令时简介(DaylightSavingTime:DST):又称“夏令时”,是人为规定当地时间以节约能源的一种制度。实行本制度时统一采用的时间称为“夏令时”。一般在黎明较早的夏季,人为将时间提前一小时,可以使人们早起早睡,减少照明量,从而充分利用光资源,从而节约照明用电。除了夏令时,还有冬令时,也就是当地的标准时间。可见意大利是有夏令时的。夏令时时间为3月28日至10月31日,冬令时(当地标准时间)为11月1日至次年3月27日。夏令时期间,时间比标准时间快一小时。例如,罗马时区为GMT+1:00,标准时间为10:00:00,夏令时为11:00:00,冬令时为10:00:00。CET(中欧标准时间)是UTC+01:00时区的名称之一。比UTC(世界标准时间)早1小时。与UTC的时间偏移可以写成+01:00。它在冬天使用,在夏天使用使用CEST-中欧夏令时(UTC+02:00,提前一小时)。LInux时区的Linux服务器的系统时间是通过NTP(NetworkTimeProtocol)服务来校准的。每隔一段时间就会与时钟源进行校验,以确保Linux系统时间的准确性。同时,Linux操作系统支持不同的国家和地区。时区设置,所有时区信息都位于/usr/share/zoneinfo目录下,如果需要设置时区,只需要将/etc/localtime软链接到特定区域即可,如果有DST机制在这个地区,那么Linux会自动设置时区,在DST和标准时间之间切换,不需要额外的代码来处理。##Linux支持的区域信息$ls-ltr/usr/share/zoneinfo/total320lrwxrwxrwx1rootroot3October2305:18Zulu->UCT-rw-r--r--1rootroot1544October2305:18W-SU-rw-r--r--1rootroot1873Oct2305:18WETlrwxrwxrwx1rootroot3Oct2305:18UTC->UCTlrwxrwxrwx1rootroot3Oct2305:18Universal->UCT-rw-r--r--1rootroot127Oct2305:18UCT-rw-r--r--1rootroot1970#:1rootroot1970#:1rootroot127Oct2305:18UCT-rw-r--r--1rootroot1970服务器区的Linux时间所在的0rootOctroot#7170前端服务位于$ls-ltr/etc/localtimelrwxrwxrwx1rootroot33November106:20/etc/localtime->/usr/share/zoneinfo/Asia/Shanghai使用zdump命令查看意大利罗马的时区属性。$zdump-v/usr/share/zoneinfo/CET/usr/share/zoneinfo/CETSunMar2800:59:592021UT=SunMar2801:59:592021CETisdst=0gmtoff=3600/usr/share/zoneinfo/CETSunMar2801:00:002021UT=SunMar2803:00:002021CESTisdst=1gmtoff=7200#2021夏令时开始/usr/share/zoneinfo/CETSunOct3100:59:592021UT=SunOct3102:59:592021CESTisdst=1gmtoff=7200#2021夏令时结束从上面的信息可以看出,2021年夏令时开始时间为3月28日星期日01:00:00,结束时间为10月31日星期日00:59:59。isdst=1表示当前处于夏令时期间,gmtoff=7200表示与格林威治时间的偏移量。单位是秒,即UTC+02:00,又称为CEST时间,意思是Linux操作系统自动实现了夏令时的自动切换夏令时。处理夏令时例如,意大利罗马的一个客户需要开发一个税务系统,用于全国各个城市的税务核算。由于意大利有夏令时,所以需要考虑夏令时DST的处理。在开发过程中,与时间相关的问题包括MySQL数据库(mysql-server)、后端服务(backend-service)和前端服务(frontend-service)。下面从三个层面分析如何应对夏令时。前端处理业务要求前端正确显示当时的时间,包括有夏令时的时间,无论是移动端还是PC端。如果是在中国,就更容易对付了。没有夏令时机制,统一使用东八区,即GMT/UTC+08:00。前端服务的时间直接取自Linux服务服务器的系统时间。linux的时区只需要设置成Asia/Shanghai就可以了,前端不需要做任何时间的传入传出。**转:**指的是POST请求写入数据,user—>frontend-service—>backend-service—>mysql-server,比如纳税接口。**转:**指GET请求查询数据,mysql-server—>backend-service—>frontend-service—>user,如查询接口。不过值得庆幸的是,Linux操作系统已经自动实现了DST转换,不需要在前端做任何处理,而且Linux时区设置为CET。#修改LInux时区为CET,也可以通过timedatectl命令修改。$ln-sf/usr/share/zoneinfo/CET/etc/localtime如意大利用户通过终端(手机端或PC端)登录系统纳税或查询时,用户时间完全一致作为前端服务时间,即完成后续步骤的处理。后端处理我们了解到,前端Linux服务器的时区设置为CET,可以自动处理意大利DST夏令时的转换。后端Java程序部署在Linux服务器上,其时区设置与前端相同,也是CET时区。后端你只需要从前端接收值来执行MySQLCRUD操作。税表结构如下:CREATETABLE`tax_form`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'主键id',`tax_id`varchar(20)NOTNULLDEFAULT''COMMENT'税号',`amount`decimal(12,4)NOTNULLDEFAULT'0.0000'COMMENT'税额',`tax_payer_id`varchar(20)NOTNULLDEFAULT''COMMENT'纳税人编号',`status`tinyintNOTNULLDEFAULT'0'COMMENT'纳税状态',`audit_time`datetimeNOTNULLDEFAULT'0000-00-0000:00:00'COMMENT'审核时间',`create_time`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'修改时间',PRIMARYKEY(`id`)USINGBTREE)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_0900_ai_ciCOMMENT='Taxrecordtable';主要看纳税和审计接口,对应的SQL语句如下:,0)audit--修改审计状态updatetax_formsetstatus=1,audit_time='2021-01-0712:02:30'wheretax_payer_id='U001';有两个字段涉及时间Class**publicfields:**create_time,update_time这些是每个表必须有的时间字段,默认是MySQL的CURRENT_TIMESTAMP,也就是MySQL服务器当前的系统时间,这个时间和MySQL时区不一样time_zone设置但是mysql也是支持DST自动转换夏令时的业务领域:audit_time审计时间属性由前端页面传给后端处理,后端不需要做任何转换。MySQL也支持处理MySQL的DST机制,但是设置时区time_zone只能设置到地区(类似于linux中设置时区),不能和史永红MySQL设置的时区相关联。mysql>showvariableslike'%zone%';+------------------+--------+|Variable_name|Value|+----------------+-------+|system_time_zone|CST|--数据库服务器当前时区,不可修改。这里的CST是指中国标准时间(ChinaStandardTimeUTC+08:00,即东八区)|time_zone|SYSTEM|--数据库时区,默认与服务器一致,可以修改。目前是东八区,改为意大利时区,也就是东一区。mysql>selectnow();+--------------------+|now()|+--------------------+|2021-01-0713:43:31--修改数据库时区为零时区,即。mysql>settime_zone='CET';ERROR1298(HY000):Unknownorincorrecttimezone:'CET'--尝试修改+0:00,可以修改成功。mysql>settime_zone='+1:00';QueryOK,0rowsaffected(0.00sec)存储时区信息的MySQL数据字典mysql>showtablesfrommysqllike'%time_zone%';+---------------------------------+|Tables_in_mysql(%time_zone%)|+-----------------------------+|time_zone|--时区信息|time_zone_leap_second|--时区闰秒信息|time_zone_name|--时区名称|time_zone_transition|--时区转换|time_zone_transition_type|--时区transitiontype默认情况下,这些表都是空的,需要通过mysql专门提供的命令mysql_tzinfo_to_sql导入,数据会插入到time_zone相关的表中。#linux/usr/share/zoneinfo下的时区信息通过命令mysql_tzinfo_to_sql加载到相关的time_zone表中。$mysql_tzinfo_to_sql/usr/share/zoneinfo|mysql-urootmysql执行后,查看表中的数据,然后尝试将时区设置为CET。mysql>select*frommysql.time_zone_namewherenamelike'%CET%';+------+------------+|Name|Time_zone_id|+------+------------+|CE??T|373|--设置时区为CETmysql>settime_zone='CET';QueryOK,0rowsaffected(0.02sec)mysql>selectnow();+--------------------+|now()|+--------------------+|2021-01-0710:00:36|还支持时区转换,如将北京时间转换为罗马时间。--北京时间17:00:00转换为CET罗马时间为10:00:00mysql>selectconvert_tz('2021-01-0717:00:00','Asia/Shanghai','CET')astime;+--------------------+|时间|+--------------------+|2021-01-0710:00:00|我们要解决的问题是:MySQL设置time_zone='CET'后能否自动实现夏令时转换?如果是这样,那么客户端、前端服务、后端服务和MySQL服务器的时区统一为CET,同时可以自动处理DST。从上面zdump-v/usr/share/zoneinfo/CET命令的输出可以看出,2021年意大利的夏令时是从3月28日的01:59:59开始的,也就是向前一小时。--01:59:59时间,没有发生夏令时切换。mysql>selectconvert_tz('2021-03-2801:59:59','+1:00','CET')astime;+-------------------+|time|+--------------------+|2021-03-2801:59:59|--02:00:00时间,确实ADSTswitchoccurred,from02:00:00to03:00:00mysql>selectconvert_tz('2021-03-2802:00:00','+1:00','CET')astime;+-------------------+|时间|+--------------------+|2021-03-2803:00:00|--将+1:00时间改为CET,结果一样,夏令时切换。mysql>selectconvert_tz('2021-03-2802:00:00','CET','CET')astime;+--------------------+|time|+--------------------+|2021-03-2803:00:00|从上面的结果可以看出,当time_zone设置为region/City,系统会自动解决夏令时的DSTQ切换问题。如果设置time_zone='+1:00',夏令时机制将丢失。目前在MySQL数据库中,初始化time_zone相关表元数据后,MySQL可以自行完成夏令时校正,不需要额外的业务处理。对于AWSRDS,time_zone可以选择地区/城市,即支持自动切换夏令时。夏令时处理小结通过上面的分析我们可以知道,linux服务器和mysql服务器都可以自动处理夏令时切换,前提是linux的时区和mysql的时区需要设置为地区,比如,两者设置为CET。闰秒是指国际计量局在年末或年中(可能是季末)对协调世界时加减1秒的调整。以使协调世界时接近世界时。最后一次闰秒发生在北京时间2017年1月1日7时59分59秒(时钟显示为07时59分60秒)。在实际业务系统中,Linux服务器、Java代码、MySQL数据库都会受到闰秒的影响。下面来看看他们分别是如何解决闰秒问题的。对于大多数新的linux内核(2.6.x内核支持LeapSecond之后,之前可能会导致LinuxKernelCrash),Linux服务器在设计时支持闰秒,Linux操作系统的时间通过NTP服务进行通信时钟源用于同步。NTP会逐级发送闰秒事件通知,直到最边缘的NTP服务器,然后NTP将闰秒通知给客户端的操作系统,由操作系统处理闰秒通知。对于闰秒2017-01-0107:59:60,Linux内核这次需要处理,所以需要做一些具体的处理。一般有以下三种解决方法。倒退一秒,停一秒,真正加一秒。第一种方法会导致一些基于时间戳的消息通知乱序,第二种方法会导致出现两个相同的时间戳,而最后一种方法不会导致时间戳问题,这也是后来Linux内核选择的方案。mysql>选择UNIX_TIMESTAMP('2017-01-0107:59:59')asnts;+------------+|nts|+------------+|1483257599|$date-d'@1483257599'--utcSunJan107:59:59UTC2017$date-d'@1483257600'--utcSunJan108:00:00UTC2017从这里我们可以看出Linux采用了第三种方案:realincreaseOnesecond,即也满足了业务系统的需求。Java代码的System.currentTimeMillis()Java代码会产生闰秒60,具体取决于Linux操作系统。LeapSecond问题已在LinuxKernel2.6.x之后得到修复。在MySQL数据库上,我们可以看到MySQL下已经存在mysql.time_zone_leap_second数据字典,说明已经支持LeapSecond了,处理方案和linux类似。--创建测试表存储时间戳CREATETABLEls(idbigintNOTNULLCOMMENT'id',tsTIMESTAMPDEFAULTCURRENT_TIMESTAMP,PRIMARYKEY(id));--设置数据库时区为UTCmysql>settime_zone='UTC';mysql>settimestamp=1483257599;--对应时间:2017-01-0107:59:59mysql>insertintols(id)values(1);mysql>settimestamp=1483257600;--对应时间:2017-01-0107:59:60mysql>insertintols(id)values(2);--可以看到MySQL对闰秒进行了处理,将07:59:60转换为08:00:00。mysql>selectid,ts,unix_timestamp(ts)fromls;+----+--------------------+--------------------+|id|ts|unix_timestamp(ts)|+----+--------------------+-------------------+|1|2017-01-0107:59:59|1483257599||2|2017-01-0108:00:00|1483257600|--leap第二次查询会报错mysql>select*fromlswherets='2017-01-0107:59:60';ERROR1525(HY000):IncorrectTIMESTAMPvalue:'2017-01-0107:59:60'mysql>选择*fromlswherets='2017-01-0108:00:00';+----+--------------------+|id|ts|+----+--------------------+|2|2017-01-0108:00:00|跨境系统处理时间上面介绍的意大利罗马税务系统,其实属于政企业务,只服务于国内用户的需求,不涉及境外用户的诉求。相对而言,地域和人员相对固定,而eBay这样的跨境电商巨头,服务的用户遍布全球。在世界各地,每个地区都有不同的时区,每个时区的夏令时开始时间也不同。我们要解决的是根据客户的位置显示正确的时间(包括夏令时),和前面的夏令时处理一样,也涉及到三端处理:前端服务(frontend-service),后端服务(后端服务)和MySQL数据库(mysql-server)。从这个图可以看出,前端服务的UI层必须和用户所在地区的时间完全一致。至于后端服务和MySQL如何处理时间,用户根本不关心,这就要求前端必须根据不同的Region,不同的时区,不同的夏令时DST用户生成不同时间进行转换处理,不同区域的时间转换目前有现成的前端(Vue/React)插件可以直接使用。同时希望用户时间的转入和转出只在前端处理,在后台和MySQL数据库不做任何修改的情况下完成业务处理和数据存储.北京用户在UTC+8,也就是东八区,而罗马用户在UTC+1东一区,都是按照UTC来处理的。那么我们可以将时区设置为UTC,然后根据用户所在的位置进行操作。相应地对待。MySQL处理将MySQL数据库的时区设置为UTC,无论用户来自哪个地区,数据库中存储的时间都是UTC,包括公共时间字段(创建时间、修改时间)和业务时间字段(交易开始时间),交易结束时间)。--SetdatabasetimezonetoUTC,即零时区--setdatabasetimezonetoUTC,即零时区setglobaltime_zone='UTC'后端处理MySQL时区为UTC,则时区后端服务所在的LinuxServer统一设置为UTC,与MySQL一致,这样后端不需要做任何转换。前端处理前端获取标准时区UTC的数据,根据用户所在时区进行转换,保证与后端数据时区的一致性,以及前端根据实际情况渲染。一般来说,前端向后端传输时间数据,后端封装成timestamp,对应timestamp类型存储在MySQL中(MySQL中的timestamp不区分时区,例如数据库为UTC02:00:00,北京用户使用eBay在CST10:00:00下单,数据库中订单表的create_time应该存储2020-12-0310:00:00),前面-结束查询数据也应该进行相应的转换。定时任务后端服务一般都会有一些定时任务。这个时间一般取自LinuxOS的时间,与前端无关。可以根据Linux的UTC时区做相应调整。综上所述,上面介绍了夏令时、闰秒、越界的时间处理问题,主要涉及MySQL数据库、后端服务、前端服务三个层面。对于夏令时和闰秒的转换处理,Linux和MySQL都可以自动完成处理,无需额外转换;对于跨境系统的时间处理,通过将Linux和MySQL的时区设置为UTC,只需要前端服务处理不同地域用户的时间问题,降低了系统改造的风险。今天就说这么多,希望对大家有所帮助。