你知道程序是怎么处理时区问题的么?

开发 前端
在实际业务开发中,会碰到夏令时,闰秒,时区转换的问题,这些问题都需要从业务角度去考虑,保证用户在任何地区看到的数据都一致的,这就需要MySQL数据库、后端服务以及前端服务做相应的处理才能完成。

[[377401]]

本文转载自微信公众号「三太子敖丙」,作者三太子敖丙 。转载本文请联系三太子敖丙公众号。

前言

在实际业务开发中,会碰到夏令时,闰秒,时区转换的问题,这些问题都需要从业务角度去考虑,保证用户在任何地区看到的数据都一致的,这就需要MySQL数据库、后端服务以及前端服务做相应的处理才能完成。

最近我也刚好在开发的时候遇到了,所幸就写下这个比较冷门的文章,跟大家聊聊夏令时,闰秒,时区转换在实际开发过程中的解决方案。

夏令时

夏令时介绍

夏令时(Daylight Saving Time: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(Network Time Protocol)服务来实现,每隔一段时间会跟时钟源进行校对,确保Linux系统时间的准确性,同时Linux操作系统支持不同国家及地区的时区设置,所有时区信息位于/usr/share/zoneinfo目录下,如果需要设置时区,只需要将/etc/localtime软链接到一个具体的地区即可,如果这个地区有DST机制,那么Linux会自动在DST和标准时间之间切换,不需要额外的代码来处理。

  1. ## Linux支持的区域信息 
  2. $ ls -ltr /usr/share/zoneinfo/ 
  3. total 320 
  4. lrwxrwxrwx  1 root root     3 10月 23 05:18 Zulu -> UCT 
  5. -rw-r--r--  1 root root  1544 10月 23 05:18 W-SU 
  6. -rw-r--r--  1 root root  1873 10月 23 05:18 WET 
  7. lrwxrwxrwx  1 root root     3 10月 23 05:18 UTC -> UCT 
  8. lrwxrwxrwx  1 root root     3 10月 23 05:18 Universal -> UCT 
  9. -rw-r--r--  1 root root   127 10月 23 05:18 UCT 
  10. -rw-r--r--  1 root root  1970 10月 23 05:18 CET 
  11. ## 前端服务所在Linux服务器的时区 
  12. $ ls -ltr /etc/localtime  
  13. lrwxrwxrwx 1 root root 33 11月  1 06:20 /etc/localtime -> /usr/share/zoneinfo/Asia/Shanghai 

通过zdump命令查看下意大利罗马的时区属性。

  1. $ zdump -v /usr/share/zoneinfo/CET 
  2. /usr/share/zoneinfo/CET  Sun Mar 28 00:59:59 2021 UT = Sun Mar 28 01:59:59 2021 CET isdst=0 gmtoff=3600 
  3. /usr/share/zoneinfo/CET  Sun Mar 28 01:00:00 2021 UT = Sun Mar 28 03:00:00 2021 CEST isdst=1 gmtoff=7200 #2021年夏令时开始 
  4. /usr/share/zoneinfo/CET  Sun Oct 31 00:59:59 2021 UT = Sun Oct 31 02:59:59 2021 CEST isdst=1 gmtoff=7200 #2021年夏令时结束 

从上面的信息可以看到,2021年夏令时的开始时间是Sun Mar 28 01:00:00,结束时间为Sun Oct 31 00:59:59,isdst = 1说明当前处于DST时段,gmtoff=7200表示与格林治时间的offset,单位秒,即UTC + 02:00,也称为CEST时间,这说明Linux操作系统已经自动实现了下夏令时DST的自动切换。

处理夏令时

举个例子,意大利罗马的客户需要开发一个税务系统,用于国内各地市的税收记账,由于意大利是有夏令时制,就需要考虑夏令时DST的处理,在开发的过程中,涉及时间问题的包括MySQL数据库(mysql-server),后端服务(backend-service)以及前端服务(frontend-service)三个方面,下面就从三个层面分析如何去处理DST。

前端处理

业务对前端的要求是:不管使用的是移动端还是PC端,都应该正确的显示当时时间,包括有夏令时制的时间。

如果在中国的话,就比较好处理,没有DST机制,统一使用东八区即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。

  1. # 修改LInux时区为CET,也可以通过timedatectl命令修改。 
  2. $ ln -sf /usr/share/zoneinfo/CET /etc/localtime 

这样在意大利国内的用户的通过终端(移动端或PC端),登录系统缴税或查询时,用户时间和前端服务的时间完全一致,即完成如下这一步的处理。

后端处理

我们了解了前端Linux服务器的时区设置为CET,就能自动处理意大利DST夏令时转换了,后端Java程序部署在Linux服务器上,将其时区设置跟前端一样,也是CET时区,后端只需要接收前端传过来的值进行MySQL的CRUD操作即可,税务表的结构如下:

  1. CREATE TABLE `tax_form` ( 
  2.   `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id'
  3.   `tax_id` varchar(20) NOT NULL DEFAULT '' COMMENT '税务编号'
  4.   `amount` decimal(12,4) NOT NULL DEFAULT '0.0000' COMMENT '纳税金额'
  5.   `tax_payer_id` varchar(20) NOT NULL DEFAULT '' COMMENT '纳税人编号'
  6.   `status` tinyint NOT NULL DEFAULT '0' COMMENT '缴税状态'
  7.   `audit_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '审核时间'
  8.   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
  9.   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间'
  10.   PRIMARY KEY (`id`) USING BTREE 
  11. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='税务记录表'

主要看一下缴税和审核接口,分别的对应的SQL语句如下:

缴税

  1. -- 缴税接口的对应的SQL 
  2. insert into tax_form(tax_id,amount,tax_payer_id,status) values('T001', 1234.56, 'U001', 0) 

审核

  1. -- 修改审核状态 
  2. update tax_form set status = 1, audit_time = '2021-01-07 12:02:30' where  tax_payer_id = 'U001'

涉及时间的字段有两类

**公共字段:**create_time,update_time这些是每个表必须有的时间字段,而且默认都是MySQL的CURRENT_TIMESTAMP,取的MySQL server的当前系统时间,而这个时间是跟MySQL的时区time_zone设置不同而变化的,同时MySQL也是支持夏令时DST自动转换的。

业务字段:audit_time审核时间属性是由前端页面传到后端进行处理,后端无需做任何转换。

MySQL处理

MySQL也是支持夏令时DST机制,不过设置时区time_zone只能设置为地区(类似Linux设置时区一样),不能石永红MySQL设置的时区的相关变量

  1. mysql> show variables like '%zone%'
  2. +------------------+--------+ 
  3. | Variable_name    | Value  | 
  4. +------------------+--------+ 
  5. | system_time_zone | CST    |   -- 数据库服务器的当前时区,不可修改,CST这里指的是中国标准时间(China Standard Time UTC+08:00,即东八区) 
  6. | time_zone        | SYSTEM |   -- 数据库时区,默认跟服务器保持一致,可修改。 

目前是东八区,修改为意大利时区,即东一区。

  1. mysql> select now(); 
  2. +---------------------+ 
  3. | now()               | 
  4. +---------------------+ 
  5. | 2021-01-07 13:43:31  
  6. -- 修改数据库时区为零时区,即。 
  7. mysql> set time_zone = 'CET'
  8. ERROR 1298 (HY000): Unknown or incorrect time zone: 'CET' 
  9. -- 尝试通过+0:00方式修改,可以成功修改。 
  10. mysql> set time_zone = '+1:00'
  11. Query OK, 0 rows affected (0.00 sec) 

MySQL存储时区信息的数据字典

  1. mysql> show tables from mysql like '%time_zone%'
  2. +-------------------------------+ 
  3. | Tables_in_mysql (%time_zone%) | 
  4. +-------------------------------+ 
  5. | time_zone                     |   -- 时区信息 
  6. | time_zone_leap_second         |   -- 时区闰秒信息 
  7. | time_zone_name                |   -- 时区名 
  8. | time_zone_transition          |   -- 时区转换 
  9. | time_zone_transition_type     |   -- 时区转换类型 

默认情况下,这些表都是空的,需要通过MySQL专门提供的命令mysql_tzinfo_to_sql导入,数据会被插入到表time_zone相关的表中。

  1. # Linux下的时区信息/usr/share/zoneinfo通过命令mysql_tzinfo_to_sql加载到相关的time_zone表中。 
  2. $ mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql 

执行完成之后,看一下表中的数据,再尝试设置时区为CET。

  1. mysql> select * from mysql.time_zone_name where name like '%CET%'
  2. +------+--------------+ 
  3. Name | Time_zone_id | 
  4. +------+--------------+ 
  5. | CET  |          373 | 
  6. -- 设置时区为CET 
  7. mysql> set time_zone = 'CET'
  8. Query OK, 0 rows affected (0.02 sec) 
  9. mysql> select now(); 
  10. +---------------------+ 
  11. | now()               | 
  12. +---------------------+ 
  13. | 2021-01-07 10:00:36 | 

而且也支持时区转换,例如将北京时间转换成罗马时间。

  1. -- 北京时间17:00:00转换成CET的罗马时间就是10:00:00 
  2. mysql> select convert_tz('2021-01-07 17:00:00''Asia/Shanghai''CET'as time
  3. +---------------------+ 
  4. time                | 
  5. +---------------------+ 
  6. | 2021-01-07 10:00:00 | 

我们要解决的问题是:MySQL设置time_zone='CET'后是否能自动实现DST转换,如果可以的话,那么用户端、前端服务、后端服务以及MySQL服务器时区就统一为CET,同时都能自动处理DST,从上面的zdump -v /usr/share/zoneinfo/CET命令输出可以看到,2021年意大利的夏令时从3月28号01:59:59号开始,也就是时间调快一小时。

  1. -- 01:59:59时间点,没有发生DST切换。 
  2. mysql> select convert_tz('2021-03-28 01:59:59''+1:00''CET'as time
  3. +---------------------+ 
  4. time                | 
  5. +---------------------+ 
  6. | 2021-03-28 01:59:59 |  
  7. -- 02:00:00时间点,确实发生DST切换,从02:00:00调快了一小时变成了03:00:00 
  8. mysql> select convert_tz('2021-03-28 02:00:00''+1:00''CET'as time
  9. +---------------------+ 
  10. time                | 
  11. +---------------------+ 
  12. | 2021-03-28 03:00:00 | 
  13. -- 将+1:00时间换成CET,结果也是一样的,发生了DST切换。 
  14. mysql> select convert_tz('2021-03-28 02:00:00''CET''CET'as time
  15. +---------------------+ 
  16. time                | 
  17. +---------------------+ 
  18. | 2021-03-28 03:00:00 | 

从上面的结果可以看到,当time_zone设置成地区/城市,系统会自动解决夏令时DSTQ切换问题,如果设置time_zone='+1:00’这种方式就失去了夏令时机制,目前在MySQL数据库中,在初始化time_zone相关表元数据以后,MySQL就可以自己完成夏令时的修正,不需要额外的服务处理。

对于AWS RDS的来说,time_zone是可以选择地区/城市的,也就是支持夏令时的自动切换。

处理夏令时总结

通过上面的分析可以知道,Linux服务器和MySQL服务器都可以自动处理DST切换,前提是需要设置Linux的时区和MySQL时区为地区,例如都设置为CET。

闰秒

指为保持协调世界时接近于世界时时刻,由国际计量局统一规定在年底或年中(也可能在季末)对协调世界时增加或减少1秒的调整。最近一次闰秒在北京时间2017年1月1日7时59分59秒(时钟显示07:59:60)出现。

在实际的业务系统,受闰秒影响的有Linux服务器,Java代码以及MySQL数据库,我们来看看它们分别是怎么解决的LeapSecond问题的。

Linux服务器

对于大多数新的linux内核(2.6.x内核以后是支持LeapSecond,在这之前可能会导致Linux Kernel Crash),在设计时都是支持闰秒的,Linux操作系统时间是通过NTP服务来和时钟源来进行同步,NTP会一级一级地下发闰秒事件通知直到最边缘的NTP服务器,然后NTP就会把闰秒通知给客户端的操作系统,由操作系统来处理闰秒通知。

对于闰秒2017-01-01 07:59:60,Linux内核需要处理这个时间,就需要做一些特定的处理,一般会有以下三种方案。

  • 后退一秒
  • 停止一秒
  • 真正的增加一秒

第一种方式会导致一些基于timestamp的消息通知乱序了,而第二种会导致出现两个一模一样的timestamp,而最后一种不会出现timestamp的问题,也是后面Linux内核选择的处理方案。

  1. mysql> select UNIX_TIMESTAMP('2017-01-01 07:59:59'as nts; 
  2. +------------+ 
  3. | nts        | 
  4. +------------+ 
  5. | 1483257599 | 
  6. date -d '@1483257599' --utc 
  7. Sun Jan  1 07:59:59 UTC 2017 
  8. date -d '@1483257600' --utc 
  9. Sun Jan  1 08:00:00 UTC 2017 

从这里可以看到,Linux采用的是第三种方案:真正的增加一秒,这也符合业务系统的需求。

Java代码

Java代码的System.currentTimeMillis()会产生闰秒60,是取决于Linux操作系统的,在Linux Kernel 2.6.x之后已经fix了LeapSecond问题。

MySQL数据库

上面看到了在MySQL下已经有了mysql.time_zone_leap_second数据字典,说明已经支持了LeapSecond,处理方案跟Linux类似。

  1. -- 创建一张测试表存储timestamp时间戳 
  2. CREATE TABLE ls(   
  3. id bigint NOT NULL COMMENT 'id'
  4. ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  5. PRIMARY KEY (id)); 
  6. -- 设置数据库时区为UTC 
  7. mysql> set time_zone = 'UTC'
  8. mysql> set timestamp = 1483257599;  --对应时间:2017-01-01 07:59:59 
  9. mysql> insert into ls(id) values(1); 
  10. mysql> set timestamp = 1483257600;  --对应时间:2017-01-01 07:59:60 
  11. mysql> insert into ls(id) values(2); 
  12. -- 可以看到MySQL对闰秒进行了处理,将07:59:60转换成了08:00:00。 
  13. mysql> select id, ts, unix_timestamp(ts) from ls; 
  14. +----+---------------------+--------------------+ 
  15. | id | ts                  | unix_timestamp(ts) | 
  16. +----+---------------------+--------------------+ 
  17. |  1 | 2017-01-01 07:59:59 |         1483257599 | 
  18. |  2 | 2017-01-01 08:00:00 |         1483257600 | 
  19. -- 通过闰秒时间查询会报错 
  20. mysql> select * from ls where ts = '2017-01-01 07:59:60'
  21. ERROR 1525 (HY000): Incorrect TIMESTAMP value: '2017-01-01 07:59:60' 
  22. mysql> select * from ls where ts = '2017-01-01 08:00:00'
  23. +----+---------------------+ 
  24. | id | ts                  | 
  25. +----+---------------------+ 
  26. |  2 | 2017-01-01 08:00:00 | 

跨境系统的时间处理

上面介绍的意大利罗马的税务系统,其实属于政企业务,只服务于国内的用户的需求,不涉及海外用户的请求,相对来说地区和人员都比较固定,但是像这种跨境电商巨头ebay,它服务的用户遍布全球各地,而且每个地区的时区不同,同时每个时区的夏令时DST的起始时间也不一样,我们要解决的是要根据客户所在地区显示正确的时间(包括DST),跟前面的DST处理一样也涉及到三端处理:前端服务(frontend-service),后端服务(backend-service)以及MySQL数据库(mysql-server)。

从这个图上可以看到,前端服务的UI层跟用户所在的地区时间要完全一致,至于后端服务和MySQL如何处理时间,对于用户来说根本不关心的,这就要求前端必须要根据不同地区,不同时区,不同夏令时DST产生不同的时间的用户进行转换处理,不同地区的时间转换目前前端(Vue/React)已经有现成的插件可直接使用。

同时希望只在前端处理用户时间的转入和转出,后端和MySQL数据库不做任何修改就能完成业务处理和数据存储。

北京用户在UTC + 8也就是东八区,而罗马用户在UTC + 1东一区,都是在UTC的基础上做处理,那我们就可以将时区都设置为UTC,然后根据用户所在地区进行相应的处理。

MySQL处理

设置MySQL数据库的时区为UTC,不管用户来自哪个地区存储在数据库的时间都是UTC,包括公共时间字段(创建时间,修改时间)以及业务时间字段(交易开始时间,交易结束时间)。

-- 设置数据库时区为UTC,即零时区

  1. -- 设置数据库时区为UTC,即零时区 
  2. set global time_zone = 'UTC' 

后端处理

MySQL时区是UTC,那么后端服务的所在的Linux Server时区统一设置为UTC,跟MySQL保持一致,这样后端就不需要做任何转换。

前端处理

前端拿到标准时区UTC的数据,统一根据用户所在时区进行转换,这样保证与后端数据时区的一致性,前端根据实际情况进行渲染。一般来讲,前端将时间数据传递到后端,后端封装成timestamp后存储在MySQL中对应timestamp类型(MySQL中的timestamp是不区分时区的,例如数据库是UTC 02:00:00,北京用户使用ebay在CST 10:00:00下单,数据库中的订单表的create_time就应该存储2020-12-03 10:00:00),同时前端查询数据的也要做相应的转换处理。

定时任务

后端服务一般都会一些定时任务,这个时间一般取自Linux OS的时间,跟前端没关系,基于Linux的UTC时区做相应的调整即可。

总结

上面介绍了夏令时,闰秒以及跨境系统的时间处理问题,主要涉及到MySQL数据库,后端服务以及前端服务三个层面,对于夏令时,闰秒的转换处理,Linux和MySQL都可以自动完成处理,不需要额外转换;对于跨境系统的时间处理,通过设置Linux和MySQL时区为UTC,只需要前端服务处理不同地区用户时间问题,降低了系统改造的风险,今天就聊这么多,希望对大家有所帮助。

 

责任编辑:武晓燕 来源: 三太子敖丙
相关推荐

2023-04-28 07:44:44

MyBatis查询SQL

2023-08-30 07:27:39

2024-04-08 00:00:00

asyncawaiPromise

2022-02-10 09:04:50

架构

2019-09-20 08:47:57

DockerLinux软件

2013-02-27 10:27:44

GitHub

2024-12-04 08:40:19

2015-03-24 14:02:45

程序员

2024-02-19 00:00:00

Docker轻量级容器

2017-12-11 15:04:58

404错误HTTP代码

2024-04-10 10:15:16

监听

2020-06-29 18:54:39

大数据新发地疫情

2024-11-26 00:45:29

free区域字段

2010-01-06 15:36:30

Linux操作系统

2023-10-08 09:42:41

GitHubDataTable​Fill

2020-12-08 09:25:41

死锁MySQL数据库

2024-12-11 08:19:34

2020-12-17 08:56:51

单例模式JVM

2022-12-09 09:46:55

插件Lombok

2022-08-11 17:14:37

Java
点赞
收藏

51CTO技术栈公众号