一文搞懂 MySQL、debezium 和 ElasticSearch 的时间格式

前言

最近在使用 FlinkCDC 做 MySQL 到 ElasticSearch 的数据同步,在数据同步的过程中遇到了一些关于日期类型的问题,在这里整理总结一下。

整个项目的数据架构如下:

在这里插入图片描述

MySQL的数据类型

以 MySQL 5.7 版本为例,MySQL 的时间类数据类型包括:DATE, YEAR, TIME, DATETIME, TIMESTAMP 5种。

  • date

    • 定义:只表示日期
    • 存储格式:YYYY-MM-DD
    • 取值范围:1000-01-01 to 9999-12-31
  • datetime[(fsp)]

    • 定义:表示日期和时间
    • 存储格式:YYYY-MM-DD hh:mm:ss[.fraction]
    • 存储范围:1000-01-01 00:00:00.0000009999-12-31 23:59:59.999999
    • 使用可选的fsp来表示秒的精度,fsp取值为0-6,默认为 0.
    • 存储空间:使用 8 字节来存储,
    • 时区:不支持时区
  • timestamp[(fsp)]

    • 定义:表示日期和时间,存储从 epoch (1970-01-01 00:00:00 UTC)开始的经过的秒数。
    • 存储范围:1970-01-01 00:00:01.000000 UTC2038-01-19 03:14:07.499999 UTC
    • 使用可选的fsp来表示秒的精度,fsp取值为0-6,默认为 0.
    • 时区:支持时区,保存的时候会将值从当前时区转化为UTC 时区保存,查询的结果会从 UTC 时区转化为当前时区。
    • 性能:底层使用 4 字节的 int 来存储秒数,如果存在毫秒则额外使用 1-4 个字节来存储毫秒数,
  • time[(fsp)]

    • 定义:只表示时间
    • 格式:hh:mm:ss[.fraction]
    • 范围:-838:59:59.000000838:59:59.000000
    • 使用可选的fsp来表示秒的精度,fsp取值为0-6,默认为 0.
  • year[(4)]

    • 定义:4位数字格式表示的年份
    • 格式:YYYY
    • 范围:默认为0000,或者为19012155

Debezium 对日期类型的处理

Flink CDC 的底层是 debeziumdebezium 将自己伪装成 mysqlslave,通过向 mysql master 发送 dump 协议来建立主从关系,然后 mysql master 就会将 binlog 发送给 mysql slavedebeziumdebezium 通过解析(反序列化) binlog 为特定的格式并写入 kafka topic

那么对于上述 5 种 mysql 时间类型,debezium 是如何解析的呢?

YEAR

debezium 使用 int32 来存储 MySQLYEAR 类型,存储的值为年份,即 debezium 反序列化后的值与 MySQL 中保存的值是一致的,写入到 kafka topic 中的值为实际值。

比如,mysql 中一个 YEAR 类型的字段值为’2023’,则在 kafka topic 中的值也为‘2023’。

DATE

MySQL 使用 DATE 类型来表示年月日,存储格式为YYYY-MM-DD,取值范围为1000-01-019999-12-31

debezium 使用INT32来存储MySQL 的 DATE 类型,存储的值为:距离 1970 年 1 月 1 日经过的天数。

比如,MySQL 中的 DATE 字段取值为1970-01-30,则 debezium 默认写入到 kafka topic 中的值为 29,即实际值与 1970 年 1 月 1 日相隔的天数。

由于 debezium 的这种处理方式,我们需要对 DATE 类型进行特殊处理。一种处理方式是自定义一个反序列化器,在其中对 DATE 类型做相应的处理,即将 29 转化为 1970-01-30,这样写入 kafka topic 中的就是能够正确读取的’1970-01-30’了。另一种处理方式是不在 debezium 反序列化的时候做处理,而是在下游消费kafka topic 的时候做处理,将29解析为 1970-01-30

TIME

MySQL 使用 TIME 类型来表示时分秒,一般用来表示一天内的时间,当然也可以用来表示超过一天的时间,或者两个时间点之间的时间间隔。

除了能够表示时分秒,还可以通过在定义的时候显式指定精确位数来表示毫秒微秒,比如 TIME(3) 就可以表示毫秒,对应的时数据格式为HH:MM:ss.SSSTIME(6)用来表示微秒,对应的时间格式为HH:MM:ss.SSSSSS

对于MySQLTIME 类型字段来说,debezium 使用 int64 来存储,将其转化为 io.debezium.time.MicroTime对象,存储的数值为从 0 时 0 分 0 秒经过的微秒数

所以,与 DATE 类型类似,由于 debezium 默认对 TIME 字段保存的语义做了转换,所以我们需要做相反的转换。要么在自定义的序列化器中处理,要么在消费 kafka topic 的下游处理。

DATETIME

对于 DATETIME 类型来说,MySQL 在定义的时候可以指定精度,所有可选的精度有:DATETIME、DATETIME(1)、DATETIME(2)、DATETIME(3)、DATETIME(4)、DATETIME(5)、DATETIME(6)。

