flink读clickhouse,自定义sink和source

目录

1.maven依赖

2.main

3.自定义source

4.自定义sink

5.参考文章


需求:flink分批读clickhouse数据,处理后再存clickhouse。当然也可以按需读存其他地方。

流式数据批量插入。

1.maven依赖

<dependency>
    <groupId>com.clickhouse</groupId>
    <artifactId>clickhouse-jdbc</artifactId>
    <version>0.3.2-patch5</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-java</artifactId>
    <version>1.14.6</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-streaming-java_2.11</artifactId>
    <version>1.14.6</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-clients_2.11</artifactId>
    <version>1.14.6</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-runtime-web_2.11</artifactId>
    <version>1.14.6</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-api-java-bridge_2.11</artifactId>
    <version>1.14.6</version>
</dependency>
<dependency>
    <groupId>org.apache.flink</groupId>
    <artifactId>flink-table-planner_2.11</artifactId>
    <version>1.14.6</version>
</dependency>

2.main

import com.alibaba.fastjson.JSONObject;
import com.sumec.flink.entity.RuleDto;
import com.sumec.flink.entity.UserProductTagDto;
import com.sumec.flink.entity.UserRule;
import org.apache.commons.lang3.StringUtils;
import org.apache.flink.api.common.RuntimeExecutionMode;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.restartstrategy.RestartStrategies;
import org.apache.flink.api.common.time.Time;
import org.apache.flink.shaded.guava30.com.google.common.collect.Lists;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

public class FlinkClickhouseDemo {

    public static void main(String[] args) {
        // 创建Flink执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);
        env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3, Time.of(10, TimeUnit.SECONDS)));

        // 数据源,读clickhouse,并行度按需设置
        DataStream<Map<String, String>> users = env.addSource(new ClickhouseSource()).setParallelism(1);

        // transformation: map keyBy timeWindow apply
        // 此处简单过滤和转换,按需设置并行度
        SingleOutputStreamOperator<List<UserProductTagDto>> singleDto = users.filter(new FilterFunction<Map<String, String>>() {
            @Override
            public boolean filter(Map<String, String> map) throws Exception {
                return filterData(map, ruleDto);
            }
        }).setParallelism(150).map(new MapFunction<Map<String, String>, List<UserProductTagDto>>() {
            @Override
            public List<UserProductTagDto> map(Map<String, String> stringStringMap) throws Exception {
                return parse(stringStringMap, ruleDto.getAnalysisNo());
            }
        }).setParallelism(150);

        // sink,写clickhouse或其他地方,按需设置并行度
        userProductList.addSink(new ClickHouseSink()).setParallelism(32);

        // execute
        try {
            env.execute();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    // transform
    private static List<UserProductTagDto> parse(Map<String, String> map) throws Exception {
        // 按需处理
    }

    public static boolean filterData(Map<String, String> map, RuleDto ruleDto) {

    }
}

3.自定义source

存量数据:按天分批取数据处理

增量数据:每隔一段时间拉取一次数据

