实时数仓项目
在之前介绍实时数仓概念时讨论过,建设实时数仓的目的。主要是增加数据计算的复用性。每次新增加统计需求时,不至于从原始数据进行计算,而是从半成品继续加工而成。
每层的职能
分层 |
数据描述 |
生成计算工具 |
存储媒介 |
ODS |
原始数据,日志和业务数据 |
日志服务器,maxwell |
kafka |
DWD |
根据数据对象为单位进行分流,比如订单、页面访问等等。 |
FLINK |
kafka |
DWM |
对于部分数据对象进行进一步加工,比如独立访问、跳出行为。依旧是明细数据。 |
FLINK |
|
DIM |
维度数据 |
FLINK |
HBase |
DWS |
根据某个维度主题将多个事实数据轻度聚合,形成主题宽表。 |
FLINK |
Clickhouse |
ADS |
把Clickhouse中的数据根据可视化需要进行筛选聚合。 |
Clickhouse SQL |
可视化展示 |
目录介绍
目录 |
|
app |
产生各层数据的flink任务 |
bean |
数据对象 |
common |
公共常量 |
utils |
工具类 |
1.11.2
2.12
org.apache.flink
flink-java
${flink.version}
org.apache.flink
flink-streaming-java_${scala.version}
${flink.version}
org.apache.flink
flink-connector-kafka_${scala.version}
${flink.version}
org.apache.flink
flink-clients_${scala.version}
${flink.version}
org.apache.flink
flink-cep_${scala.version}
${flink.version}
org.apache.flink
flink-json
${flink.version}
com.alibaba
fastjson
1.2.68
org.apache.maven.plugins
maven-assembly-plugin
3.0.0
jar-with-dependencies
make-assembly
package
single
2.4.2 logback.xml
Flink程序默认使用logback日志,所以为了控制日志输出,增加logback.xml放在resources目录下即可。
%msg%n
第3章 计算DWD层(用户行为日志)
1、分流处理,要把数据分成启动主题数据、页面主题数据、曝光主题数据。这三类数据虽然都是用户行为数据,但是有着完全不一样的数据结构,所以要拆分处理。
2、识别新老用户,本身客户端业务有新老用户的标识,但是不够准确,需要用实时计算再次确认。
3、推送下游的kafka
1)工具类
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
import org.apache.flink.streaming.connectors.kafka.KafkaSerializationSchema;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import java.util.Properties;
public class MyKafkaUtil {
private static Properties properties = new Properties();
private static final String DEFAULT_TOPIC = "DEFAULT_DATA";
static {
String kafkaServer = "hadoop102:9092,hadoop103:9092,hadoop104:9092";
properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaServer);
}
public static FlinkKafkaConsumer getKafkaSource(String topic, String groupId) {
properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, groupId);
return new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), properties);
}
}
2)主程序
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.logcat.utils.MyKafkaUtil;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.runtime.state.filesystem.FsStateBackend;
import org.apache.flink.streaming.api.CheckpointingMode;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Dwd_Base_Log {
//定义用户行为主题信息
private static final String TOPIC_START = "DWD_START_LOG";
private static final String TOPIC_PAGE = "DWD_PAGE_LOG";
private static final String TOPIC_DISPLAY = "DWD_DISPLAY_LOG";
public static void main(String[] args) throws Exception {
//1.获取执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);
//2.设置CK相关参数
env.enableCheckpointing(5000, CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setCheckpointTimeout(60000);
env.setStateBackend(new FsStateBackend("hdfs://hadoop102:8020/gmall/flink/checkpoint"));
//3.指定消费者配置信息
String groupId = "ods_dwd_base_log_app";
String topic = "ODS_BASE_LOG";
//4.从指定的Kafka主题中读取数据
FlinkKafkaConsumer kafkaSource = MyKafkaUtil.getKafkaSource(topic, groupId);
DataStreamSource baseLogDS = env.addSource(kafkaSource);
//5.转换为Json对象
SingleOutputStreamOperator jsonObjectDS = baseLogDS.process(new ProcessFunction() {
@Override
public void processElement(String value, Context context, Collector collector) throws Exception {
try {
JSONObject jsonObject = JSON.parseObject(value);
collector.collect(jsonObject);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
});
//打印测试
jsonObjectDS.print();
env.execute("Dwd_Base_Log Job");
}
}
保存每个mid的首次访问日期,每条进入该算子的访问记录,都会把mid对应的首次访问时间读取出来,跟当前日期进行比较,只有首次访问时间不为空,且首次访问时间早于当日的,则认为该访客是老访客,否则是新访客。
同时如果是新访客且没有访问记录的话,会写入首次访问时间。
主程序
//6.按照Mid分组
KeyedStream midKeyedStream = jsonObjectDS.keyBy(data -> data.getJSONObject("common").getString("mid"));
//7.对数据做处理,校验数据中的Mid是否为今天第一次访问
SingleOutputStreamOperator midWithNewFlagDStream = midKeyedStream.map(new RichMapFunction() {
//声明第一次访问日期的状态
private ValueState firstVisitDataState;
//声明日期数据格式化对象
private SimpleDateFormat simpleDateFormat;
@Override
public void open(Configuration parameters) throws Exception {
//初始化数据
firstVisitDataState = getRuntimeContext().getState(new ValueStateDescriptor<>("newMidDateState", String.class));
simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
}
@Override
public JSONObject map(JSONObject jsonObject) throws Exception {
//打印数据
System.out.println(jsonObject);
//获取数据中所携带的是否为第一次访问标记("0"表示不是第一次,"1"表示为第一次)
String isNew = jsonObject.getJSONObject("common").getString("is_new");
//获取数据中的时间戳
Long ts = jsonObject.getLong("ts");
//判断标记如果为"1",则继续校验数据
if ("1".equals(isNew)) {
String newMidDate = firstVisitDataState.value();
String tsDate = simpleDateFormat.format(new Date(ts));
//如果新用户状态不为空,则将标记置为"0"
if (newMidDate != null && newMidDate.length() == 0 && !newMidDate.equals(tsDate)) {
isNew = "0";
jsonObject.getJSONObject("common").put("is_new", isNew);
}
//更新状态
firstVisitDataState.update(tsDate);
}
//返回添加了新的标记之后的数据
return jsonObject;
}
});
//8.定义启动和曝光数据的侧输出流标签
final OutputTag startTag = new OutputTag("start") {};
final OutputTag displayTag = new OutputTag("display") {};
//9.根据数据内容,将数据分为3类,页面日志输出到主流,启动日志输出到启动侧输出流,曝光日志输出到曝光日志侧输出流
SingleOutputStreamOperator pageDStream = midWithNewFlagDStream.process(new ProcessFunction() {
@Override
public void processElement(JSONObject jsonObject, Context context, Collector collector) throws Exception {
//获取数据中的启动相关字段
JSONObject startJsonObj = jsonObject.getJSONObject("start");
//将数据转换为字符串,准备最后的输出
String dataString = jsonObject.toString();
//启动数据不为空,输出到侧输出流
if (startJsonObj != null && startJsonObj.size() > 0) {
context.output(startTag, dataString);
} else {
//非启动日志,则为页面日志或者曝光日志(携带页面信息)
System.out.println("PageString:" + dataString);
//将数据输出到主流,即是页面日志流
collector.collect(dataString);
//获取数据中的曝光数据,如果不为空,则将每条曝光数据取出输出到曝光日志侧输出流
JSONArray display = jsonObject.getJSONArray("display");
if (display != null && display.size() > 0) {
for (int i = 0; i < display.size(); i++) {
JSONObject displayJsonObj = display.getJSONObject(i);
String pageId = jsonObject.getJSONObject("page").getString("page_id");
displayJsonObj.put("page_id", pageId);
context.output(displayTag, displayJsonObj.toString());
}
}
}
}
});
//10.获取侧输出流数据
DataStream startDStream = pageDStream.getSideOutput(startTag);
DataStream displayDStream = pageDStream.getSideOutput(displayTag);
1)工具类
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
import org.apache.flink.streaming.connectors.kafka.KafkaSerializationSchema;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import java.util.Properties;
public class MyKafkaUtil {
private static Properties properties = new Properties();
private static final String DEFAULT_TOPIC = "DEFAULT_DATA";
static {
String kafkaServer = "hadoop102:9092,hadoop103:9092,hadoop104:9092";
properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaServer);
}
//创建从Kafka读取数据的Source方法
public static FlinkKafkaConsumer getKafkaSource(String topic, String groupId) {
properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, groupId);
return new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), properties);
}
//创建往Kafka写出数据的Sink方法
public static FlinkKafkaProducer getKafkaSink(String topic) {
return new FlinkKafkaProducer<>(topic, new SimpleStringSchema(), properties);
}
}
2)主程序
//11.将各个流的数据写入对应的主题
FlinkKafkaProducer startSink = MyKafkaUtil.getKafkaSink(TOPIC_START);
FlinkKafkaProducer pageSink = MyKafkaUtil.getKafkaSink(TOPIC_PAGE);
FlinkKafkaProducer displaySink = MyKafkaUtil.getKafkaSink(TOPIC_DISPLAY);
pageDStream.addSink(pageSink);
startDStream.addSink(startSink);
displayDStream.addSink(displaySink);
1、提取数据,把MaxWell抓取数据中有用的部分保留,没用的过滤掉。
2、实现动态分流功能。
3、把分好的流保存到对应表、主题中。
由于MaxWell是把全部数据统一写入一个Topic中, 这样显然不利于日后的数据处理。所以需要把各个表拆开处理。但是由于每个表有不同的特点,有些表是维度表,有些表是事实表,有的表既是事实表在某种情况下也是维度表。
在实时计算中一般把维度数据写入存储容器,一般是方便通过主键查询的数据库比如HBase,Redis,MySQL等。一般把事实数据写入流中,进行进一步处理,最终形成宽表。但是作为Flink实时计算任务,如何得知哪些表是维度表,哪些是事实表呢?而这些表又应该采集哪些字段呢?
这样的配置不适合写在配置文件中,因为这样的话,业务端随着需求变化每增加一张表,就要修改配置重启计算程序。所以这里需要一种动态配置方案,把这种配置长期保存起来,一旦配置有变化,实时计算可以自动感知。
这种可以有两个方案实现,一种是用Zookeeper存储,通过Watch感知数据变化。
另一种是用mysql数据库存储,周期性的同步。
这里选择第二种方案,主要是mysql对于配置数据初始化和维护管理,用sql都比较方便,虽然周期性操作时效性差一点,但是配置变化并不频繁。
所以就有了如下图:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.logcat.app.dwd.func.DimSink;
import com.logcat.app.dwd.func.TableProcessFunction;
import com.logcat.bean.TableProcess;
import com.logcat.utils.MyKafkaUtil;
import org.apache.flink.api.common.functions.FilterFunction;
import org.apache.flink.api.common.serialization.SerializationSchema;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
import org.apache.flink.streaming.connectors.kafka.KafkaSerializationSchema;
import org.apache.flink.util.OutputTag;
import org.apache.kafka.clients.producer.ProducerRecord;
import javax.annotation.Nullable;
public class Dwd_Base_DB {
public static void main(String[] args) {
//1.创建执行环境并设置并行度
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(2);
//2.定义消费者的
String groupId = "ods_order_detail_group";
String topic = "ODS_BASE_DB_M";
//3.读取Kafka业务主题数据并转换为JSONObject
FlinkKafkaConsumer kafkaSource = MyKafkaUtil.getKafkaSource(topic, groupId);
DataStreamSource jsonDStream = env.addSource(kafkaSource);
SingleOutputStreamOperator jsonObjectDStream = jsonDStream.map(JSON::parseObject);
//4.过滤掉空值数据
SingleOutputStreamOperator filterDS = jsonObjectDStream.filter(new FilterFunction() {
@Override
public boolean filter(JSONObject value) throws Exception {
return value.getString("table") != null &&
value.getString("data") != null &&
value.getString("data").length() > 3;
}
});
}
}
1)准备工作
导入依赖,创建JavaBean以及工具类
(1)引入pom.xml 依赖
commons-beanutils
commons-beanutils
1.9.4
com.google.guava
guava
29.0-jre
mysql
mysql-connector-java
5.1.47
(2)mysql 建库建表
create database gmall_reatime;
(3)建表
CREATE TABLE `table_process` (
`source_table` varchar(200) NOT NULL COMMENT '来源表',
`operate_type` varchar(200) NOT NULL COMMENT '操作类型 insert,update,delete',
`sink_type` varchar(200) DEFAULT NULL COMMENT '输出类型 hbase kafka',
`sink_table` varchar(200) DEFAULT NULL COMMENT '输出表(主题)',
`sink_columns` varchar(2000) DEFAULT NULL COMMENT '输出字段',
`sink_pk` varchar(200) DEFAULT NULL COMMENT '主键字段',
`sink_extend` varchar(200) DEFAULT NULL COMMENT '建表扩展',
PRIMARY KEY (`source_table`,`operate_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
(4)创建配置表实体类
import lombok.Data;
@Data
public class TableProcess {
public static final String SINK_TYPE_HBASE = "HBASE";
public static final String SINK_TYPE_KAFKA = "KAFKA";
public static final String SINK_TYPE_CK = "CLICKHOUSE";
String sourceTable;
String operateType;
String sinkType;
String sinkTable;
String sinkColumns;
String sinkPk;
String sinkExtend;
}
(5)MySQL工具类
import com.google.common.base.CaseFormat;
import org.apache.commons.beanutils.BeanUtils;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class MySQLUtil {
public static List queryList(String sql, Class clazz, Boolean underScoreToCamel) {
//获取MySQL连接
try {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://hadoop102:3306/gmall_test?characterEncoding=utf-8&useSSL=false", "root", "000000");
//创建SQL执行语句
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
//获取查询结果中的元数据信息
ResultSetMetaData metaData = resultSet.getMetaData();
//创建集合用于存放结果数据
ArrayList resultList = new ArrayList<>();
//遍历数据,将每行数据封装成对象存入集合
while (resultSet.next()) {
T obj = clazz.newInstance();
for (int i = 0; i < metaData.getColumnCount(); i++) {
String columnName = metaData.getColumnName(i);
if (underScoreToCamel) {
CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, columnName);
}
BeanUtils.setProperty(obj, columnName, resultSet.getObject(i));
}
resultList.add(obj);
}
//释放资源
statement.close();
connection.close();
//返回结果
return resultList;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("查询数据失败!!!");
}
}
}
程序流程分析
关于TableProcessFunction 这是一个自定义算子,这个算子包括了三条时间线的任务。
图中紫线,这个时间线与数据流入无关,只要任务启动就会执行。主要的任务方法是open()这个方法在任务启动时就会执行。他的主要工作就是初始化一些连接,开启周期调度。
图中绿线,这个时间线也与数据流入无关,只要周期调度启动,会自动周期性执行。主要的任务是同步配置表(tableProcessMap)。通过在open()方法中加入timer实现。同时还有个附带任务就是如果发现不存在数据表,要根据配置自动创建数据库表。
图中黑线,这个时间线就是随着数据的流入持续发生,这部分的任务就是根据同步到内存的tableProcessMap,来为流入的数据进行标识,同时清理掉没用的字段。
代码实现:
2)TableProcessFunction
自定义算子,实现将数据分流的业务
import com.alibaba.fastjson.JSONObject;
import com.logcat.bean.TableProcess;
import com.logcat.common.GmallConfig;
import com.logcat.utils.MySQLUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.ProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
public class TableProcessFunction extends ProcessFunction {
//侧输出流标签
private OutputTag outputTag;
public TableProcessFunction(OutputTag outputTag) {
this.outputTag = outputTag;
}
//用于在内存中存储表配置对象 [表名,表配置信息]
private Map tableProcessMap = null;
//表示目前内存中已经存在的HBase表
private Set existsTables = new HashSet<>();
//声明Phoenix连接
private Connection connection = null;
}
校验表是否之前已经导入过,如果没有则在Pheonix中创建新增的表3)checkTable()
//用于检查字段,配置表中添加了新数据,在Phoenix中创建新的表
private void checkTable(String tableName, String fields, String pk, String ext) {
//主键不存在,则给定默认值
if (pk == null) {
pk = "id";
}
//扩展字段不存在,则给定默认值
if (ext == null) {
ext = "";
}
//创建字符串拼接对象,用于拼接建表语句SQL
StringBuilder createSql = new StringBuilder("create table if not exists " + GmallConfig.HBASE_SCHEMA + "." + tableName + "(");
//将列做切分,并拼接至建表语句SQL中
String[] fieldsArr = fields.split(",");
for (int i = 0; i < fieldsArr.length; i++) {
String field = fieldsArr[i];
if (pk.equals(field)) {
createSql.append(field).append(" varchar");
createSql.append(" primary key ");
} else {
createSql.append("info.").append(field).append(" varchar");
}
if (i < fieldsArr.length - 1) {
createSql.append(",");
}
}
createSql.append(")");
createSql.append(ext);
try {
//执行建表语句在Phoenix中创建表
Statement statement = connection.createStatement();
System.out.println(createSql);
statement.execute(createSql.toString());
statement.close();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("建表失败!!!");
}
}
4)refreshMeta()
读取MySQL中关于需要写入的表的信息,更新到内存Map中
//用于读取配置表信息存入内存的方法
private Map refreshMeta() {
System.out.println("更新处理信息");
//创建HashMap用于存放结果数据
HashMap processHashMap = new HashMap<>();
//查询MySQL中的配置表数据
List tableProcessList = MySQLUtil.queryList("select * from table_process", TableProcess.class, true);
//遍历查询结果,将数据存入结果集合
for (TableProcess tableProcess : tableProcessList) {
//获取源表表名
String sourceTable = tableProcess.getSourceTable();
//获取数据操作类型
String operateType = tableProcess.getOperateType();
//获取结果表表名
String sinkTable = tableProcess.getSinkTable();
//拼接字段创建主键
String key = sourceTable + ":" + operateType;
//将数据存入结果结合
tableProcessMap.put(key, tableProcess);
if ("insert".equals(operateType) && "hbase".equals(sinkTable)) {
boolean noExist = existsTables.add(sourceTable);
//如果表信息数据不存在内存,则在Phoenix中创建新的表
if (noExist) {
checkTable(sinkTable, tableProcess.getSinkColumns(), tableProcess.getSinkPk(), tableProcess.getSinkExtend());
}
}
}
if (tableProcessMap.size() == 0) {
throw new RuntimeException("缺少处理信息");
}
//返回最新的配置表数据信息
return processHashMap;
}
5)open()
声明周期方法,初始化连接,开启定时任务
//生命周期方法,初始化连接,初始化配置表信息并开启定时任务,用于不断读取配置表信息
@Override
public void open(Configuration parameters) throws Exception {
//初始化连接
Class.forName("org.apache.phoenix.jdbc.PhoenixDriver");
connection = DriverManager.getConnection("jdbc:phoenix:hadoop102,hadoop103,hadoop104:2181");
//初始化配置表信息
tableProcessMap = refreshMeta();
//开启定时任务,用于不断读取配置表信息
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
tableProcessMap = refreshMeta();
}
}, 5000, 5000);
}
6)filterColumn()
校验字段,过滤掉多余的字段
private void filterColumn(JSONObject data, String sinkColumns) {
String[] columns = StringUtils.split(sinkColumns, ",");
Set> entries = data.entrySet();
List columnList = Arrays.asList(columns);
entries.removeIf(next -> !columnList.contains(next.getKey()));
}
7)processElement()
核心处理方法,根据MySQL中的数据,将流区分开
//根据配置表的信息为每条数据打标签,走kafka还是hbase
@Override
public void processElement(JSONObject value, Context ctx, Collector out) throws Exception {
String table = value.getString("table");
String type = value.getString("type");
JSONObject data = value.getJSONObject("data");
if (data == null || data.size() <= 3) {
return;
}
if (type.equals("bootstrap-insert")) {
type = "insert";
value.put("type", type);
}
if (tableProcessMap != null && tableProcessMap.size() > 0) {
String key = table + ":" + type;
TableProcess tableProcess = tableProcessMap.get(key);
if (tableProcess != null) {
value.put("sink_table", tableProcess.getSinkTable());
if (tableProcess.getSinkColumns() != null && tableProcess.getSinkColumns().length() > 0) {
filterColumn(value.getJSONObject("data"), tableProcess.getSinkColumns());
}
} else {
System.out.println("No This Key:" + key);
}
if (tableProcess != null && TableProcess.SINK_TYPE_HBASE.equalsIgnoreCase(tableProcess.getSinkType())) {
ctx.output(outputTag, value);
} else if (tableProcess != null && TableProcess.SINK_TYPE_KAFKA.equalsIgnoreCase(tableProcess.getSinkType())) {
out.collect(value);
}
}
}
8)主程序中调用TableProcessFunction
//5.定义输出到HBase的侧输出流标签
OutputTag hbaseTag = new OutputTag(TableProcess.SINK_TYPE_HBASE) {};
//6.使用ProcessFunction实现数据分流操作,事实表放入主流,维度表根据维度信息放入侧输出流
SingleOutputStreamOperator kafkaDStream = filterDS.process(new TableProcessFunction(hbaseTag));
//7.获取侧输出流数据,即将来写往HBase(Phoenix)的数据
DataStream hbaseDStream = kafkaDStream.getSideOutput(hbaseTag);
把分好的流保存到对应表、主题中,这个功能其实是两个问题,一个是Kafka的分表Sink,一个是HBase(Phoenix)的分表Sink。
首先先说HBase(Phoenix)的分表Sink。
流程分析:
DimSink 继承了RickSinkFunction,这个function的分两条时间线。
一条是任务启动时执行open操作(图中紫线),我们可以把连接的初始化工作放在此处一次性执行。
另一条是随着每条数据的到达反复执行invoke()(图中黑线),在这里面我们要实现数据的保存,主要策略就是根据数据组合成sql提交给hbase。
代码实现:
1)引入phoenix依赖
org.apache.phoenix
phoenix-spark
5.0.0-HBase-2.0
org.glassfish
javax.el
2)因为要用单独的schema,所以在程序中加入hbase-site.xml
hbase.rootdir
hdfs://hadoop102:8020/hbase
hbase.cluster.distributed
true
hbase.zookeeper.quorum
hadoop102,hadoop103,hadoop104
hbase.unsafe.stream.capability.enforce
false
hbase.wal.provider
filesystem
hbase.master.loadbalancer.class
org.apache.phoenix.hbase.index.balancer.IndexLoadBalancer
hbase.coprocessor.master.classes
org.apache.phoenix.hbase.index.master.IndexMasterObserver
hbase.regionserver.wal.codec
org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec
hbase.region.server.rpc.scheduler.factory.class
org.apache.hadoop.hbase.ipc.PhoenixRpcSchedulerFactory
Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata
updates
hbase.rpc.controllerfactory.class
org.apache.hadoop.hbase.ipc.controller.ServerRpcControllerFactory
Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata
updates
phoenix.schema.isNamespaceMappingEnabled
true
phoenix.schema.mapSystemTablesToNamespace
true
3)在phoenix中执行
create schema GMALL_REALTIME;
4)创建公共配置类
public class GmallConfig {
public static final String HBASE_SCHEMA = "GMALL_REALTIME";
public static final String PHOENIX_SERVER="jdbc:phoenix:hadoop102,hadoop103,hadoop104:2181";
}
5)DimSink
import java.util.Set;
import java.sql.Statement;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.DriverManager;
import com.logcat.common.GmallConfig;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
public class DimSink extends RichSinkFunction {
private Connection connection = null;
@Override
public void open(Configuration parameters) throws Exception {
Class.forName("org.apache.phoenix.jdbc.PhoenixDriver");
connection = DriverManager.getConnection(GmallConfig.PHOENIX_SERVER);
}
@Override
public void invoke(JSONObject value, Context context) {
String tableName = value.getString("sink_table");
JSONObject data = value.getJSONObject("data");
if (data != null && data.size() > 0) {
try {
String upsertSql = genUpsertSql(tableName.toUpperCase(), data);
System.out.println(upsertSql);
Statement statement = connection.createStatement();
statement.executeUpdate(upsertSql);
connection.commit();
statement.close();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException("执行SQL失败!");
}
}
}
private String genUpsertSql(String tableName, JSONObject data) {
Set fields = data.keySet();
String upsertSql = "upsert into " + GmallConfig.HBASE_SCHEMA + "." + tableName + "(" + StringUtils.join(fields, ",") + ")";
String valuesSql = " values('" + StringUtils.join(data.values(), "','") + "')";
return upsertSql + valuesSql;
}
}
6)主程序中调用DimSink
//8.将侧输出流数据写入HBase(Phoenix)
hbaseDStream.addSink(new DimSink());
4.3.4 功能四:分表Sink之Kafka分表Sink
getKafkaSinkBySchema
在之前的KafkaUtil中我们实现了如下方法, 但是这种方式创建的FlinkKafkaProducer只能针对一个主题进行写入。而该功能要求针对不同的数据写入到不同的主题中。
public static FlinkKafkaProducer getKafkaSink(String topic) {
return new FlinkKafkaProducer<>(topic, new SimpleStringSchema(), properties);
}
所以在KafkaUtil中加入另外一个方式创建FlinkKafkaProducer
public static FlinkKafkaProducer getKafkaSinkBySchema(KafkaSerializationSchema serializationSchema) {
return new FlinkKafkaProducer<>(DEFAULT_TOPIC, serializationSchema, properties, FlinkKafkaProducer.Semantic.EXACTLY_ONCE);
}
在主程序中加入新KafkaSink这两者创建 FlinkKafkaProducer的区别是前者给定确定的Topic,而后者除了缺省情况下回采用DEFAULT_TOPIC,一般情况下可以根据不同的业务数据在KafkaSerializationSchema中通过方法实现。
//9.将主流数据写入Kafka
FlinkKafkaProducer kafkaSink = MyKafkaUtil.getKafkaSinkBySchema(new KafkaSerializationSchema() {
@Override
public void open(SerializationSchema.InitializationContext context) throws Exception {
System.out.println("启动Kafka Sink");
}
@Override
public ProducerRecord serialize(JSONObject jsonObject, @Nullable Long aLong) {
return new ProducerRecord<>(topic, jsonObject.toJSONString().getBytes());
}
});
kafkaDStream.addSink(kafkaSink);
第5章 总结
DWD的实时计算核心就是数据分流,其次是状态识别。
在开发过程中我们实践了几个灵活度较强算子,比如RichMapFunction, ProcessFunction, RichSinkFunction。 那这几个我们什么时候会用到呢?如何选择?
Function |
可转换结构 |
可过滤数据 |
侧输出 |
open方法 |
可以使用状态 |
输出至 |
MapFunction |
Yes |
No |
No |
No |
No |
下游算子 |
FilterFunction |
No |
Yes |
No |
No |
No |
下游算子 |
RichMapFunction |
Yes |
No |
No |
Yes |
Yes |
下游算子 |
RichFilterFunction |
No |
Yes |
No |
Yes |
Yes |
下游算子 |
ProcessFunction |
Yes |
Yes |
Yes |
Yes |
Yes |
下游算子 |
SinkFunction |
Yes |
Yes |
No |
No |
No |
外部 |
RichSinkFunction |
Yes |
Yes |
No |
Yes |
Yes |
外部 |
从对比表中能明显看出,Rich系列能功能强大,ProcessFunction功能更强大,但是相对的越全面的算子使用起来也更加繁琐。