新聞中心
本文轉(zhuǎn)載自微信公眾號「三太子敖丙」,作者三太子敖丙 。轉(zhuǎn)載本文請聯(lián)系三太子敖丙公眾號。

專業(yè)從事成都網(wǎng)站建設(shè)、網(wǎng)站建設(shè),高端網(wǎng)站制作設(shè)計,微信小程序定制開發(fā),網(wǎng)站推廣的成都做網(wǎng)站的公司。優(yōu)秀技術(shù)團隊竭力真誠服務(wù),采用H5建站+CSS3前端渲染技術(shù),響應(yīng)式網(wǎng)站,讓網(wǎng)站在手機、平板、PC、微信下都能呈現(xiàn)。建站過程建立專項小組,與您實時在線互動,隨時提供解決方案,暢聊想法和感受。
前言
在實際業(yè)務(wù)開發(fā)中,會碰到夏令時,閏秒,時區(qū)轉(zhuǎn)換的問題,這些問題都需要從業(yè)務(wù)角度去考慮,保證用戶在任何地區(qū)看到的數(shù)據(jù)都一致的,這就需要MySQL數(shù)據(jù)庫、后端服務(wù)以及前端服務(wù)做相應(yīng)的處理才能完成。
最近我也剛好在開發(fā)的時候遇到了,所幸就寫下這個比較冷門的文章,跟大家聊聊夏令時,閏秒,時區(qū)轉(zhuǎn)換在實際開發(fā)過程中的解決方案。
夏令時
夏令時介紹
夏令時(Daylight Saving Time:DST):又稱"日光節(jié)約時制",是一種為節(jié)約能源而人為規(guī)定地方時間的制度,在這一制度實行期間所采用的統(tǒng)一時間稱為“夏令時間”。
一般在天亮早的夏季人為將時間調(diào)快一小時,可以使人早起早睡,減少照明量,以充分利用光照資源,從而節(jié)約照明用電,除了夏令時外還有冬令時,采用的是本地的標準時間。
可以看到意大利是有夏令時制,夏令時的時間從3月28日到10月31日,冬令時(本地標準時間)是從11月1日到3月27日,在夏令時時段內(nèi),時間比標準時間快一個小時,例如羅馬市的時區(qū)GMT + 1:00,標準時間為10:00:00,在夏令時的時間就是11:00:00,冬令時的時間就是10:00:00。
CET (中歐標準時間) 是UTC + 01:00時區(qū)的名稱之一,比UTC(世界標準時間)提前1個小時,與UTC的時間偏差可寫為+01:00,在冬天使用,在夏季時使用CEST - 中歐夏令時間 (UTC + 02:00,提前一個小時)。
LInux時區(qū)
Linux服務(wù)器的系統(tǒng)時間的校準是通過NTP(Network Time Protocol)服務(wù)來實現(xiàn),每隔一段時間會跟時鐘源進行校對,確保Linux系統(tǒng)時間的準確性,同時Linux操作系統(tǒng)支持不同國家及地區(qū)的時區(qū)設(shè)置,所有時區(qū)信息位于/usr/share/zoneinfo目錄下,如果需要設(shè)置時區(qū),只需要將/etc/localtime軟鏈接到一個具體的地區(qū)即可,如果這個地區(qū)有DST機制,那么Linux會自動在DST和標準時間之間切換,不需要額外的代碼來處理。
- ## Linux支持的區(qū)域信息
- $ ls -ltr /usr/share/zoneinfo/
- total 320
- lrwxrwxrwx 1 root root 3 10月 23 05:18 Zulu -> UCT
- -rw-r--r-- 1 root root 1544 10月 23 05:18 W-SU
- -rw-r--r-- 1 root root 1873 10月 23 05:18 WET
- lrwxrwxrwx 1 root root 3 10月 23 05:18 UTC -> UCT
- lrwxrwxrwx 1 root root 3 10月 23 05:18 Universal -> UCT
- -rw-r--r-- 1 root root 127 10月 23 05:18 UCT
- -rw-r--r-- 1 root root 1970 10月 23 05:18 CET
- ## 前端服務(wù)所在Linux服務(wù)器的時區(qū)
- $ ls -ltr /etc/localtime
- lrwxrwxrwx 1 root root 33 11月 1 06:20 /etc/localtime -> /usr/share/zoneinfo/Asia/Shanghai
通過zdump命令查看下意大利羅馬的時區(qū)屬性。
- $ zdump -v /usr/share/zoneinfo/CET
- /usr/share/zoneinfo/CET Sun Mar 28 00:59:59 2021 UT = Sun Mar 28 01:59:59 2021 CET isdst=0 gmtoff=3600
- /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年夏令時開始
- /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年夏令時結(jié)束
從上面的信息可以看到,2021年夏令時的開始時間是Sun Mar 28 01:00:00,結(jié)束時間為Sun Oct 31 00:59:59,isdst = 1說明當前處于DST時段,gmtoff=7200表示與格林治時間的offset,單位秒,即UTC + 02:00,也稱為CEST時間,這說明Linux操作系統(tǒng)已經(jīng)自動實現(xiàn)了下夏令時DST的自動切換。
處理夏令時
舉個例子,意大利羅馬的客戶需要開發(fā)一個稅務(wù)系統(tǒng),用于國內(nèi)各地市的稅收記賬,由于意大利是有夏令時制,就需要考慮夏令時DST的處理,在開發(fā)的過程中,涉及時間問題的包括MySQL數(shù)據(jù)庫(mysql-server),后端服務(wù)(backend-service)以及前端服務(wù)(frontend-service)三個方面,下面就從三個層面分析如何去處理DST。
前端處理
業(yè)務(wù)對前端的要求是:不管使用的是移動端還是PC端,都應(yīng)該正確的顯示當時時間,包括有夏令時制的時間。
如果在中國的話,就比較好處理,沒有DST機制,統(tǒng)一使用東八區(qū)即GMT/UTC + 08:00即可,前端服務(wù)的時間直接取Linux服務(wù)服務(wù)器的系統(tǒng)時間,Linux的時區(qū)只需要設(shè)置為Asia/Shanghai即可,前端不需要做任何時間的轉(zhuǎn)入轉(zhuǎn)出。
**轉(zhuǎn)入:**指POST請求寫入數(shù)據(jù),user —> frontend-service —> backend-service —> mysql-server,例如繳稅接口。
**轉(zhuǎn)出:**指GET請求查詢數(shù)據(jù),mysql-server —> backend-service —> frontend-service —> user,例如查詢接口。
不過開心的是,Linux操作系統(tǒng)已經(jīng)自動實現(xiàn)了DST轉(zhuǎn)換,在前端不需要做任何處理,設(shè)置Linux時區(qū)為CET。
- # 修改LInux時區(qū)為CET,也可以通過timedatectl命令修改。
- $ ln -sf /usr/share/zoneinfo/CET /etc/localtime
這樣在意大利國內(nèi)的用戶的通過終端(移動端或PC端),登錄系統(tǒng)繳稅或查詢時,用戶時間和前端服務(wù)的時間完全一致,即完成如下這一步的處理。
后端處理
我們了解了前端Linux服務(wù)器的時區(qū)設(shè)置為CET,就能自動處理意大利DST夏令時轉(zhuǎn)換了,后端Java程序部署在Linux服務(wù)器上,將其時區(qū)設(shè)置跟前端一樣,也是CET時區(qū),后端只需要接收前端傳過來的值進行MySQL的CRUD操作即可,稅務(wù)表的結(jié)構(gòu)如下:
- CREATE TABLE `tax_form` (
- `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
- `tax_id` varchar(20) NOT NULL DEFAULT '' COMMENT '稅務(wù)編號',
- `amount` decimal(12,4) NOT NULL DEFAULT '0.0000' COMMENT '納稅金額',
- `tax_payer_id` varchar(20) NOT NULL DEFAULT '' COMMENT '納稅人編號',
- `status` tinyint NOT NULL DEFAULT '0' COMMENT '繳稅狀態(tài)',
- `audit_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '審核時間',
- `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間',
- `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='稅務(wù)記錄表';
主要看一下繳稅和審核接口,分別的對應(yīng)的SQL語句如下:
繳稅
- -- 繳稅接口的對應(yīng)的SQL
- insert into tax_form(tax_id,amount,tax_payer_id,status) values('T001', 1234.56, 'U001', 0)
審核
- -- 修改審核狀態(tài)
- 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的當前系統(tǒng)時間,而這個時間是跟MySQL的時區(qū)time_zone設(shè)置不同而變化的,同時MySQL也是支持夏令時DST自動轉(zhuǎn)換的。
業(yè)務(wù)字段:audit_time審核時間屬性是由前端頁面?zhèn)鞯胶蠖诉M行處理,后端無需做任何轉(zhuǎn)換。
MySQL處理
MySQL也是支持夏令時DST機制,不過設(shè)置時區(qū)time_zone只能設(shè)置為地區(qū)(類似Linux設(shè)置時區(qū)一樣),不能石永紅MySQL設(shè)置的時區(qū)的相關(guān)變量
- mysql> show variables like '%zone%';
- +------------------+--------+
- | Variable_name | Value |
- +------------------+--------+
- | system_time_zone | CST | -- 數(shù)據(jù)庫服務(wù)器的當前時區(qū),不可修改,CST這里指的是中國標準時間(China Standard Time UTC+08:00,即東八區(qū))
- | time_zone | SYSTEM | -- 數(shù)據(jù)庫時區(qū),默認跟服務(wù)器保持一致,可修改。
目前是東八區(qū),修改為意大利時區(qū),即東一區(qū)。
- mysql> select now();
- +---------------------+
- | now() |
- +---------------------+
- | 2021-01-07 13:43:31
- -- 修改數(shù)據(jù)庫時區(qū)為零時區(qū),即。
- mysql> set time_zone = 'CET';
- ERROR 1298 (HY000): Unknown or incorrect time zone: 'CET'
- -- 嘗試通過+0:00方式修改,可以成功修改。
- mysql> set time_zone = '+1:00';
- Query OK, 0 rows affected (0.00 sec)
MySQL存儲時區(qū)信息的數(shù)據(jù)字典
- mysql> show tables from mysql like '%time_zone%';
- +-------------------------------+
- | Tables_in_mysql (%time_zone%) |
- +-------------------------------+
- | time_zone | -- 時區(qū)信息
- | time_zone_leap_second | -- 時區(qū)閏秒信息
- | time_zone_name | -- 時區(qū)名
- | time_zone_transition | -- 時區(qū)轉(zhuǎn)換
- | time_zone_transition_type | -- 時區(qū)轉(zhuǎn)換類型
默認情況下,這些表都是空的,需要通過MySQL專門提供的命令mysql_tzinfo_to_sql導(dǎo)入,數(shù)據(jù)會被插入到表time_zone相關(guān)的表中。
- # Linux下的時區(qū)信息/usr/share/zoneinfo通過命令mysql_tzinfo_to_sql加載到相關(guān)的time_zone表中。
- $ mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
執(zhí)行完成之后,看一下表中的數(shù)據(jù),再嘗試設(shè)置時區(qū)為CET。
- mysql> select * from mysql.time_zone_name where name like '%CET%';
- +------+--------------+
- | Name | Time_zone_id |
- +------+--------------+
- | CET | 373 |
- -- 設(shè)置時區(qū)為CET
- mysql> set time_zone = 'CET';
- Query OK, 0 rows affected (0.02 sec)
- mysql> select now();
- +---------------------+
- | now() |
- +---------------------+
- | 2021-01-07 10:00:36 |
而且也支持時區(qū)轉(zhuǎn)換,例如將北京時間轉(zhuǎn)換成羅馬時間。
- -- 北京時間17:00:00轉(zhuǎn)換成CET的羅馬時間就是10:00:00
- mysql> select convert_tz('2021-01-07 17:00:00', 'Asia/Shanghai', 'CET') as time;
- +---------------------+
- | time |
- +---------------------+
- | 2021-01-07 10:00:00 |
我們要解決的問題是:MySQL設(shè)置time_zone='CET'后是否能自動實現(xiàn)DST轉(zhuǎn)換,如果可以的話,那么用戶端、前端服務(wù)、后端服務(wù)以及MySQL服務(wù)器時區(qū)就統(tǒng)一為CET,同時都能自動處理DST,從上面的zdump -v /usr/share/zoneinfo/CET命令輸出可以看到,2021年意大利的夏令時從3月28號01:59:59號開始,也就是時間調(diào)快一小時。
- -- 01:59:59時間點,沒有發(fā)生DST切換。
- mysql> select convert_tz('2021-03-28 01:59:59', '+1:00', 'CET') as time;
- +---------------------+
- | time |
- +---------------------+
- | 2021-03-28 01:59:59 |
- -- 02:00:00時間點,確實發(fā)生DST切換,從02:00:00調(diào)快了一小時變成了03:00:00
- mysql> select convert_tz('2021-03-28 02:00:00', '+1:00', 'CET') as time;
- +---------------------+
- | time |
- +---------------------+
- | 2021-03-28 03:00:00 |
- -- 將+1:00時間換成CET,結(jié)果也是一樣的,發(fā)生了DST切換。
- mysql> select convert_tz('2021-03-28 02:00:00', 'CET', 'CET') as time;
- +---------------------+
- | time |
- +---------------------+
- | 2021-03-28 03:00:00 |
從上面的結(jié)果可以看到,當time_zone設(shè)置成地區(qū)/城市,系統(tǒng)會自動解決夏令時DSTQ切換問題,如果設(shè)置time_zone='+1:00’這種方式就失去了夏令時機制,目前在MySQL數(shù)據(jù)庫中,在初始化time_zone相關(guān)表元數(shù)據(jù)以后,MySQL就可以自己完成夏令時的修正,不需要額外的服務(wù)處理。
對于AWS RDS的來說,time_zone是可以選擇地區(qū)/城市的,也就是支持夏令時的自動切換。
處理夏令時總結(jié)
通過上面的分析可以知道,Linux服務(wù)器和MySQL服務(wù)器都可以自動處理DST切換,前提是需要設(shè)置Linux的時區(qū)和MySQL時區(qū)為地區(qū),例如都設(shè)置為CET。
閏秒
指為保持協(xié)調(diào)世界時接近于世界時時刻,由國際計量局統(tǒng)一規(guī)定在年底或年中(也可能在季末)對協(xié)調(diào)世界時增加或減少1秒的調(diào)整。最近一次閏秒在北京時間2017年1月1日7時59分59秒(時鐘顯示07:59:60)出現(xiàn)。
在實際的業(yè)務(wù)系統(tǒng),受閏秒影響的有Linux服務(wù)器,Java代碼以及MySQL數(shù)據(jù)庫,我們來看看它們分別是怎么解決的LeapSecond問題的。
Linux服務(wù)器
對于大多數(shù)新的linux內(nèi)核(2.6.x內(nèi)核以后是支持LeapSecond,在這之前可能會導(dǎo)致Linux Kernel Crash),在設(shè)計時都是支持閏秒的,Linux操作系統(tǒng)時間是通過NTP服務(wù)來和時鐘源來進行同步,NTP會一級一級地下發(fā)閏秒事件通知直到最邊緣的NTP服務(wù)器,然后NTP就會把閏秒通知給客戶端的操作系統(tǒng),由操作系統(tǒng)來處理閏秒通知。
對于閏秒2017-01-01 07:59:60,Linux內(nèi)核需要處理這個時間,就需要做一些特定的處理,一般會有以下三種方案。
- 后退一秒
- 停止一秒
- 真正的增加一秒
第一種方式會導(dǎo)致一些基于timestamp的消息通知亂序了,而第二種會導(dǎo)致出現(xiàn)兩個一模一樣的timestamp,而最后一種不會出現(xiàn)timestamp的問題,也是后面Linux內(nèi)核選擇的處理方案。
- mysql> select UNIX_TIMESTAMP('2017-01-01 07:59:59') as nts;
- +------------+
- | nts |
- +------------+
- | 1483257599 |
- $ date -d '@1483257599' --utc
- Sun Jan 1 07:59:59 UTC 2017
- $ date -d '@1483257600' --utc
- Sun Jan 1 08:00:00 UTC 2017
從這里可以看到,Linux采用的是第三種方案:真正的增加一秒,這也符合業(yè)務(wù)系統(tǒng)的需求。
Java代碼
Java代碼的System.currentTimeMillis()會產(chǎn)生閏秒60,是取決于Linux操作系統(tǒng)的,在Linux Kernel 2.6.x之后已經(jīng)fix了LeapSecond問題。
MySQL數(shù)據(jù)庫
上面看到了在MySQL下已經(jīng)有了mysql.time_zone_leap_second數(shù)據(jù)字典,說明已經(jīng)支持了LeapSecond,處理方案跟Linux類似。
- -- 創(chuàng)建一張測試表存儲timestamp時間戳
- CREATE TABLE ls(
- id bigint NOT NULL COMMENT 'id',
- ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (id));
- -- 設(shè)置數(shù)據(jù)庫時區(qū)為UTC
- mysql> set time_zone = 'UTC';
- mysql> set timestamp = 1483257599; --對應(yīng)時間:2017-01-01 07:59:59
- mysql> insert into ls(id) values(1);
- mysql> set timestamp = 1483257600; --對應(yīng)時間:2017-01-01 07:59:60
- mysql> insert into ls(id) values(2);
- -- 可以看到MySQL對閏秒進行了處理,將07:59:60轉(zhuǎn)換成了08:00:00。
- mysql> select id, ts, unix_timestamp(ts) from ls;
- +----+---------------------+--------------------+
- | id | ts | unix_timestamp(ts) |
- +----+---------------------+--------------------+
- | 1 | 2017-01-01 07:59:59 | 1483257599 |
- | 2 | 2017-01-01 08:00:00 | 1483257600 |
- -- 通過閏秒時間查詢會報錯
- mysql> select * from ls where ts = '2017-01-01 07:59:60';
- ERROR 1525 (HY000): Incorrect TIMESTAMP value: '2017-01-01 07:59:60'
- mysql> select * from ls where ts = '2017-01-01 08:00:00';
- +----+---------------------+
- | id | ts |
- +----+---------------------+
- | 2 | 2017-01-01 08:00:00 |
跨境系統(tǒng)的時間處理
上面介紹的意大利羅馬的稅務(wù)系統(tǒng),其實屬于政企業(yè)務(wù),只服務(wù)于國內(nèi)的用戶的需求,不涉及海外用戶的請求,相對來說地區(qū)和人員都比較固定,但是像這種跨境電商巨頭ebay,它服務(wù)的用戶遍布全球各地,而且每個地區(qū)的時區(qū)不同,同時每個時區(qū)的夏令時DST的起始時間也不一樣,我們要解決的是要根據(jù)客戶所在地區(qū)顯示正確的時間(包括DST),跟前面的DST處理一樣也涉及到三端處理:前端服務(wù)(frontend-service),后端服務(wù)(backend-service)以及MySQL數(shù)據(jù)庫(mysql-server)。
從這個圖上可以看到,前端服務(wù)的UI層跟用戶所在的地區(qū)時間要完全一致,至于后端服務(wù)和MySQL如何處理時間,對于用戶來說根本不關(guān)心的,這就要求前端必須要根據(jù)不同地區(qū),不同時區(qū),不同夏令時DST產(chǎn)生不同的時間的用戶進行轉(zhuǎn)換處理,不同地區(qū)的時間轉(zhuǎn)換目前前端(Vue/React)已經(jīng)有現(xiàn)成的插件可直接使用。
同時希望只在前端處理用戶時間的轉(zhuǎn)入和轉(zhuǎn)出,后端和MySQL數(shù)據(jù)庫不做任何修改就能完成業(yè)務(wù)處理和數(shù)據(jù)存儲。
北京用戶在UTC + 8也就是東八區(qū),而羅馬用戶在UTC + 1東一區(qū),都是在UTC的基礎(chǔ)上做處理,那我們就可以將時區(qū)都設(shè)置為UTC,然后根據(jù)用戶所在地區(qū)進行相應(yīng)的處理。
MySQL處理
設(shè)置MySQL數(shù)據(jù)庫的時區(qū)為UTC,不管用戶來自哪個地區(qū)存儲在數(shù)據(jù)庫的時間都是UTC,包括公共時間字段(創(chuàng)建時間,修改時間)以及業(yè)務(wù)時間字段(交易開始時間,交易結(jié)束時間)。
-- 設(shè)置數(shù)據(jù)庫時區(qū)為UTC,即零時區(qū)
- -- 設(shè)置數(shù)據(jù)庫時區(qū)為UTC,即零時區(qū)
- set global time_zone = 'UTC'
后端處理
MySQL時區(qū)是UTC,那么后端服務(wù)的所在的Linux Server時區(qū)統(tǒng)一設(shè)置為UTC,跟MySQL保持一致,這樣后端就不需要做任何轉(zhuǎn)換。
前端處理
前端拿到標準時區(qū)UTC的數(shù)據(jù),統(tǒng)一根據(jù)用戶所在時區(qū)進行轉(zhuǎn)換,這樣保證與后端數(shù)據(jù)時區(qū)的一致性,前端根據(jù)實際情況進行渲染。一般來講,前端將時間數(shù)據(jù)傳遞到后端,后端封裝成timestamp后存儲在MySQL中對應(yīng)timestamp類型(MySQL中的timestamp是不區(qū)分時區(qū)的,例如數(shù)據(jù)庫是UTC 02:00:00,北京用戶使用ebay在CST 10:00:00下單,數(shù)據(jù)庫中的訂單表的create_time就應(yīng)該存儲2020-12-03 10:00:00),同時前端查詢數(shù)據(jù)的也要做相應(yīng)的轉(zhuǎn)換處理。
定時任務(wù)
后端服務(wù)一般都會一些定時任務(wù),這個時間一般取自Linux OS的時間,跟前端沒關(guān)系,基于Linux的UTC時區(qū)做相應(yīng)的調(diào)整即可。
總結(jié)
上面介紹了夏令時,閏秒以及跨境系統(tǒng)的時間處理問題,主要涉及到MySQL數(shù)據(jù)庫,后端服務(wù)以及前端服務(wù)三個層面,對于夏令時,閏秒的轉(zhuǎn)換處理,Linux和MySQL都可以自動完成處理,不需要額外轉(zhuǎn)換;對于跨境系統(tǒng)的時間處理,通過設(shè)置Linux和MySQL時區(qū)為UTC,只需要前端服務(wù)處理不同地區(qū)用戶時間問題,降低了系統(tǒng)改造的風險,今天就聊這么多,希望對大家有所幫助。
當前文章:你知道程序是怎么處理時區(qū)問題的么?
鏈接分享:http://www.dlmjj.cn/article/djopjds.html


咨詢
建站咨詢
