实时数仓项目用到了Flink-CDC,这里记录一下学习的过程。
CDC是Change Data Capture(变更数据获取)的简称。核心思想是监测并捕获数据库的变动(包括数据或者数据表的插入、更新以及删除等),将这些变更按发生的完成顺序完整记录下来,写入到消息中间件以供其他服务进行订阅及消费。
CDC主要分为基于查询和基于Binlog两种方式,如下表:
基于查询的CDC | 基于Binlog的CDC | |
---|---|---|
开源产品 | Sqoop、Kafka JDBC Source | Canal、Maxwell、Debezium |
执行模式 | Batch | Streaming |
是否可以捕获所有数据变化 | 否(只能查询出最终结果) | 是(一条数据多次修改的中间过程也可以捕获) |
延迟性 | 高延迟(攒成一批的,延迟高) | 低延迟(类似于流式的,一条一条,延迟低) |
是否增加数据库压力 | 是(因为要select表) | 否 (是基于文件的) |
开源地址:https://github.com/ververica/flink-cdc-connectors
使用Flink-CDC需要先导入以下依赖:
<dependencies>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-javaartifactId>
<version>1.12.0version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-streaming-java_2.12artifactId>
<version>1.12.0version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-clients_2.12artifactId>
<version>1.12.0version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-clientartifactId>
<version>3.1.3version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
dependency>
<dependency>
<groupId>org.apache.flinkgroupId>
<artifactId>flink-table-planner-blink_2.12artifactId>
<version>1.12.0version>
dependency>
<dependency>
<groupId>com.ververicagroupId>
<artifactId>flink-connector-mysql-cdcartifactId>
<version>2.0.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.75version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-assembly-pluginartifactId>
<version>3.0.0version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependenciesdescriptorRef>
descriptorRefs>
configuration>
<executions>
<execution>
<id>make-assemblyid>
<phase>packagephase>
<goals>
<goal>singlegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
public class FlinkCDC {
public static void main(String[] args) throws Exception {
//1、创建流式执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//2、Flink-CDC将读取binlog的位置信息以状态的方式保存在CheckPoint
//2.1 所以需要开启检查点,这里开启checkpoint,每隔5s做一次
env.enableCheckpointing(5000L);
//2.2 指定checkpoint的语义
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
//2.3 设置任务关闭的时候保留最后一次checkpoint的数据
env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
//2.4 指定从Checkpoint自动重启策略
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3,2000L));
//2.5 设置状态后端
env.setStateBackend(new FsStateBackend("hdfs://hadoop102:8020/flinkCDC"));
//2.6 设置访问HDFS的用户名
System.setProperty("HADOOP_USER_NAME","atguigu");
//3、创建Flink-MySQL-CDC的Source
DebeziumSourceFunction<String> mysqlSource = MySqlSource.<String>builder()
.hostname("hadoop102")
.port(3306)
.username("root")
.password("000000")
.databaseList("gmall-flink")
.tableList("gmall-flink.z_user_info") //可选配置,默认读取数据库中所有的表
.startupOptions(StartupOptions.initial())
.deserializer(new StringDebeziumDeserializationSchema())
.build();
DataStreamSource<String> mysqlDataStream = env.addSource(mysqlSource);
mysqlDataStream.print();
env.execute();
}
}
会先基于查询读取mysql表中现有的数据,然后会切换到binlog模式到最新的位置开始读取新增及变化的数据。
英文翻译:
在第一次启动时对被监视的数据库表执行初始快照,并继续读取最新的binlog。
什么是第一次?
从保存点或检查点恢复叫做不是第一次(不需要先把表中现有的数据读取出来),其余情况都叫做第一次。第一次都需要把表中现有的数据读取出来。
会从binlog开始的位置,从表开始创建读取表中的数据。
如果要使用这个,我们必须先开启某个库某个表的binlog,然后再去建表,也就是说binlog要包含从建表开始的所有信息。
英文翻译:
永远不要在第一次启动时对监视的数据库表执行快照,只从binlog的开始读取。应该小心使用,因为只有当binlog保证包含数据库的整个历史记录时,它才有效。
直接从binlog最新的位置开始读取数据,也就是启动连接器连接到了以后。
英文翻译:
永远不要在第一次启动时对被监视的数据库表执行快照,只从binlog的末尾读取,这意味着只有自连接器启动以来的更改。
public class FlinkSQLCDC {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
//使用Flink SQL方式构建CDC表
tableEnv.executeSql("CREATE TABLE user_info ( " +
"id STRING primary key, " +
"name STRING, " +
"sex STRING " +
") WITH ( " +
"'connector'='mysql-cdc', " +
"'scan.startup.mode'='latest-offset', " +
"'hostname'='hadoop102', " +
"'port'='3306', " +
"'username'='root', " +
"'password'='000000', " +
"'database-name'='dc_test', " +
"'table-name'='user_info' " +
")");
Table table = tableEnv.sqlQuery("select * from user_info");
DataStream<Tuple2<Boolean, Row>> retractStream = tableEnv.toRetractStream(table, Row.class);
retractStream.print();
env.execute();
}
}
注意:
1)这里使用的是Flink-CDC2.0版本,所以需要使用1.13版本的Flink,否则会报错
2)Flink SQL方式创建表时,只支持监控单个表
3)Flink SQL中不需要指定序列化器
4)Flink SQL中可以用如下参数指定对应的执行模式(默认为initial,还可以指定为latest-offset):
public class Flink_CDCWithCustomerSchema {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
DebeziumSourceFunction<String> sourceFunction = MySqlSource.<String>builder()
.hostname("hadoop102")
.port(3306)
.username("root")
.databaseList("gmall_flink")
.tableList("gmall_flink.z_user_info")
.startupOptions(StartupOptions.initial())
.deserializer(new MyDeserializer())
.build();
env.execute();
}
public static class MyDeserializer implements DebeziumDeserializationSchema<String>{
/*自定义的返回类型的格式
*{
* "db":"",
* "tableName":"",
* "before":{"id":"1001","name":""...},
* "after":{"id":"1001","name":""...},
* "op":""
* }
*/
@Override
public void deserialize(SourceRecord sourceRecord, Collector<String> collector) throws Exception {
//创建JSON对象用于封装结果数据
JSONObject result = new JSONObject();
//获取库名&表名
String topic = sourceRecord.topic();
String[] fields = topic.split("\\.");
result.put("db",fields[1]);
result.put("tableName",fields[2]);
//获取before数据
Struct value = (Struct) sourceRecord.value();
Struct before = value.getStruct("before");
JSONObject beforeJson = new JSONObject();
if(before!=null){
//如果before里面有数据,就获取对应的数据
//先获取列名
Schema schema = before.schema();
List<Field> fieldList = schema.fields();
for (Field field : fieldList) {
beforeJson.put(field.name(),before.get(field));
}
}
//如果before里面为空,就直接返回一个空的jsonObject
result.put("before",beforeJson);
//获取after数据,同理
Struct after = value.getStruct("after");
JSONObject afterJson = new JSONObject();
if(after!=null){
//如果after里面有数据,就获取对应的数据
//先获取列名
Schema schema = after.schema();
List<Field> fieldList = schema.fields();
for (Field field : fieldList) {
afterJson.put(field.name(),after.get(field));
}
}
//如果before里面为空,就直接返回一个空的jsonObject
result.put("after",afterJson);
//获取op,READ DELETE UPDATE CREATE
Envelope.Operation op = Envelope.operationFor(sourceRecord);
result.put("op",op);
//输出数据
collector.collect(result.toJSONString());
}
@Override
public TypeInformation<String> getProducedType() {
return BasicTypeInfo.STRING_TYPE_INFO;
}
}
}
运行结果:
注意:
1)在获取before和after时,要先判断是否为空,然后再获取列名,最后才能获取对应列的数据。
2)获取op的方法:Envelope.operationFor(sourceRecord);