1 前言
本文主要讲解Elasticsearch中date
类型数据的底层存储原理,以及对带时区日期字符串
和不带时区的日期字符
串如何在ES底层存储进行验证。对于直接存储Long
类型时间戳,不作过多描述。
1.1 Date
类型数据的存储
UTC(Universal Time Coordinated)
叫做世界统一时间,中国大陆所用的时间是东8区
时间,比UTC时间超前8小时。即与 UTC
的时差是 +8
,也就是 UTC+8
。
在Elasticsearch内部,不论 date
是什么展示格式,所有date类型数据(时间字符串 or 时间戳等)在 Elasticsearch 内部存储时全部都会转换成 UTC时间戳(并且把时区也会计算进去
),最后以milliseconds-since-the-epoch
作为存储的格式。
1.2 Date
类型数据的查询
在Elasticsearch内部,date
被转为UTC
,并被存储为一个长整型数字,代表从1970年1月1号0点到现在的毫秒数。
date
类型字段上的查询会在内部被转为对long
型值的范围查询,查询的结果类型是字符串。
-
假如插入的时候,值是
"2018-01-01"
,则返回"2018-01-01"
-
假如插入的时候,值是
"2018-01-01 12:00:00"
,则返回"2018-01-01 12:00:00"
-
假如插入的时候,值是
1514736000000
,则返回"1514736000000"
。(进去是long
型,出来是String
型)
在查询日期时,会执行下面的过程:
- 转换成
long
整形格式的范围(range
) 查询 - 得到聚合的结果
- 将结果中的
date
类型(long
整型数据)根据date format
字段转换回对应的展示格式
1.3 Date
类型数据的展示
Elasticsearch 数据是以 json
格式存储的,而 json
中是并没有 date
数据类型,因此 Elasticsearch 中虽然有 date
类型,但在展示时却要转化成另外的格式。
date
类型在 Elasticsearch 展示的格式有下面几种:
- 将日期时间格式化后的字符串,如
"2015-01-01"
或者"2015/01/01 12:10:30"
long
型的整数,意义是milliseconds-since-the-epoch
,翻译一下就是自1970-01-01 00:00:00 UTC
以来经过的毫秒数。int
型的整数,意义是seconds-since-the-epoch,
是指自1970-01-01 00:00:00 UTC
以来经过的秒数。
3. 易错知识点
以时间字符串1970-01-01 00:00:00
为例(程序运行时区为东8区
):
在Java
程序中,将时间字符串1970-01-01 00:00:00
使用SimpleDateFormat
转换为Date
类型时,如果没有指定时区,那么该字符串会被认为是东八区的时间1970-01-01 00:00:00
,对应的时间戳为-28800000
。
UTC时间1970-01-01 00:00:00
对应的时间戳为0
,时间戳-28800000
对应于UTC时间1969-12-31 16:00:00
,其值也等于北京时间1970-01-01 00:00:00
所对应的时间戳(北京时间东八区,超前于UTC时间8小时)。
public static void main(String[] args) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
Date date = format.parse("1970-01-01 00:00:00");
System.out.println(date);
System.out.println(date.getTime());
} catch (ParseException e) {
e.printStackTrace();
}
}
# 输出
Thu Jan 01 00:00:00 CST 1970
-28800000
问题:假如我们将时间字符串通过ES提供的接口,将其插入的某个ES索引(例如:ddate)的date类型字段中,然后再在java程序中,使用SimpleDateFormat转换后的Date对应的时间戳,去调用ES的查询API去查询该数据时,会出现什么情况呢?
ES
中对时间字符串的处理:
当时间字符串中没有时区信息时,此时,在ES内部会将其(“2019-09-24 00:00:00“)
当成是0 时区
的“2019-09-24 00:00:00”
。
java
程序中对时间字符串的处理(假设java程序运行在东8区
的服务器上):
如果使用SimpleDateFormat
将字符转为Date
类型时未指定时区,那么拿到Date
对象对应的就是东8区 2019-09-24 00:00:00
对应的时间,此时再使用Date.getTime()
所获取的时间戳为东8区 2019-09-24 00:00:00
所对应得时间戳(即返回的是UTC时间2019-09-23 16:00:00
所对应的时间戳)。
此时,相同的时间字符串在ES
和java
程序构造的查询条件中代表的时间戳,其实就已经产生了区别:它们所对应的时间戳已经不同了。
那么就会出现:
我插入ES的时间戳为:2019-09-24 00:00:00
,但是我在java程序中再用该时间字符串得到的Date date = format.parse("2019-09-24 00:00:00");
对象,date.getTime()
得到的时间戳去查询该条数据是查不到的。
解决办法:
- 直接使用时间字符串
2019-09-24 00:00:00
去查询。 - 使用SimpleDateFormat对象转换时间字符串时,指定时区。
- 插入ES时使用带时区信息的时间字符串。
3. 验证
3.1 准备工作
创建索引,设置date
类型字段format
:
PUT /ddate
PUT /ddate/default/_mapping
{
"properties": {
"name":{
"type": "keyword"
},
"ddate":{
"type": "date",
"format": "strict_date_optional_time||yyyy-MM-dd HH:mm:ss||epoch_millis"
}
}
}
3.2 无时区信息验证
数据情况:
date
类型数据信息如下,通过使用Java
程序,分别获取该时间字符串“2019-09-24 00:00:00”
的东8区
时间戳、GMT
时间戳、以及UTC
时间戳。如下:
获取指定时区时间戳信息,详情可见:Java获取指定时区的时间戳
# 2019-09-24 00:00:00
#
# local: 1569254400953(表示东8区的时间:2019-09-24 00:00:00<(即返回的是`北京时间1970年01月1日0点0分0秒`以来的`毫秒数`,对应`UTC`时间`1970年01月1日8点0分0秒`以来的`毫秒数`,其数值大小等于`0时区`的`“2019-09-23 16:00:00”`所对应的时间戳)>,对应的UTC时间戳)
# GMT: 1569283200985(表示东0时区的时间:2019-09-24 00:00:00,对应的GMT时间戳)
# UTC: 1569283200734(表示东0时区的时间:2019-09-24 00:00:00,对应的UTC时间戳)
# ES底层实际存储的
# es utc: 1569283200000
插入数据,其中date类型插入无时区信息的时间字符串,如下:
POST ddate/default
{
"name":"lzz",
"ddate": "2019-09-24 00:00:00"
}
查询验证:
使用“2019-09-24 00:00:00”
对应的东8区
时间戳(精确到毫秒:1569254400953
)查询,查询结果如下:
GET /ddate/default/_search
{
"query": {
"term": {
"ddate": 1569254400953
}
}
}
查询结果:
{
"took": 0,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 0,
"max_score": null,
"hits": []
}
}
使用“2019-09-24 00:00:00”
对应的UTC
时间戳(精确到毫秒:1569283200734
)查询,查询结果如下:
GET /ddate/default/_search
{
"query": {
"term": {
"ddate": 1569283200734
}
}
}
查询结果:
{
"took": 0,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 0,
"max_score": null,
"hits": []
}
}
使用“2019-09-24 00:00:00”
对应的东8区
时间戳(精确到秒:1569254400000
)查询,查询结果如下:
GET /ddate/default/_search
{
"query": {
"term": {
"ddate": 1569254400000
}
}
}
查询结果:
{
"took": 0,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 0,
"max_score": null,
"hits": []
}
}
使用“2019-09-24 00:00:00”
对应的UTC
时间戳(精确到秒:1569283200000
)查询,查询结果如下:
GET /ddate/default/_search
{
"query": {
"term": {
"ddate": 1569283200000
}
}
}
查询结果:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": "ddate",
"_type": "default",
"_id": "AXLgmQOJp7tSUSJKLQSm",
"_score": 1,
"_source": {
"name": "lzz",
"ddate": "2019-09-24 00:00:00"
}
}
]
}
}
小结
无时区时间字符串
- 时间戳精确到毫秒:由于在
java
程序中对于的时间字符串“2019-09-24 00:00:00”
转换为精确到毫秒的13位
时间戳时,存在精度误差,此时直接使用转换后的毫秒时间戳(无论时区是否正确:UTC的1569283200734
、东8区的1569254400953
)去查询插入的时间字符串,都会出现查询不到的情况(正常现象)。 - 时间戳精确到秒:此时只有使用正确时区(
UTC
)对应的秒级时间戳(1569283200
),才能查询命中插入的数据。使用东8区
对应的秒级时间戳(1569254400000
)无法命中。
3.3 有时区信息验证
数据情况:
date
类型数据信息如下,通过使用Java
程序,分别获取该时间字符串“ 2019-09-24T00:00:00+0800”
的东8区
时间戳、GMT
时间戳、以及UTC
时间戳。如下:
注意:
当字符串中有时区信息时,此时,在ES内部“2019-09-24T00:00:00+0800”
所对应的东8区
时间戳、GMT
时间戳、UTC
时间戳为一致的。都是将其当做东8区的“2019-09-24 00:00:00”
(即返回的是北京时间1970年01月1日0点0分0秒
以来的毫秒数
,对应UTC
时间1970年01月1日8点0分0秒
以来的毫秒数
,其数值大小等于0时区
的“2019-09-23 16:00:00”
所对应的时间戳)
# 2019-09-24T00:00:00+0800 (表示东8区的时间:2019-09-24 00:00:00)
#
# local: 1569254400402(表示东8区的时间:2019-09-24 00:00:00<大小等同0时区的 2019-09-23 16:00:00>,对应的东8区时间戳)
# GMT: 1569254400461(表示东8区的时间:2019-09-24 00:00:00<大小等同0时区的 2019-09-23 16:00:00>,对应的GMT时间戳)
# UTC: 1569254400092(表示东8区的时间:2019-09-24 00:00:00<大小等同0时区的 2019-09-23 16:00:00>,对应的UTC时间戳)
# ES底层实际存储的
# es utc: 1569254400000
插入数据,其中date类型插入无时区信息的时间字符串,如下:
POST ddate/default
{
"name":"lcc",
"ddate": "2019-09-24T00:00:00+0800"
}
查询验证:
使用"2019-09-24T00:00:00+0800"
对应的UTC
时间戳(精确到秒:1569254400000
)查询,查询结果如下:
GET /ddate/default/_search
{
"query": {
"term": {
"ddate": 1569254400000
}
}
}
查询结果:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": "ddate",
"_type": "default",
"_id": "AXLg21Sjp7tSUSJKLQ-r",
"_score": 1,
"_source": {
"name": "lcc",
"ddate": "2019-09-24T00:00:00+0800"
}
}
]
}
}
小结
对于有时区的时间字符串“2019-09-24T00:00:00+0800”
,使用精确到毫秒:1569254400092
的UTC
时间戳进行查询时,查询无法命中。使用精确到秒:1569254400000
的UTC
时间戳,可以查询可以命中。
4. 总结
-
内部存储形式:ES内部,时间所有
date
类型的数据,最终都是转换为UTC时间戳
存储的,并且精度为豪秒级
。 -
无时区信息的时间字符串:对于无时区信息的时间字符串(
“2019-09-24 00:00:00”
),在ES内部会将其(“2019-09-24 00:00:00”
)当成是0时区
的“2019-09-24 00:00:00”
来对待,转换为0时区
的“2019-09-24 00:00:00”
对应的UTC
时间戳存储。需要注意的是:
当存入无时区信息的时间字符串时,使用ES的Java api
(或其他api)进行查询时,要特别注意时区的问题。例如,在Java中将“2019-09-24 00:00:00”
转换为Date
类型,然后调用Date.getTime()
,获取的其实是东8区的“2019-09-24 00:00:00”
(即返回的是北京时间1970年01月1日0点0分0秒
以来的毫秒数
,对应UTC
时间1970年01月1日8点0分0秒
以来的毫秒数
,其数值大小等于0时区
的“2019-09-23 16:00:00”
所对应的时间戳)对应的时间戳,而ES中存储的是0时区
的时间“2019-09-24 00:00:00”
所对应得时间戳,就会出现查询不到结果的问题。 -
有时区信息的时间字符串:对于有时区信息的时间字符串(
“2019-09-24T00:00:00+0800”
),在ES内部会将其(“2019-09-24T00:00:00+0800”
)当成是0时区
的“2019-09-23 16:00:00”
来对待,转换为0时区
的2019-09-23 16:00:00”
对应的UTC
时间戳存储。