flink从kafka读取数据写入clickhouse本地表的实现

实现功能

因为直接写clickhouse的分布式表在数据量比较大的时候会有各种问题,所以做了一个flink读取kafka数据然后路由写入到相应的本地表节点,并且关于不同的表的配置信息可以随时更改并设置生效时间。

实现流程

  1. 首先从kafka将数据读取过来
  2. 然后进行相应的处理及逻辑判断写入到对应的clickhouse表格中
  3. 最后根据CDC读取来的配置信息进行相应节点的hash路由,直接写入本地表

读取kafka数据

定义kafka工具类,实现kafka的source获取函数

public static FlinkKafkaConsumer<String> getKafkaSource(String topic, String groupId) {
        Properties prop = new Properties();
        prop.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_SERVER);
        prop.setProperty(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        return new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), prop);
    }

之后在主程序中获取环境,并读取数据

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();


env.addSource(KafkaUtils.getKafkaSource(topic,groupId).setStartFromGroupOffsets()).setParallelism(4)

flinkCDC获取配置信息并进行广播

这里将clickhouse的分布式表的配置信息存储在MySQL中,利用CDC进行一个实时的监控和更改

在配置流没有到达之前,将采用代码中的默认配置进行处理,同时将默认配置的数据存储在Redis中,也可以做到很方便的修改。

//flinkcdc 读取配置
DebeziumSourceFunction<String> configSource = MySQLSource.<String>builder()
       .hostname("******")
       .port(3306)
       .databaseList("test_config")
       .tableList("test_config.ck_config_info")
       .username("root")
       .password("******")
       .startupOptions(StartupOptions.initial())
       .deserializer(new MyCustomSchema())
       .build();

其中的序列化采用自定义方式,会对每一条数据打一个标记,是新增还是更新或者删除,以便于后续配置状态的更新

配置流准备好后开始和主流进行广播连接,之后自定义处理函数继承BroadcastProcessFunction,来进行数据的节点路由

路由逻辑

主要是将配置流数据存进一个状态里,然后主流数据每次都去状态里读取自己需要的配置信息,然后进行想要写入的本地表节点的路由写入

主流数据路由逻辑如下

if(tableConfig != null){
            String ipList = tableConfig.getIpList();
            String[] split = ipList.split(",");

            assert stringRowTuple2.f1.getField(0) != null;

            //hash获取对应节点
            int index = CommonUtils.getHash(stringRowTuple2.f1.getField(0).toString()) % split.length;
            String ip = split[index];
            collector.collect(Tuple2.of(ip,stringRowTuple2.f1));
        }else {
            System.out.println("no this key in TableConfig:" + stringRowTuple2.f0);
            System.out.println("use default......");
            collector.collect(Tuple2.of("default",stringRowTuple2.f1));
        }

对于采用default方案的数据,会在后续处理中采用默认的节点进行相同逻辑的路由,同时,这要求我们在进行row类型数据存放的时候,需要将主键放在第一位,以达到相同主键的数据路由到同一节点,以便于后续根据不同表引擎的特性进行去重或者其他处理。

Sink端自定义函数

最关键的就是sink函数的编写,这里可以参考flink的JdbcSink.sink()的实现进行处理

public class ClickHouseJDBCSinkFunction extends RichSinkFunction<Tuple2<String,Row>> {

    final ClickHouseJDBCOutputFormat clickHouseJDBCOutputFormat;

    public ClickHouseJDBCSinkFunction(ClickHouseJDBCOutputFormat clickHouseJDBCOutputFormat) {
        this.clickHouseJDBCOutputFormat = clickHouseJDBCOutputFormat;
    }



