Flink基于 Table API 实现实时报表

欢迎关注今日头条号、微信公众号、知乎号:仰望夜空一万次

随意聊聊并记录从小城市到上海工作生活的所思所想。

不去记录,有些事情都好像没有发生过。

示例作用
1.示例提供了docker命令启动,可以查看控制台的各项指标。
2.可以参考docker编排脚本,自己开发基于docker的交付软件
3.参考此项目的上一级项目flink-playground的data-generator项目,获得使用kafka模拟持续数据流入的示例
4.学习docker操作命令


编码值得借鉴的点:
1.SpendReportTest测试用例Matchers.containsInAnyOrder
2.SpendReportTest类try catch 资源方式写法
3.SpendReportTest使用批的方式构造数据
4.SpendReportTest类中LocalDateTime类的使用
5.SpendReportTest测试用例运行通过,可以作为以后写测试用例的模板

Apache Flink提供Table API作为批处理和流处理的统一的关系API,即查询在无边界的实时流或有边界的批处理数据集上以相同的语义执行,并产生相同的结果。 Flink中的Table API通常用于简化数据分析,数据管道和ETL应用程序的定义。

 

SpendReport主类

public class SpendReport {

/**目的是建立一个报告,显示每个帐户在一天中每个小时的总支出。 这意味着时间戳列需要从毫秒舍入到小时粒度。
就像SQL查询一样,Flink可以选择必填字段并按您的键进行分组。 这些功能以及floor和sum等内置函数可以编写此report业务函数。*/
    public static Table report(Table rows) {
    /**
    两种方式都可以,逻辑一样
    return   rows.select(
         $("account_id"),
         $("transaction_time").floor(TimeIntervalUnit.HOUR).as("log_ts"),
         $("amount"))
         .groupBy($("account_id"),$("log_ts"))
         .select($("account_id"),$("log_ts"),$("amount").sum().as("amount"));
         */
        return rows.window(Tumble.over(lit(1).hour()).on($("transaction_time")).as("log_ts"))
                .groupBy($("account_id"), $("log_ts"))
                .select(
                        $("account_id"),
                        $("log_ts").start().as("log_ts"),
                        $("amount").sum().as("amount"));
    }
    
//第三种实现方式,使用自定义的函数代替floor函数
//Flink包含有限数量的内置函数,有时您需要使用用户定义的函数对其进行扩展。 如果未预先定义floor,则可以自己实现,SpendReportTest测试通过。
/**return rows.select(
        $("account_id"),
        call(MyFloor.class, $("transaction_time")).as("log_ts"),
        $("amount"))
        .groupBy($("account_id"), $("log_ts"))
        .select(
                $("account_id"),
                $("log_ts"),
                $("amount").sum().as("amount")); */

    public static void main(String[] args) throws Exception {
    /**设置TableEnvironment。 
    表环境是您可以为Job设置属性,指定是编写批处理应用程序还是流应用程序以及创建源的方法。 
    本演练将创建一个使用流运行时的标准表环境。*/
        EnvironmentSettings settings = EnvironmentSettings.newInstance().build();
        TableEnvironment tEnv = TableEnvironment.create(settings);

/**接下来,在可用于连接到外部系统的环境中注册表,以读取和写入批处理和流数据。 table source提供对存储在外部系统(例如数据库,键值存储,消息队列或文件系统)中的数据的访问。 table sink将表发送到外部存储系统。 根据源和接收器的类型,它们支持不同的格式,例如CSV,JSON,Avro或Parquet。*/
        tEnv.executeSql("CREATE TABLE transactions (\n" +
                "    account_id  BIGINT,\n" +
                "    amount      BIGINT,\n" +
                "    transaction_time TIMESTAMP(3),\n" +
                "    WATERMARK FOR transaction_time AS transaction_time - INTERVAL '5' SECOND\n" +
                ") WITH (\n" +
                "    'connector' = 'kafka',\n" +
                "    'topic'     = 'transactions',\n" +
                "    'properties.bootstrap.servers' = 'kafka:9092',\n" +
                "    'format'    = 'csv'\n" +
                ")");

/**
两个表被注册; 交易输入表和支出报告输出表。 通过交易(交易)表,我们可以读取信用卡交易,其中包含帐户ID(account_id),时间戳记(transaction_time)和美元金额(金额)。 该表是有关Kafka主题(包含CSV数据的事务)的逻辑视图。
第二个表spread_report存储聚合的最终结果。 它的基础存储是MySql数据库中的表。*/
        tEnv.executeSql("CREATE TABLE spend_report (\n" +
                "    account_id BIGINT,\n" +
                "    log_ts     TIMESTAMP(3),\n" +
                "    amount     BIGINT\n," +
                "    PRIMARY KEY (account_id, log_ts) NOT ENFORCED" +
                ") WITH (\n" +
                "  'connector'  = 'jdbc',\n" +
                "  'url'        = 'jdbc:mysql://mysql:3306/sql-demo',\n" +
                "  'table-name' = 'spend_report',\n" +
                "  'driver'     = 'com.mysql.jdbc.Driver',\n" +
                "  'username'   = 'sql-demo',\n" +
                "  'password'   = 'demo-sql'\n" +
                ")");

/**配置好环境并注册表后,就可以构建第一个应用程序了。 您可以从TableEnvironment中读取输入表以读取其行,然后使用executeInsert将这些结果写入输出表中。*/
        Table transactions = tEnv.from("transactions");
        report(transactions).executeInsert("spend_report");
    }
}