import com.clickhouse.client.internal.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;
import ru.yandex.clickhouse.ClickHouseConnection;
import ru.yandex.clickhouse.ClickHouseDataSource;
import ru.yandex.clickhouse.ClickHouseStatement;
import ru.yandex.clickhouse.settings.ClickHouseProperties;
import ru.yandex.clickhouse.settings.ClickHouseQueryParam;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class ClickhouseSource extends RichParallelSourceFunction<Map<String, String>> {

    private boolean flag = true;
    private ClickHouseConnection conn = null;
    private ClickHouseStatement stmt = null;
    private ResultSet rs = null;
    private Map<ClickHouseQueryParam, String> additionalDBParams = new HashMap<>();

    // open只执行一次,适合开启资源
    @Override
    public void open(Configuration parameters) throws Exception {
        String url = "jdbc:clickhouse://xxxxx:8123";

        ClickHouseProperties properties = new ClickHouseProperties();
        properties.setSessionId(UUID.randomUUID().toString());
//        properties.setUser("default");
//        properties.setPassword("");
        properties.setDatabase("xxx");

        ClickHouseDataSource dataSource = new ClickHouseDataSource(url, properties);
        // ClickHouseProperties
        additionalDBParams.put(ClickHouseQueryParam.SESSION_ID, UUID.randomUUID().toString());
        conn = dataSource.getConnection();
        stmt = conn.createStatement();
    }

    @Override
    public void run(SourceContext<Map<String, String>> ctx) throws Exception {

        String sql = "select * from xxx where event_time between '%s 00:00:00' and '%s 23:59:59' limit %s,%s";
        String start = "2024-01-01";
        String end = "2024-01-01";
        int limitStart = 0;
        int limit = 50000;
        while (flag) {
            String newsql = String.format(sql, start, end, limitStart, limit);

            rs = stmt.executeQuery(newsql, additionalDBParams);
            int c = 0;
            while (rs.next()) {
                c++;
                Map<String, String> hm = Maps.newHashMap();
                ResultSetMetaData rsmd = rs.getMetaData();
                int count = rsmd.getColumnCount();// 获取列的数量
                for (int i = 1; i <= count; i++) {
                    String key = rsmd.getColumnLabel(i);
                    String value = rs.getString(i);
                    hm.put(key, value);
                }
                ctx.collect(hm);
            }

            limitStart += c;

            if (c < limit) {
                // 查完了,换时间
                limitStart = 0;
                LocalDate localDate = LocalDate.parse(start, DateTimeFormatter.ofPattern("yyyy-MM-dd")).plusDays(1);
                start = localDate.toString();
                end = localDate.toString();

                if (localDate.isAfter(LocalDate.now())) {
                    break;
                }
            }
        }
    }

    // 接收到cancel命令时取消数据生成
    @Override
    public void cancel() {
        flag = false;
    }

    @Override
    public void close() throws Exception {
        if (conn != null)
            conn.close();
        if (stmt != null)
            stmt.close();
        if (rs != null)
            rs.close();
    }
}

4.自定义sink

注意:

1.流式处理要求实时性,可即时插入,数据量大时会有性能问题,可以转存kafka

2.对于历史数据处理任务,流式数据做批量插入,可缓存后批量插入,最后一批数据未达到batchSize时会丢失,需要在close方法中再做一次处理。

import com.sumec.flink.entity.UserProductTagDto;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import ru.yandex.clickhouse.ClickHouseConnection;
import ru.yandex.clickhouse.ClickHouseDataSource;
import ru.yandex.clickhouse.ClickHouseStatement;
import ru.yandex.clickhouse.settings.ClickHouseProperties;
import ru.yandex.clickhouse.settings.ClickHouseQueryParam;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

@Slf4j
public class ClickHouseSink extends RichSinkFunction<List<UserProductTagDto>> {
    private ClickHouseConnection conn = null;
    private PreparedStatement stmt = null;
    private ResultSet rs = null;
    private Map<ClickHouseQueryParam, String> additionalDBParams = new HashMap<>();
    private final int batchSize = 10000;
    private String sql = "insert into";

    // open只执行一次,适合开启资源
    @Override
    public void open(Configuration parameters) throws Exception {
        String url = "jdbc:clickhouse://xxxxxx:8123";

        ClickHouseProperties properties = new ClickHouseProperties();
        properties.setSessionId(UUID.randomUUID().toString());
//        properties.setUser("default");
//        properties.setPassword("");
        properties.setDatabase("xxxx");

        ClickHouseDataSource dataSource = new ClickHouseDataSource(url, properties);
        // ClickHouseProperties
        additionalDBParams.put(ClickHouseQueryParam.SESSION_ID, UUID.randomUUID().toString());
        conn = dataSource.getConnection();
        stmt = conn.createStatement(sql);
    }

