Elasticsearch中的date与时区问题

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所对应的时间戳)。

此时,相同的时间字符串在ESjava程序构造的查询条件中代表的时间戳,其实就已经产生了区别:它们所对应的时间戳已经不同了。

那么就会出现:
我插入ES的时间戳为:2019-09-24 00:00:00,但是我在java程序中再用该时间字符串得到的Date date = format.parse("2019-09-24 00:00:00");对象,date.getTime()得到的时间戳去查询该条数据是查不到的。

解决办法:

  1. 直接使用时间字符串2019-09-24 00:00:00去查询。
  2. 使用SimpleDateFormat对象转换时间字符串时,指定时区。
  3. 插入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"
        }
      }
    ]
  }
}

小结
无时区时间字符串

  1. 时间戳精确到毫秒:由于在java程序中对于的时间字符串“2019-09-24 00:00:00”转换为精确到毫秒的13位时间戳时,存在精度误差,此时直接使用转换后的毫秒时间戳(无论时区是否正确:UTC的1569283200734东8区的1569254400953)去查询插入的时间字符串,都会出现查询不到的情况(正常现象)。
  2. 时间戳精确到秒:此时只有使用正确时区(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”,使用精确到毫秒:1569254400092UTC时间戳进行查询时,查询无法命中。使用精确到秒:1569254400000UTC时间戳,可以查询可以命中。

4. 总结

  1. 内部存储形式:ES内部,时间所有date类型的数据,最终都是转换为UTC时间戳存储的,并且精度为豪秒级

  2. 无时区信息的时间字符串:对于无时区信息的时间字符串(“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”所对应得时间戳,就会出现查询不到结果的问题。

  3. 有时区信息的时间字符串:对于有时区信息的时间字符串(“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时间戳存储。

  • 9
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
如果你在使用Spring Boot集成Elasticsearch,并且需要同步日期类型数据,可以按照以下步骤进行: 1. 确认你的数据模型日期类型使用的是java.util.Date或java.time.LocalDateTime等类型。 2. 在使用Elasticsearch高版本,日期类型默认使用的是date类型,可以在映射文件进行配置。 3. 在Spring Boot,可以使用Spring Data Elasticsearch来操作Elasticsearch。在实体类使用@Field注解来指定映射关系,例如: ``` @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss.SSS") private Date createTime; ``` 在上面的示例,我们指定了createTime字段的类型为日期类型,格式为自定义格式,对应的日期格式为“yyyy-MM-dd HH:mm:ss.SSS”。 4. 在Elasticsearch高版本,日期类型默认使用UTC时区,可以在映射文件进行配置。例如: ``` "date": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss.SSS", "timezone": "+08:00" } ``` 在上面的示例,我们指定了日期类型的格式和时区。 5. 在Spring Boot,可以使用ElasticsearchTemplate或ElasticsearchRestTemplate来进行数据操作,例如: ``` List<Entity> entities = repository.findAll(); elasticsearchTemplate.putMapping(Entity.class); elasticsearchTemplate.save(entities); ``` 在上面的示例,我们使用Repository来查询数据,并使用ElasticsearchTemplate来进行数据操作。 综上所述,如果你需要在Spring Boot集成Elasticsearch同步日期类型数据,可以在实体类使用@Field注解指定映射关系,并在映射文件配置日期类型的格式和时区。同时,可以使用Spring Data ElasticsearchElasticsearchTemplate或ElasticsearchRestTemplate来进行数据操作。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值