该查询使用交易表中的所有记录,计算报告,并以有效,可扩展的方式输出结果。 使用此实现运行测试将通过。

 

 

SpendReportTest测试类

该项目包含一个辅助测试类SpendReportTest,用于验证报告的逻辑。 它以批处理方式创建表环境。

public class SpendReportTest {

    private static final LocalDateTime DATE_TIME = LocalDateTime.of(2020, 1, 1, 0, 0);
    
    @Test
    public void testReport() {
        EnvironmentSettings settings = EnvironmentSettings.newInstance().inBatchMode().build();
        TableEnvironment tEnv = TableEnvironment.create(settings);

        Table transactions =
                tEnv.fromValues(
                        DataTypes.ROW(
                                DataTypes.FIELD("account_id", DataTypes.BIGINT()),
                                DataTypes.FIELD("amount", DataTypes.BIGINT()),
                                DataTypes.FIELD("transaction_time", DataTypes.TIMESTAMP(3))),
                        Row.of(1, 188, DATE_TIME.plusMinutes(12)),
                        Row.of(2, 374, DATE_TIME.plusMinutes(47)),
                        Row.of(3, 112, DATE_TIME.plusMinutes(36)),
                        Row.of(4, 478, DATE_TIME.plusMinutes(3)),
                        Row.of(5, 208, DATE_TIME.plusMinutes(8)),
                        Row.of(1, 379, DATE_TIME.plusMinutes(53)),
                        Row.of(2, 351, DATE_TIME.plusMinutes(32)),
                        Row.of(3, 320, DATE_TIME.plusMinutes(31)),
                        Row.of(4, 259, DATE_TIME.plusMinutes(19)),
                        Row.of(5, 273, DATE_TIME.plusMinutes(42)));

        try {
            TableResult results = SpendReport.report(transactions).execute();

            MatcherAssert.assertThat(
                    materialize(results),
                    Matchers.containsInAnyOrder(
                            Row.of(1L, DATE_TIME, 567L),
                            Row.of(2L, DATE_TIME, 725L),
                            Row.of(3L, DATE_TIME, 432L),
                            Row.of(4L, DATE_TIME, 737L),
                            Row.of(5L, DATE_TIME, 481L)));
        } catch (UnimplementedException e) {
            Assume.assumeNoException("The walkthrough has not been implemented", e);
        }
    }
    
    private static List<Row> materialize(TableResult results) {
        try (CloseableIterator<Row> resultIterator = results.collect()) {
            return StreamSupport
                    .stream(Spliterators.spliteratorUnknownSize(resultIterator, Spliterator.ORDERED), false)
                    .collect(Collectors.toList());
        } catch (Exception e) {
            throw new RuntimeException("Failed to materialize results", e);
        }
    }
}

Flink的独特属性之一是,它在批处理和流传输之间提供一致的语义。 这意味着您可以在静态数据集上以批处理模式开发和测试应用程序,并以流应用程序的形式部署到生产环境中!

 

 

用户自定义函数

实现floor函数的功能

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

import org.apache.flink.table.annotation.DataTypeHint;
import org.apache.flink.table.functions.ScalarFunction;

public class MyFloor extends ScalarFunction {

    public @DataTypeHint("TIMESTAMP(3)") LocalDateTime eval(
            @DataTypeHint("TIMESTAMP(3)") LocalDateTime timestamp) {

        return timestamp.truncatedTo(ChronoUnit.HOURS);
    }
}

添加窗口函数

基于时间对数据进行分组是数据处理中的典型操作,尤其是在处理无限流时。 基于时间的分组称为窗口,而Flink提供了灵活的窗口语义。 最基本的窗口类型称为滚动窗口,该窗口具有固定大小,并且其存储桶不重叠。

public static Table report(Table rows) {
    return rows.window(Tumble.over(lit(1).hour()).on($("transaction_time")).as("log_ts"))
        .groupBy($("account_id"), $("log_ts"))
        .select(
            $("account_id"),
            $("log_ts").start().as("log_ts"),
            $("amount").sum().as("amount"));
}

这将您的应用程序定义为使用基于timestamp列的一小时滚动窗口。 因此,将带有时间戳2019-06-01 01:23:47的行放在2019-06-01 01:00:00窗口中。

 

基于时间的聚合是唯一的,因为与其他属性相反,时间通常在连续流应用程序中向前移动。 与floor和UDF不同,窗口函数是内部函数,它允许运行时应用其他优化。 在批处理上下文中,Windows提供了一种方便的API,用于按timestamp属性对记录进行分组。

 

使用此实现运行测试也将通过。

 

启动应用

就是这样,一个功能齐全,有状态的分布式流应用程序! 查询持续消耗来自Kafka的交易流,计算每小时支出,并在准备就绪后立即发出结果。 由于输入是无界的,因此查询将一直运行,直到手动将其停止为止。 而且由于作业使用基于时间窗口的聚合,因此Flink可以执行特定的优化,例如当框架知道不再有特定窗口的记录到达时,进行状态清除。

 

这个示例已经配置docker,可作为流应用程序在本地运行。 该环境包含一个Kafka topic,一个连续数据生成器,MySql和Grafana。

 

从table-walkthrough文件夹中启动docker-compose脚本。

$ docker-compose build (首次运行,只运行一次完成编译)
$ docker-compose up -d  

 

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值