对于前四种来说,debezium 使用 int64 来存储,对应的 schema 名称为io.debezium.time.Timestamp对象,存储的数值的含义是从 1970 年 1 月 1 日 0 时 0 分 0 秒经过的毫秒数

对于后三种来说,debezium 依旧使用 int64 来存储,只是对应的 schema 名称变为了io.debezium.time.MicroTimestamp对象,存储的数值的含义也发生了变化,从 1970 年 1 月 1 日 0 时 0 分 0 秒经过的微秒数

假如 MySQL 数据表存在三个类型分别为datetime、datetime(3)、datetime(6)的字段,字段名称分别为create_timeupdate_timesys_time,假设有一条记录在 MySQL 中的取值分别为:1970-01-01 01:00:001970-01-01 01:00:00.9991970-01-01 01:00:00.999999,则他们经过 debezium 的反序列化后写入到kafka topic中的值分别为:3600000、3600999、3600999999。

create_time字段的取值1970-01-01 01:00:00相比1970-01-01 00:00:00经过了 1 小时,换算成毫秒为3600000;update_time字段的取值1970-01-01 01:00:00.999相比1970-01-01 00:00:00经过了 1 小时加999 毫秒,换算成毫秒为3600999;sys_time字段的取值1970-01-01 01:00:00.999999相比1970-01-01 00:00:00经过了 1 小时加 999999 微秒,换算成微秒为3600999999。

TIMESTAMP

对于 TIMESTAMP 类型来说,MySQL 在定义的时候可以指定精度,所有可选的精度有:TIMESTAMP、TIMESTAMP(1)、TIMESTAMP(2)、TIMESTAMP(3)、TIMESTAMP(4)、TIMESTAMP(5)、TIMESTAMP(6)。

对于 MySQL 所有精度的 TIMESTAMP 字段类型来说,debezium 使用字符串来保存,对应的 schema 类为io.debezium.time.ZonedTimestamp,即带时区的时间戳。

对于不带精度的 TIMESTAMP 字段,debezium 使用yyyy-mm-dd'T'hh:MM:ssZ格式的字符传来表示,其中末尾的Z表示时区,如果是 0 时区可以忽略。

ElasticSearch 中的日期类型

纪元(Epoch)是指具有历史意义的某一刻,其实就是一个参考点,常用的纪元为 1970-01-01T00:00:00.000000Z

ES 中只有一种日期类型date,可以是下面三种格式之一:

  • 表示格式化日期的字符串,例如:2015-01-012015/01/01 12:10:30
  • 一个表示从 1970 年 1 月 1 日 0 时 0 分 0 秒到现在的秒数的 long 类型
  • 一个表示从 1970 年 1 月 1 日 0 时 0 分 0 秒到现在的毫秒数的 long 类型

ES 如何存储 date 类型

在 ES 的内部,date类型被转换为UTC时区(如果指定了时区),并存储为表示从epoch到现在毫秒数的long 类型。也就是说,不论写入的是2023-01-01这种只有日期没有时间的值,还是2023-01-01 12:00:00这种同时具有日期和时间的值,或者是3600000这种表示从 epoch 经过毫秒数的值,在 ES 的内部都被保存为从 epoch 经过的毫秒数的 long 类型。

这种存储方式的好处显而易见:对于 date 类型可以十分方便的进行函数计算。对date 类型的查询在ES内部被转换为对 long 类型的范围查询,查询结果再转换为字符串类型。

ES 支持哪些类型的日期

我们可以通过如下命令在一个 index 中定义一个 date类型的字段:

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "date": {
        "type": "date" 
      }
    }
  }
}

这种情况下,我们没有指定date类型可以接受的时间格式,默认为strict_date_optional_time||epoch_millis。表示可以将下面这几种格式的值写入该字段:

PUT my-index-000001/_doc/1
{ "date": "2023-01-01" } 

PUT my-index-000001/_doc/2
{ "date": "2023-01-01T12:10:30Z" } 

PUT my-index-000001/_doc/3
{ "date": 1420070400001 } 

即默认情况下,支持上面这几种格式。注意,默认情况下不支持类似 2023-01-01 12:30:00 的非ISO标准的时间格式。如果我们想要自定义 date 类型字段需要支持的格式,可以在创建 date 类型的时候通过指定 format 来实现:

PUT my-index-000001
{
  "mappings": {
    "properties": {
      "date": {
        "type":   "date",
        "format": "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss"
      }
    }
  }
}

参考

  • mysql 5.7关于时间类型的说明:https://dev.mysql.com/doc/refman/5.7/en/date-and-time-types.html
  • debezium 关于时间类型处理的说明:https://debezium.io/documentation/reference/1.9/connectors/mysql.html#mysql-temporal-types
  • ElasticSearch date 字段类型的说明:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/date.html
  • Elasticsearch date 字段所有支持的 format:https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html

你可能感兴趣的:(flink,mysql,elasticsearch,数据库,大数据,flink,debezium,CDC)