    @Override
    public void invoke(List<UserProductTagDto> value, Context context) throws Exception {
        if (CollectionUtils.isEmpty(value)) {
            return;
        }

        for (UserProductTagDto dto : value) {
            preparedStatement.setString(1, dto.getUserNo());
            preparedStatement.setString(2, dto.getAnalysisNo());
            preparedStatement.setString(3, dto.getProductTagNo());
            preparedStatement.setInt(4, dto.getActionType());
            preparedStatement.setString(5, dto.getEventTime());
            preparedStatement.setString(6, DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
            preparedStatement.addBatch();
            count++;
        }

        // 流式数据缓存后,批量插入clickhouse,能大大提高性能
        if (count >= batchSize) {
            preparedStatement.executeBatch();
            count = 0;
        }

        // 按需,也可以即时处理
//      preparedStatement.executeBatch();
    }

    @Override
    public void close() throws Exception {
        if (count != 0) {
            // 缓存批量插入,注意插入最后一批数据,防止未达到batchSize丢失
            preparedStatement.executeBatch();
        }
        if (conn != null)
            conn.close();
        if (stmt != null)
            stmt.close();
        if (rs != null)
            rs.close();
    }
}

5.参考文章

Flink(十二)Flink source和sink 的 clickhouse 详细示例 - 知乎

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Flink自定义SinkSource是指用户可以根据自己的需求,编写自己的数据源和数据输出方式。Flink提供了一些内置的SinkSource,但是有时候用户需要根据自己的业务需求,自定义数据源和数据输出方式。 自定义SinkSource需要实现Flink提供的接口,例如实现SinkFunction接口来自定义数据输出方式,实现SourceFunction接口来自定义数据源。用户可以根据自己的需求,实现这些接口,然后将自定义SinkSource应用到Flink程序中。 自定义SinkSource可以帮助用户更好地满足自己的业务需求,提高数据处理的效率和准确性。 ### 回答2: Flink自定义SinkSource方便开发人员根据特定业务需求定制化的数据输入和输出。这也是Flink作为DataStream处理引擎的一个强大特性。 自定义Sink的主要作用是将Flink处理的数据流输出到外部存储或处理系统中,如Kafka、Hadoop、Elasticsearch、MySQL等。通过自定义Sink,我们可以满足不同业务场景下,数据输出的不同需求。 自定义Sink的实现需要继承Flink提供的`RichSinkFunction`或者`SinkFunction`抽象类,并实现其抽象方法。`RichSinkFunction`中提供了一些状态管理的方法,如`open`、`close`等,我们可以在这些方法中添加额外的代码逻辑。自定义SinkFunction可以重写invoke方法,将不需要状态管理的代码集中在此方法中。 自定义Source的主要作用是将外部数据源中的数据取并发送给Flink的DataStream处理模块。自定义Source可以取各种类型的数据源,如Kafka、文件、Socket等。 自定义Source的实现需要继承Flink提供的`RichParallelSourceFunction`或者`SourceFunction`抽象类,并实现其抽象方法。`RichParallelSourceFunction`中支持在并行算子中运行,因此对于大规模数据的处理尤为适合。 在自定义Source中,需要实现一个`run`方法和一个`cancel`方法。`run`方法中是数据源处理逻辑的主要实现,`cancel`方法用于停止数据源的取。我们还可以通过Flink提供的Checkpoint机制来管理数据源。 总之,自定义SinkSourceFlink处理数据流的重要特性,使得开发人员可以根据业务需求灵活定制化的输入输出逻辑。 ### 回答3: Flink是一个开源流式处理框架,它提供了丰富的内置SinkSource,同时也支持用户自定义SinkSource,以便满足不同的业务需求。 自定义Sink可以用于将流式数据写入外部系统中,比如数据库、消息队列和文件系统等。Flink提供了一个简单的接口SinkFunction,通过实现该接口可以快速开发自己的SinkSinkFunction接口定义了一个抽象方法invoke(),该方法是在每个输入元素处理完成时被调用。开发者需要编写自己的业务逻辑,在invoke()中实现将数据写入目标系统的逻辑。 自定义Source可以用于从外部系统取数据,并将其逐个交付给Flink程序进行处理。同样地,Flink也提供了一个简单的接口SourceFunction,通过实现该接口可以快速开发自己的SourceSourceFunction接口定义了两个抽象方法:run()和cancel()。run()方法是在源自生命周期内调用的,它是源自执行主逻辑的地方。cancel()方法是用于清理资源的。开发者需要在run()方法中编写从外部系统取数据的逻辑,并且能够异步地产生数据,最后将数据通过SourceContext将数据一条一条源源不断地输出。 自定义SinkSourceFlink框架中非常常用的一个扩展方式,它可以满足用户自定义的需求,在具体的业务场景中,能够灵活的使用自定义SinkSource对数据的处理进行个性化的定制化。同时,自定义SinkSource的开发也相对简单,可以通过实现简单的接口,快速完成自定义SinkSource的开发。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值