最近在使用 FlinkCDC 做 MySQL 到 ElasticSearch 的数据同步,在数据同步的过程中遇到了一些关于日期类型的问题,在这里整理总结一下。
整个项目的数据架构如下:
以 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.000000
到9999-12-31 23:59:59.999999
timestamp[(fsp)]
1970-01-01 00:00:00 UTC
)开始的经过的秒数。1970-01-01 00:00:01.000000 UTC
到2038-01-19 03:14:07.499999 UTC
time[(fsp)]
hh:mm:ss[.fraction]
-838:59:59.000000
到838:59:59.000000
year[(4)]
YYYY
0000
,或者为1901
到2155
Flink CDC
的底层是 debezium
,debezium
将自己伪装成 mysql
的 slave
,通过向 mysql master
发送 dump
协议来建立主从关系,然后 mysql master
就会将 binlog
发送给 mysql slave
即 debezium
,debezium
通过解析(反序列化) binlog
为特定的格式并写入 kafka topic
。
那么对于上述 5 种 mysql
时间类型,debezium
是如何解析的呢?
debezium
使用 int32
来存储 MySQL
的 YEAR
类型,存储的值为年份,即 debezium
反序列化后的值与 MySQL
中保存的值是一致的,写入到 kafka topic
中的值为实际值。
比如,mysql
中一个 YEAR
类型的字段值为’2023’,则在 kafka topic
中的值也为‘2023’。
MySQL 使用 DATE 类型来表示年月日,存储格式为YYYY-MM-DD
,取值范围为1000-01-01
到 9999-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。
MySQL
使用 TIME
类型来表示时分秒,一般用来表示一天内的时间,当然也可以用来表示超过一天的时间,或者两个时间点之间的时间间隔。
除了能够表示时分秒,还可以通过在定义的时候显式指定精确位数来表示毫秒和微秒,比如 TIME(3)
就可以表示毫秒,对应的时数据格式为HH:MM:ss.SSS
,TIME(6)
用来表示微秒,对应的时间格式为HH:MM:ss.SSSSSS
。
对于MySQL
的 TIME
类型字段来说,debezium
使用 int64
来存储,将其转化为 io.debezium.time.MicroTime
对象,存储的数值为从 0 时 0 分 0 秒经过的微秒数。
所以,与 DATE
类型类似,由于 debezium
默认对 TIME
字段保存的语义做了转换,所以我们需要做相反的转换。要么在自定义的序列化器中处理,要么在消费 kafka topic
的下游处理。
对于 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_time
,update_time
,sys_time
,假设有一条记录在 MySQL 中的取值分别为:1970-01-01 01:00:00
、1970-01-01 01:00:00.999
、1970-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 类型来说,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 时区可以忽略。
纪元(Epoch)是指具有历史意义的某一刻,其实就是一个参考点,常用的纪元为 1970-01-01T00:00:00.000000Z
。
ES 中只有一种日期类型date,可以是下面三种格式之一:
2015-01-01
或 2015/01/01 12:10:30
在 ES 的内部,date类型被转换为UTC时区(如果指定了时区),并存储为表示从epoch到现在毫秒数的long 类型。也就是说,不论写入的是2023-01-01
这种只有日期没有时间的值,还是2023-01-01 12:00:00
这种同时具有日期和时间的值,或者是3600000
这种表示从 epoch 经过毫秒数的值,在 ES 的内部都被保存为从 epoch 经过的毫秒数的 long 类型。
这种存储方式的好处显而易见:对于 date 类型可以十分方便的进行函数计算。对date 类型的查询在ES内部被转换为对 long 类型的范围查询,查询结果再转换为字符串类型。
我们可以通过如下命令在一个 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"
}
}
}
}