ES系列之一文帶你避開日期類型存在的坑

概述

時間相關的字段是ElasticsSearch(以下簡稱ES)最常用的字段了,幾乎所有的索引應用場景都會有時間字段,一般用於基於時間範圍的搜索,聚合等場景。但是由於時區的問題,相信很多小夥伴都踩到過時間字段的坑,筆者自己就踩過。

本文希望給你提供一個避坑指南。

瞭解時區的基本概念

因爲本文不是專門講時區的,你只需要瞭解一些基本的概念就可以了。

在這裏插入圖片描述

我們知道全球分爲24個時區,包含23個整時區及180°經線左右兩側的2個半時區。東經的時間比西經要早,也就是如果格林威治時間是中午12時,則中央經線15°E的時區爲下午1時。比如北京位於東8區,所以北京時間應該是晚上8點。

  • 格林威治標準時間GMT或者UTC

GMT和UTC可以認爲是一個東西,只是精度的差異。他們代表的是全球的一個時間參考點,全球都以格林威治的時間作爲標準來設定時間。

在程序中我們經常能見到這樣的字符串:

Thu Oct 16 07:13:48 GMT 2019

這說明這個時間是GMT時間。

  • CST中國標準時間

China Standard Time,是中國的標準時間。CST = GMT(UTC) + 8。比如

Thu Aug 25 17:15:49 CST 2019

表示的就是CST時間。有時候我們也能見到類似下面這樣的表示:

2020-03-15T11:45:43Z

其中Z表示的就是UTC時間。

坑一,日期字段映射問題

我們知道ES有個Dynamic Mapping的機制,當索引不存在或者索引中的某些字段沒有設置mapping屬性,index的時候ES會自動創建索引並且根據傳入的字段內容自動推斷字段的格式。比如,整型的數字會變成Long,“yyyy-dd-mm”等格式的字段會轉成date ),不過有時候這個推斷並不是我們想要的。

舉個我自己在項目中遇到的例子。當時有個實體對象要寫入ES中,我用了fastjson轉換成json的字符串然後寫入ES。在ES查看的時候發現寫入的字段變成了Long型失去了日期的屬性,導致不能根據此字段進行日期相關的條件搜索。下面模擬下整個過程。

首先定義一個實體對象,

@Data
@ToString
public class TestEntity {
    private String stringData;
    private Byte byteData;
    private Date timeData;
}

然後寫入整個對象,

TestEntity entity = new TestEntity();
entity.setByteData((byte)2);
entity.setStringData("test");
entity.setTimeData(new Date());
IndexRequest request = new IndexRequest("test_index");
request.id(id);
request.source(JSON.toJSONString(), XContentType.JSON);
client.index(request, RequestOptions.DEFAULT);

寫入成功後發現無法根據整個時間字段進行排序和篩選,在ES裏查看索引的mapping發現,timeData字段居然被識別成了Long型。

在這裏插入圖片描述
原因是fastjson默認把Date類型轉換成long型的時間戳了。到ES這邊以爲是一個普通的整型。

這個問題的解決方案有兩種。

第一種是在fastjson序列化的時候不要使用默認行爲,而是指定日期類型的格式,

@Data
@ToString
public class TestEntity {
    private String stringData;
    private Byte byteData;

    @JSONField(format="yyyy-MM-dd HH:mm:ss")
    private Date timeData;
}

這樣寫進ES就會被自動識別成日期類型。

另一種解決方案是,在ES的maping裏明確的指定字段的屬性。

PUT test_index
{
  "mappings": {
    "properties": {
      "TimeData": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
      }
    }
  }
}

這裏我們給TimeData設置了日期類型,並且可以識別三種不同的日期格式。其中最後一個epoch_millis就是毫秒單位的時間戳。

坑二,時區問題

這個坑最常見。比如很多時候我們是直接把mysql的數據讀出然後寫入到ES。mysql裏的日期寫入到ES後發現時間ES查詢的時間跟實際看到的時間差了8個小時,究竟是怎麼回事呢?

先來看看官方文檔怎麼說,

Internally, dates are converted to UTC (if the time-zone is specified) and stored as a long number representing milliseconds-since-the-epoch.

Queries on dates are internally converted to range queries on this long representation, and the result of aggregations and stored fields is converted back to a string depending on the date format that is associated with the field.

這兩段的意思是說,在ES內部默認使用UTC時間並且是以毫秒時間戳的long型存儲的。針對日期字段的查詢其實對long型時間戳的範圍查詢。

我們舉一個例子,很多時候我們會把mysql的數據同步的ES,方法很多,我這裏以用logstash遷移數據舉例。(關於logstash具體的配置方法不是本文的重點我就不表了)mysql的數據是這樣的:

在這裏插入圖片描述

logstash的配置如下:(只給出部分配置)

input {
  jdbc {
    jdbc_driver_class => "com.mysql.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://localhost:3306/test"
    jdbc_user => "root"
    jdbc_password => "11111111"
    use_column_value => false
    #記錄最後一次運行的結果
    record_last_run => true
    #上面運行結果的保存位置
    last_run_metadata_path => "jdbc-position.txt"
    statement => "SELECT * FROM kafkalogin"

執行logstash進行遷移,然後我們在kibana裏發現數據是這樣的:

在這裏插入圖片描述
很奇怪,似乎相差的時間也不是8個小時,而是5個小時或者6個小時。

這種問題我們的解決方案也很簡單。我們已經知道輸出端(ES)的默認時區是UTC,只需要再在輸入端(mysql)也明確時區即可。改下logstash的配置如下:

input {
  jdbc {
    jdbc_driver_class => "com.mysql.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"

然後你就會發現兩邊的時間是一樣的。

如果你的mysql裏的時間不是UTC而是東八區的時間,可以用如下的配置:

input {
  jdbc {
    jdbc_driver_class => "com.mysql.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai"

這樣遷移的數據在ES裏查看是相差8個小時的。

還有一種解決方案是你存儲的時間字符串本身就帶有時區信息,比如 “2016-07-15T12:58:17.136+0800”。

我們在ES進行查詢或者聚合的時候,建議指定時區避免產生意想不到的結果。比如:

GET _search
{
    "query": {
        "range" : {
            "timestamp" : {
                "time_zone": "+01:00", 
                "gte": "2015-01-01 00:00:00", 
                "lte": "now" 
            }
        }
    }
}

加上這個時區信息,ES在搜索的時候時間起始就是2014-12-31T23:00:00 UTC

此外在使用Java Client聚合查詢日期的時候,也需要注意時區問題,最好是指定時區進行搜索或者聚合。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章