    /**
     * 初始化参数化
     * @param parameters
     * @throws Exception
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        RuntimeContext ctx = getRuntimeContext();
        clickHouseJDBCOutputFormat.setRuntimeContext(ctx);
        clickHouseJDBCOutputFormat.open(ctx.getIndexOfThisSubtask(), ctx.getNumberOfParallelSubtasks());
        System.out.println("init...");
    }

    @Override
    public void invoke(Tuple2<String, Row> value, Context context) throws Exception {
        clickHouseJDBCOutputFormat.writeRecord(value);
    }


    @Override
    public void close() throws Exception {
        clickHouseJDBCOutputFormat.close();
        super.close();
    }
}

写入表的操作在writeRecord方法里实现

进入此方法会根据设定的时间间隔及批次大小决定是将批次数据刷写入表还是继续往批次里面添加数据,当达到刷写时机的时候会调用flush()方法来进行书写,并且针对刷写失败的情况设置了重试次数

private void flush(List<Row> rows, PreparedStatement preparedStatement, Connection connection,int maxRetryTimes,String ip) throws SQLException {
        checkFlushException();

        if(maxRetryTimes>0){

            DateUtils.sleepMinutes(CommonUtils.MAX_RETRY_TIMES-maxRetryTimes);

            try {
                for (int i = 0; i < rows.size(); ++i) {
                    Row row = rows.get(i);
                    for (int j = 0; j < this.tableColumns.length; ++j) {

                        if (null != row.getField(j)) {
                            preparedStatement.setObject(j + 1, row.getField(j));
                        } else {
                            preparedStatement.setObject(j + 1, "null");
                        }
                    }
                    preparedStatement.addBatch();
                }

                preparedStatement.executeBatch();
                connection.commit();
                preparedStatement.clearBatch();

                rows.clear();

             
            } catch (Exception e){
                System.out.println("failed to insert ,will try it "+(CommonUtils.MAX_RETRY_TIMES-maxRetryTimes+1)+" minutes again......");

                //判断连接是否可用,不可用重新获取连接
                if(!connection.isValid(60)){
                    try {
                        reEstablishConnection(ip);

                        preparedStatement = connection.prepareStatement(query);
                        this.preparedStatementMap.put(ip,preparedStatement);


                    } catch (SQLException sqe) {
                        throw new IllegalArgumentException("open() failed.", sqe);
                    } catch (ClassNotFoundException cnfe) {
                        throw new IllegalArgumentException("JDBC driver class not found.", cnfe);
                    }
                }

                flush(rows, preparedStatement, connection, maxRetryTimes-1,ip);
            }

        }else {
            //三次重试失败后换一个节点继续尝试发送
            String otherIp = findOtherIp(ip);
            flush(rows,preparedStatementMap.get(otherIp), connectionMap.get(otherIp),CommonUtils.MAX_RETRY_TIMES,otherIp);
        }

    }

这里我设置的是三次重试失败后会换一个节点继续重试,然后一直循环,除非没有了可用节点,当然也可以不这么做,我们也能够将写入失败的数据写入临时文件或者采用其他方式

关于定时的实现方案

对于当批次数量达不到的情况下,在时间间隔内触发flush的设置,我这边尝试了两种方案,一种是采用Java的scheduledFuture来进行设置,但是这种方案会有个问题,就是在一个时间间隔的末尾阶段正好批次量达到的时候,会在短时间内进行两次刷写操作,第二次写入的数据量可能会很小,这在使用的时候需要注意。另一种方案就是采用系统时间来进行设置,通过不断更新上次刷写的系统时间并与当前系统时间进行做差值,当差值满足条件时进行刷写操作,这可以避免上个方案的问题,但是这要求数据流不能断,因为当数据流没数据后,就无法在对时间进行作差,会导致最后一批数据迟迟不能写入表中。

总结

在数据量不大的情况下写入数据可以直接写入分布式表,这无可厚非,毕竟代码实现起来很方便,这次是对于写本地表的一次尝试练习,经过测试,想要的功能基本实现,以此记录,对于其中的不足之处也望大家多多指正。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
用户画像作为大数据的根基,它抽象出一个用户的信息全貌,为进一步精准、快速地分析用户行为习惯、消费习惯等重要信息,提供了足够的数据基础,奠定了大数据时代的基石。 用户画像,即用户信息标签化,就是企业通过收集与分析消费者社会属性、生活习惯、消费行为等主要信息的数据之后,抽象出一个用户的商业全貌作是企业应用大数据技术的基本方式。用户画像为企业提供了足够的信息基础,能够帮助企业快速找到精准用户群体以及用户需求等更为广泛的反馈信息。 用户画像系统能很好地帮助企业分析用户的行为与消费习惯,可以预测商品的发展的趋势,提高产品质量,同时提高用户满意度。构建一个用户画像,包括数据源端数据收集、数据预处理、行为建模、构建用户画像。有些标签是可以直接获取到的,有些标签需要通过数据挖掘分析到!本套课程会带着你一步一步的实现用户画像案例,掌握了本套课程内容,可以让你感受到Flink+ClickHouse技术架构的强大和大数据应用的广泛性。 在这个数据爆发的时代,像大型电商的数据量达到百亿级别,我们往往无法对海量的明细数据做进一步层次的预聚合,大量的业务数据都是好几亿数据关联,并且我们需要聚合结果能在秒级返回。 包括我们的画像数据,也是有这方便的需求,那怎么才能达到秒级返回呢?ClickHouse正好满足我们的需求,它是非常的强大的。 本课程采用Flink+ClickHouse技术架构实现我们的画像系统,通过学习完本课程可以节省你摸索的时间,节省企业成本,提高企业开发效率。希望本课程对一些企业开发人员和对新技术栈有兴趣的伙伴有所帮助,如对我录制的教程内容有建议请及时交流。项目中采用到的算法包含Logistic Regression、Kmeans、TF-IDF等,Flink暂时支持的算法比较少,对于以上算法,本课程将带大家用Flink实现,并且结合真实场景,学完即用。系统包含所有终端的数据(移动端、PC端、小程序端),支持亿级数据量的分析和查询,并且是实时和近实时的对用户进行画像计算。本课程包含的画像指标包含:概况趋势,基础属性,行为特征,兴趣爱好,风险特征,消费特征,营销敏感度,用户标签信息,用户群里,商品关键字等几大指标模块,每个指标都会带大家实现。课程所涵盖的知识点包括:开发工具为:IDEA FlinkClickhouseHadoopHbaseKafkaCanalbinlogSpringBootSpringCloudHDFSVue.jsNode.jsElemntUIEcharts等等 课程亮点: 1.企业级实战、真实工业界产品 2.ClickHouse高性能列式存储数据库 3.提供原始日志数据进行效果检测 4.Flink join企业级实战演练 5.第四代计算引擎Flink+ClickHouse技术架构6.微服务架构技术SpringBoot+SpringCloud技术架构7.算法处理包含Logistic Regression、Kmeans、TF-IDF等8.数据库实时同步落地方案实操9.统计终端的数据(移动端、PC端、小程序端) 10.支撑亿级海量数据的用户画像平台11.实时和近实时的对用户进行画像计算12.后端+大数据技术栈+前端可视化13.提供技术落地指导支持 14.课程凝聚讲师多年实战经验,经验直接复制15.掌握全部内容能独立进行大数据用户平台的设计和实操企业一线架构师讲授,代码在老师的指导下企业可以复用,提供企业解决方案。  版权归作者所有,盗版将进行法律维权。 

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值