時間相關的字段是ElasticsSearch(如下簡稱ES)最經常使用的字段了,幾乎全部的索引應用場景都會有時間字段,通常用於基於時間範圍的搜索,聚合等場景。可是因爲時區的問題,相信不少小夥伴都踩到過期間字段的坑,筆者本身就踩過。mysql
本文但願給你提供一個避坑指南。sql
由於本文不是專門講時區的,你只須要了解一些基本的概念就能夠了。json
咱們知道全球分爲24個時區,包含23個整時區及180°經線左右兩側的2個半時區。東經的時間比西經要早,也就是若是格林威治時間是中午12時,則中央經線15°E的時區爲下午1時。好比北京位於東8區,因此北京時間應該是晚上8點。api
GMT和UTC能夠認爲是一個東西,只是精度的差別。他們表明的是全球的一個時間參考點,全球都以格林威治的時間做爲標準來設定時間。app
在程序中咱們常常能見到這樣的字符串:this
Thu Oct 16 07:13:48 GMT 2019
這說明這個時間是GMT時間。spa
China Standard Time,是中國的標準時間。CST = GMT(UTC) + 8。好比code
Thu Aug 25 17:15:49 CST 2019
表示的就是CST時間。有時候咱們也能見到相似下面這樣的表示:orm
2020-03-15T11:45:43Z
其中Z表示的就是UTC時間。server
咱們知道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聚合查詢日期的時候,也須要注意時區問題,最好是指定時區進行搜索或者聚合。