flink-2

实时数仓项目

 

第1章 分层设计

在之前介绍实时数仓概念时讨论过,建设实时数仓的目的。主要是增加数据计算的复用性。每次新增加统计需求时,不至于从原始数据进行计算,而是从半成品继续加工而成。

每层的职能

分层

数据描述

生成计算工具

存储媒介

ODS

原始数据,日志和业务数据

日志服务器,maxwell

kafka

DWD

根据数据对象为单位进行分流,比如订单、页面访问等等。

FLINK

kafka

DWM

对于部分数据对象进行进一步加工,比如独立访问、跳出行为。依旧是明细数据。

FLINK

 

DIM

维度数据

FLINK

HBase

DWS

根据某个维度主题将多个事实数据轻度聚合,形成主题宽表。

FLINK

Clickhouse

ADS

把Clickhouse中的数据根据可视化需要进行筛选聚合。

Clickhouse SQL

可视化展示

 

第2章 搭建工程

2.1 在工程中新建模块

 

 

2.3 创建包

目录介绍

目录

 

app

产生各层数据的flink任务

bean

数据对象

common

公共常量

utils

工具类

 

2.4 配置文件

2.4.1 pom.xml


<properties>
    <flink.version>1.11.2</flink.version>
    <scala.version>2.12</scala.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-java</artifactId>
        <version>${flink.version}</version>
    </dependency>

    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java_${scala.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>

    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-kafka_${scala.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>

    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-clients_${scala.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>

    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-cep_${scala.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>

    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-json</artifactId>
        <version>${flink.version}</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.68</version>
    </dependency>

</dependencies>


<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>3.0.0</version>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
            <executions>
                <execution>
                    <id>make-assembly</id>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

2.4.2 logback.xml

Flink程序默认使用logback日志,所以为了控制日志输出,增加logback.xml放在resources目录下即可。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%msg%n</pattern>
        </encoder>
    </appender>

    <root level="error">
        <appender-ref ref="console"/>
    </root>
</configuration>

第3章 计算DWD层(用户行为日志)

3.1 主要任务

1、分流处理,要把数据分成启动主题数据、页面主题数据、曝光主题数据。这三类数据虽然都是用户行为数据,但是有着完全不一样的数据结构,所以要拆分处理。

2、识别新老用户,本身客户端业务有新老用户的标识,但是不够准确,需要用实时计算再次确认。

3、推送下游的kafka

3.2 代码实现

3.2.1 功能一:接收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<String> 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<String> kafkaSource = MyKafkaUtil.getKafkaSource(topic, groupId);
        DataStreamSource<String> baseLogDS = env.addSource(kafkaSource);

        //5.转换为Json对象
        SingleOutputStreamOperator<JSONObject> jsonObjectDS = baseLogDS.process(new ProcessFunction<String, JSONObject>() {
            @Override
            public void processElement(String value, Context context, Collector<JSONObject> 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");
    }
}

3.2.2 功能二:增加识别新老访客

保存每个mid的首次访问日期,每条进入该算子的访问记录,都会把mid对应的首次访问时间读取出来,跟当前日期进行比较,只有首次访问时间不为空,且首次访问时间早于当日的,则认为该访客是老访客,否则是新访客。

      同时如果是新访客且没有访问记录的话,会写入首次访问时间。

主程序

//6.按照Mid分组
KeyedStream<JSONObject, String> midKeyedStream = jsonObjectDS.keyBy(data -> data.getJSONObject("common").getString("mid"));

//7.对数据做处理,校验数据中的Mid是否为今天第一次访问
SingleOutputStreamOperator<JSONObject> midWithNewFlagDStream = midKeyedStream.map(new RichMapFunction<JSONObject, JSONObject>() {

    //声明第一次访问日期的状态
    private ValueState<String> 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;
    }
});

3.2.3 功能三:利用侧输出流实现数据拆分

//8.定义启动和曝光数据的侧输出流标签
final OutputTag<String> startTag = new OutputTag<String>("start") {};
final OutputTag<String> displayTag = new OutputTag<String>("display") {};

//9.根据数据内容,将数据分为3类,页面日志输出到主流,启动日志输出到启动侧输出流,曝光日志输出到曝光日志侧输出流
SingleOutputStreamOperator<String> pageDStream = midWithNewFlagDStream.process(new ProcessFunction<JSONObject, String>() {
    @Override
    public void processElement(JSONObject jsonObject, Context context, Collector<String> 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<String> startDStream = pageDStream.getSideOutput(startTag);
DataStream<String> displayDStream = pageDStream.getSideOutput(displayTag);

3.2.4 功能四:把结果输出到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);
    }

//创建从Kafka读取数据的Source方法
    public static FlinkKafkaConsumer<String> getKafkaSource(String topic, String groupId) {
        properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        return new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), properties);
    }
    
//创建往Kafka写出数据的Sink方法
public static FlinkKafkaProducer<String> getKafkaSink(String topic) {
      return new FlinkKafkaProducer<>(topic, new SimpleStringSchema(), properties);
    }

}

2)主程序

//11.将各个流的数据写入对应的主题
FlinkKafkaProducer<String> startSink = MyKafkaUtil.getKafkaSink(TOPIC_START);
FlinkKafkaProducer<String> pageSink = MyKafkaUtil.getKafkaSink(TOPIC_PAGE);
FlinkKafkaProducer<String> displaySink = MyKafkaUtil.getKafkaSink(TOPIC_DISPLAY);

pageDStream.addSink(pageSink);
startDStream.addSink(startSink);
displayDStream.addSink(displaySink);

第4章 计算DWD层(业务数据)

4.1 主要任务

1、提取数据,把MaxWell抓取数据中有用的部分保留,没用的过滤掉。

2、实现动态分流功能。

3、把分好的流保存到对应表、主题中。

4.2 关于动态分流功能策略

由于MaxWell是把全部数据统一写入一个Topic中, 这样显然不利于日后的数据处理。所以需要把各个表拆开处理。但是由于每个表有不同的特点,有些表是维度表,有些表是事实表,有的表既是事实表在某种情况下也是维度表。

在实时计算中一般把维度数据写入存储容器,一般是方便通过主键查询的数据库比如HBase,Redis,MySQL等。一般把事实数据写入流中,进行进一步处理,最终形成宽表。但是作为Flink实时计算任务,如何得知哪些表是维度表,哪些是事实表呢?而这些表又应该采集哪些字段呢?

这样的配置不适合写在配置文件中,因为这样的话,业务端随着需求变化每增加一张表,就要修改配置重启计算程序。所以这里需要一种动态配置方案,把这种配置长期保存起来,一旦配置有变化,实时计算可以自动感知。

这种可以有两个方案实现,一种是用Zookeeper存储,通过Watch感知数据变化。

另一种是用mysql数据库存储,周期性的同步。

这里选择第二种方案,主要是mysql对于配置数据初始化和维护管理,用sql都比较方便,虽然周期性操作时效性差一点,但是配置变化并不频繁。

所以就有了如下图:

4.3 功能实现

4.3.1 功能一:接收Kafka数据,过滤空值数据

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<String> kafkaSource = MyKafkaUtil.getKafkaSource(topic, groupId);
        DataStreamSource<String> jsonDStream = env.addSource(kafkaSource);
        SingleOutputStreamOperator<JSONObject> jsonObjectDStream = jsonDStream.map(JSON::parseObject);

        //4.过滤掉空值数据
        SingleOutputStreamOperator<JSONObject> filterDS = jsonObjectDStream.filter(new FilterFunction<JSONObject>() {
            @Override
            public boolean filter(JSONObject value) throws Exception {
                return value.getString("table") != null &&
                        value.getString("data") != null &&
                        value.getString("data").length() > 3;
            }
        });
    }
}

