在國際化的業務場景中,時區問題是常見的。本文將就Web開發中的時區問題進行探索。前端
關於時區的概念,想必你們都有些瞭解。咱們的地球被劃分爲24個時區,北京時間爲東八區,而美國的太平洋時間爲西八區,和咱們差了16個小時。java
下面咱們從一個案例提及,咱們的服務器和數據庫部署在北京,而這時美國用戶經過瀏覽器但願能查詢北京時間下的「2020年7月1日8點-2020年7月1日18點」這10個小時的數據。
瀏覽器上選擇時間區域查詢數據mysql
爲了模擬瀏覽器在太平洋時間,只需將系統時間設置爲太平洋時間便可。而系統時間的改變會影響到JVM的默認時區,因此爲了讓服務器程序仍處於北京時間,須要經過代碼指定時區,以下:git
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
而數據庫MySQL的時區也設置爲北京時間,SQL以下:github
set global time_zone = '+8:00'; set time_zone = '+8:00'; flush privileges;
下面,讓咱們點擊查詢,先看下咱們發送的內容:
發送數據的格式sql
能夠看到開始時間和結束時間都比界面上顯示的時間多了8小時。這是由於我使用的ElementUI組件的日期時間選擇器,其默認時區爲0時區,因此會將咱們選擇的時間根據瀏覽器的時區(西八區)轉換成0時區的時間。最後傳輸的內容爲時間+時區的字符串表示。
時間-時區的字符串表示數據庫
前端把數據成功發出來了,下面咱們看下後端接收數據的狀況。後端我使用的是SpringBoot,Controller的代碼以下。json
@PostMapping("/time") public List<Data> test(@RequestBody TimeDto dto) { Date startTime = dto.getStartTime(); Date endTime = dto.getEndTime(); System.out.println(startTime); System.out.println(endTime); // 格林時間(0) String format = "yyyy-MM-dd HH:mm:ss"; SimpleDateFormat sdfGreen = new SimpleDateFormat(format); sdfGreen.setTimeZone(TimeZone.getTimeZone("GMT+0")); System.out.println("格林時間:" + sdfGreen.format(startTime) + "至" + sdfGreen.format(endTime)); // 北京時間(+8) SimpleDateFormat sdfBeijing = new SimpleDateFormat(format); sdfBeijing.setTimeZone(TimeZone.getTimeZone("GMT+8")); System.out.println("北京時間:" + sdfBeijing.format(startTime) + "至" + sdfBeijing.format(endTime)); // 太平洋時間(-8) SimpleDateFormat sdfPacific = new SimpleDateFormat(format); sdfPacific.setTimeZone(TimeZone.getTimeZone("GMT-8")); System.out.println("太平洋時間:" + sdfPacific.format(startTime) + "至" + sdfPacific.format(endTime)); List<Data> dataList = queryDate(dto); return dataList; } /** Thu Jul 02 00:00:00 GMT+08:00 2020 Thu Jul 02 10:00:00 GMT+08:00 2020 格林時間:2020-07-01 16:00:00至2020-07-02 02:00:00 北京時間:2020-07-02 00:00:00至2020-07-02 10:00:00 太平洋時間:2020-07-01 08:00:00至2020-07-01 18:00:00 **/
因爲JVM時區爲東八區,因此反序列化時獲得的Date對象也是東八區的時間,即2號0點-2號10點。若是咱們直接用startTime和endTime去查詢,獲得的將是北京時間2號0點到10點的數據,和預想的結果有差別。
時區問題致使的查詢時間範圍錯誤後端
那如何才能查詢到北京時間1號8點-1號18點的數據呢。因爲咱們前端傳輸的太平洋時間在後臺接收時發生時區轉換,因此能夠在前端直接傳輸須要查詢的北京時間。也就是1號8點-1號18點。經過設置el-date-picker的value-format屬性,指定選擇的時間格式「yyyy-MM-dd HH:mm:ss」,這樣傳輸的時間字符串將不具備時區屬性。瀏覽器
<el-date-picker v-model="dateTimeRange" type="datetimerange" range-separator="至" start-placeholder="開始日期" end-placeholder="結束日期" value-format="yyyy-MM-dd HH:mm:ss" > </el-date-picker>
修正後的發送數據格式
然後端若是不修改,將報出如下錯誤,沒法將該格式的時間轉換成Date對象。
JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2020-07-01 08:00:00": not a valid representation (error: Failed to parse Date value '2020-07-01 08:00:00': Cannot parse date "2020-07-01 08:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2020-07-01 08:00:00": not a valid representation (error: Failed to parse Date value '2020-07-01 08:00:00': Cannot parse date "2020-07-01 08:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null))↵ at [Source: (PushbackInputStream); line: 1, column: 14] (through reference chain: com.chaycao.timezone.TimeDto["startTime"])
因此爲能正確反序列化,須要爲jackjson作反序列化提供額外的信息。加上@JsonFormat註解,指定時區和時間格式,便能達到指望的效果,獲得的將是北京時間的1號8點和1號18點。因此,在先後端傳輸發生的時區問題,注意時間數據的序列化和反序列化方式就能解決。
public class TimeDto { @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") Date startTime; @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") Date endTime; //... }
下面咱們再看下數據庫中會發生的時區問題。
咱們將MySQL的時區改成太平洋時間。
set global time_zone = '-8:00'; set time_zone = '-8:00'; flush privileges;
看下查詢的結果是否會發生變化,查詢的程序以下:
private List<Data> queryDate(TimeDto dto) { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost/test?useSSL=false&useUnicode=true&characterEncoding=UTF8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai"); dataSource.setUsername("root"); dataSource.setPassword("caoniezi"); Date startTime = dto.getStartTime(); Date endTime = dto.getEndTime(); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); String sql = "SELECT * FROM data WHERE create_time >= ? and create_time <= ?"; List<Map<String, Object>> maps = jdbcTemplate.queryForList( sql, new Object[]{startTime, endTime}); List<Data> dataList = new ArrayList<>(); for (Map<String, Object> map : maps) { Data data = new Data(); data.setId((Integer) map.get("id")); data.setContent((String) map.get("content")); data.setCreateTime((Date) map.get("create_time")); dataList.add(data); } return dataList; }
查詢的結果仍然是「D,E,F」,看來數據庫時區的改變對於咱們本次查詢未產生影響。
修改MySQL時區後查詢時間範圍正確
這是由於在create_time字段的類型爲datetime,而datetime是沒有時區概念的,存儲的是格式爲YYYYMMDDHHMMSS(年月日時分秒)的整數,不會受到時區的影響。
而若是咱們先將時區改回東八區,將create_time的類型改成timestamp,再把時區改成西八區。查詢的結果是「H,I,J」。
set global time_zone = '+8:00'; set time_zone = '+8:00'; flush privileges; ALTER TABLE `data` MODIFY COLUMN `create_time` TIMESTAMP DEFAULT NULL; set global time_zone = '-8:00'; set time_zone = '-8:00'; flush privileges;
修改create_time字段類型爲timestamp
這是由於timestamp是有時區概念,存入的是自時間紀元以來的秒數,在咱們將類型改成timestamp時,create_time的值也會由東八區計算爲0時區的時間秒數存儲。當咱們以西八區查詢時,會減小16小時。
修改成timestamp後查詢
那如何才能在西八區的數據庫中查出咱們想要的數據。
jdbc鏈接url中的serverTimezone參數,其做用是爲驅動指定MySQL的時區,在以前的操做中,咱們修改了MySQL的時區,而serverTimezone未修改,仍然是東八區。
jdbc:mysql://localhost/test?useSSL=false&useUnicode=true&characterEncoding=UTF8&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
查詢狀況以下,MySQL驅動會根據指定的serverTimezone和JVM時區作轉換,因爲二者都是東八區,因此startTime和endTime的時間字符串不變,可是因爲MySQL時區已變爲西八區,查詢結果就落到了H、I、J上。
serverTimezone爲東八區的查詢狀況
下面咱們把serverTimezone去掉,在未指定serverTimezone的狀況下,驅動會根據MySQL的時區做爲serverTimezone,而後作轉換,這樣獲得的結果就是咱們想要的。
serverTimezone不指定的查詢狀況
可是這樣作有一個問題,就是在查詢datetime類型的數據時,也會發生轉換,查詢的結果將是30號16點到1號2點的數據。那麼如何才能保證datetime類型、timestamp類型的數據都正確。首先serverTimezone是須要指定Asia/Shanghai的,否則datetime的數據會發生轉換。而因爲serverTimezone和MySQL時區不一致,查詢的timestampe數據存在時區問題,因此最後的辦法就是修改MySQL時區爲東八區。經過保證MySQL時區、serverTimezone和JVM時區三者一致,來保證時間數據讀寫的正確性。
文中的代碼已上傳至Github,感興趣的同窗能夠本身試下:
https://github.com/chaycao/Learn/tree/master/LearnTimeZone