4.3.2 功能二:根据MySQL的动态配置表,进行分流。

1)准备工作

导入依赖,创建JavaBean以及工具类

(1)引入pom.xml 依赖

<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>29.0-jre</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>

(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 <T> List<T> queryList(String sql, Class<T> 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<T> 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<JSONObject, JSONObject> {

    //侧输出流标签
    private OutputTag<JSONObject> outputTag;

    public TableProcessFunction(OutputTag<JSONObject> outputTag) {
        this.outputTag = outputTag;
    }

    //用于在内存中存储表配置对象 [表名,表配置信息]
    private Map<String, TableProcess> tableProcessMap = null;

    //表示目前内存中已经存在的HBase表
    private Set<String> 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<String, TableProcess> refreshMeta() {

    System.out.println("更新处理信息");

    //创建HashMap用于存放结果数据
    HashMap<String, TableProcess> processHashMap = new HashMap<>();

    //查询MySQL中的配置表数据
    List<TableProcess> 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<Map.Entry<String, Object>> entries = data.entrySet();
    List<String> 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<JSONObject> 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<JSONObject> hbaseTag = new OutputTag<JSONObject>(TableProcess.SINK_TYPE_HBASE) {};

//6.使用ProcessFunction实现数据分流操作,事实表放入主流,维度表根据维度信息放入侧输出流
SingleOutputStreamOperator<JSONObject> kafkaDStream = filterDS.process(new TableProcessFunction(hbaseTag));

//7.获取侧输出流数据,即将来写往HBase(Phoenix)的数据
DataStream<JSONObject> hbaseDStream = kafkaDStream.getSideOutput(hbaseTag);

4.3.3 功能三:分表Sink之HBase(Phoenix)分表Sink

把分好的流保存到对应表、主题中,这个功能其实是两个问题,一个是Kafka的分表Sink,一个是HBase(Phoenix)的分表Sink。

首先先说HBase(Phoenix)的分表Sink。

流程分析:

DimSink 继承了RickSinkFunction,这个function的分两条时间线。

一条是任务启动时执行open操作(图中紫线),我们可以把连接的初始化工作放在此处一次性执行。

另一条是随着每条数据的到达反复执行invoke()(图中黑线),在这里面我们要实现数据的保存,主要策略就是根据数据组合成sql提交给hbase。

代码实现:

1)引入phoenix依赖


<dependency>
    <groupId>org.apache.phoenix</groupId>
    <artifactId>phoenix-spark</artifactId>
    <version>5.0.0-HBase-2.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.el</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2)因为要用单独的schema,所以在程序中加入hbase-site.xml

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<configuration>
    <property>
        <name>hbase.rootdir</name>
        <value>hdfs://hadoop102:8020/hbase</value>
    </property>

    <property>
        <name>hbase.cluster.distributed</name>
        <value>true</value>
    </property>

    <property>
        <name>hbase.zookeeper.quorum</name>
        <value>hadoop102,hadoop103,hadoop104</value>
    </property>

    <property>
        <name>hbase.unsafe.stream.capability.enforce</name>
        <value>false</value>
    </property>

    <property>
        <name>hbase.wal.provider</name>
        <value>filesystem</value>
    </property>

    <!-- phoenix master 配置参数 -->
    <property>
        <name>hbase.master.loadbalancer.class</name>
        <value>org.apache.phoenix.hbase.index.balancer.IndexLoadBalancer</value>
    </property>

    <property>
        <name>hbase.coprocessor.master.classes</name>
        <value>org.apache.phoenix.hbase.index.master.IndexMasterObserver</value>
    </property>

    <!-- phoenix regionserver 配置参数 -->
    <property>
        <name>hbase.regionserver.wal.codec</name>
        <value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value>
    </property>

    <property>
        <name>hbase.region.server.rpc.scheduler.factory.class</name>
        <value>org.apache.hadoop.hbase.ipc.PhoenixRpcSchedulerFactory</value>
        <description>Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata
            updates
        </description>
    </property>

    <property>
        <name>hbase.rpc.controllerfactory.class</name>
        <value>org.apache.hadoop.hbase.ipc.controller.ServerRpcControllerFactory</value>
        <description>Factory to create the Phoenix RPC Scheduler that uses separate queues for index and metadata
            updates
        </description>
    </property>

    <property>
        <name>phoenix.schema.isNamespaceMappingEnabled</name>
        <value>true</value>
    </property>

    <property>
        <name>phoenix.schema.mapSystemTablesToNamespace</name>
        <value>true</value>
    </property>

</configuration>

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<JSONObject> {

    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<String> 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<String> getKafkaSink(String topic) {
    return new FlinkKafkaProducer<>(topic, new SimpleStringSchema(), properties);
}

所以在KafkaUtil中加入另外一个方式创建FlinkKafkaProducer

public static <T> FlinkKafkaProducer<T> getKafkaSinkBySchema(KafkaSerializationSchema<T> serializationSchema) {
    return new FlinkKafkaProducer<>(DEFAULT_TOPIC, serializationSchema, properties, FlinkKafkaProducer.Semantic.EXACTLY_ONCE);
}

在主程序中加入新KafkaSink这两者创建 FlinkKafkaProducer的区别是前者给定确定的Topic,而后者除了缺省情况下回采用DEFAULT_TOPIC,一般情况下可以根据不同的业务数据在KafkaSerializationSchema中通过方法实现。

//9.将主流数据写入Kafka
FlinkKafkaProducer<JSONObject> kafkaSink = MyKafkaUtil.getKafkaSinkBySchema(new KafkaSerializationSchema<JSONObject>() {
    @Override
    public void open(SerializationSchema.InitializationContext context) throws Exception {
        System.out.println("启动Kafka Sink");
    }
    @Override
    public ProducerRecord<byte[], byte[]> 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功能更强大,但是相对的越全面的算子使用起来也更加繁琐。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值