Flink 系例 之 TableAPI & SQL 与 MYSQL 分组统计

使用 Tbale&SQL 与 Flink JDBC 连接器读取 MYSQL 数据,并用 GROUP BY 语句根据一个或多个列对结果集进行分组。

示例环境

java.version: 1.8.x
flink.version: 1.11.1
kafka:2.11

示例数据源 (项目码云下载)

Flink 系例 之 搭建开发环境与数据

示例模块 (pom.xml)

Flink 系例 之 TableAPI & SQL 与 示例模块

GroupToMysql.java

package com.flink.examples.mysql;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.types.Row;
import org.apache.flink.util.Collector;

import static org.apache.flink.table.api.Expressions.$;

/**
 * @Description 使用Tbale&SQL与Flink JDBC连接器读取MYSQL数据,并用GROUP BY语句根据一个或多个列对结果集进行分组。
 */
public class GroupToMysql {

    /**
     官方参考:https://ci.apache.org/projects/flink/flink-docs-release-1.11/dev/table/connectors/jdbc.html

     分区扫描
     为了加速并行Source任务实例中的数据读取,Flink为JDBC表提供了分区扫描功能。
     scan.partition.column:用于对输入进行分区的列名。
     scan.partition.num:分区数。
     scan.partition.lower-bound:第一个分区的最小值。
     scan.partition.upper-bound:最后一个分区的最大值。
     */

    //flink-jdbc-1.11.1写法,所有属性名在JdbcTableSourceSinkFactory工厂类中定义
    static String table_sql =
            "CREATE TABLE my_users (\n" +
                    "  id BIGINT,\n" +
                    "  name STRING,\n" +
                    "  age INT,\n" +
                    "  status INT,\n" +
                    "  PRIMARY KEY (id) NOT ENFORCED\n" +
                    ") WITH (\n" +
                    "  'connector.type' = 'jdbc',\n" +
                    "  'connector.url' = 'jdbc:mysql://192.168.110.35:3306/flink?useUnicode=true&characterEncoding=utf-8', \n" +
                    "  'connector.driver' = 'com.mysql.jdbc.Driver', \n" +
                    "  'connector.table' = 'users', \n" +
                    "  'connector.username' = 'root',\n" +
                    "  'connector.password' = 'password' \n" +
//                    "  'connector.read.fetch-size' = '10' \n" +
                    ")";

    public static void main(String[] args) throws Exception {
        //构建StreamExecutionEnvironment
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //设置setParallelism并行度
        env.setParallelism(1);
        //构建EnvironmentSettings 并指定Blink Planner
        EnvironmentSettings bsSettings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build();
        //构建StreamTableEnvironment
        StreamTableEnvironment tEnv = StreamTableEnvironment.create(env, bsSettings);
        //注册mysql数据维表
        tEnv.executeSql(table_sql);

        //Table table = avg(tEnv);
        //Table table = count(tEnv);
        //Table table = min(tEnv);
        Table table = max(tEnv);

        //打印字段结构
        table.printSchema();

        //普通查询操作用toAppendStream
        //tEnv.toAppendStream(table, Row.class).print();
        //group操作用toRetractStream
        //tEnv.toRetractStream(table, Row.class).print();

        //table 转成 dataStream 流,Tuple2第一个参数flag是true表示add添加新的记录流,false表示retract表示旧的记录流
        DataStream<Tuple2<Boolean, Row>> behaviorStream = tEnv.toRetractStream(table, Row.class);
        behaviorStream.flatMap(new FlatMapFunction<Tuple2<Boolean, Row>, Object>() {
            @Override
            public void flatMap(Tuple2<Boolean, Row> value, Collector<Object> out) throws Exception {
                if (value.f0) {
                    System.out.println(value.f1.toString());
                }
            }
        }).print();

        env.execute();
    }

    /**
     * avg 获取一组数据流中的数值平均值
     * @param tEnv
     * @return
     */
    public static Table avg(StreamTableEnvironment tEnv){
        //第一种:执行SQL
        String sql = "select status,avg(age) as age1 from my_users group by status";
        //Table table = tEnv.sqlQuery(sql);

        //第二种:通过方法拼装执行语句
        Table table = tEnv.from("my_users").groupBy($("status")).select($("status"),$("age").avg().as("age1"));
        return table;
    }

    /**
     * count 获取一组数据流中累加分组的行数之和
     * @param tEnv
     * @return
     */
    public static Table count(StreamTableEnvironment tEnv){
        //第一种:执行SQL
        String sql = "select status,count(age) as age1 from my_users group by status";
        //Table table = tEnv.sqlQuery(sql);

        //第二种:通过方法拼装执行语句
        Table table = tEnv.from("my_users").groupBy($("status")).select($("status"),$("age").count().as("age1"));
        return table;
    }

    /**
     * sum 获取一组数据流中累加分组的数值之和
     * @param tEnv
     * @return
     */
    public static Table sum(StreamTableEnvironment tEnv){
        //第一种:执行SQL
        String sql = "select status,sum(age) as age1 from my_users group by status";
        //Table table = tEnv.sqlQuery(sql);

        //第二种:通过方法拼装执行语句
        Table table = tEnv.from("my_users").groupBy($("status")).select($("status"),$("age").sum().as("age1"));
        return table;
    }


    /**
     * min 获取一组数据流中的最小值
     * @param tEnv
     * @return
     */
    public static Table min(StreamTableEnvironment tEnv){
        //第一种:执行SQL
        String sql = "select status,min(age) as age1 from my_users group by status";
        //Table table = tEnv.sqlQuery(sql);

        //第二种:通过方法拼装执行语句
        Table table = tEnv.from("my_users").groupBy($("status")).select($("status"),$("age").min().as("age1"));
        return table;
    }

    /**
     * max 获取一组数据流中的最大值,每一次数据加入,则计算一次
     * @param tEnv
     * @return
     */
    public static Table max(StreamTableEnvironment tEnv){
        //第一种:执行SQL
        String sql = "select status,max(age) as age1 from my_users group by status";
        //Table table = tEnv.sqlQuery(sql);

        //第二种:通过方法拼装执行语句
        Table table = tEnv.from("my_users").groupBy($("status")).select($("status"),$("age").max().as("age1"));
        return table;
    }

}

建表 SQL

CREATE TABLE `users` (
  `id` bigint(8) NOT NULL AUTO_INCREMENT,
  `name` varchar(40) DEFAULT NULL,
  `age` int(8) DEFAULT NULL,
  `status` tinyint(2) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

打印结果

root
 |-- status: INT
 |-- age1: INT

0,16
0,18
1,21
1,28
2,31
&lt;think&gt;我们正在讨论如何使用Flink处理MySQL数据流。根据用户需求,需要实现实时计算。Flink提供了多种连接MySQL的方式,包括作为数据源(Source)和数据汇(Sink)。此外,FlinkSQL也可以用于实时计算。参考之前提供的引用[^1]中提到了JDBCAppendTableSink,这是用于写入MySQL的。但用户的问题更广泛,包括实时计算整个流程。步骤:1.从MySQL读取数据(作为数据源)2.对数据流进行转换和计算(实时计算)3.将计算结果写入MySQL或其他存储(作为数据汇)Flink提供了不同的API:DataStreamAPI和TableAPI/SQL。这里我们主要展示DataStreamAPI的方式,因为引用中使用了DataStreamAPI。但是,Flink本身并不提供MySQL的流式数据源(CDC变更数据捕获),通常需要借助Debezium等工具捕获MySQL的binlog,然后通过Kafka传递给Flink。不过,Flink1.11+引入了CDC连接器,可以直接使用。由于问题中没有指定具体版本,我们以Flink1.12为,介绍两种方式:方式一:使用传统的JDBC连接器读取MySQL,但这种方式是批处理,不是流处理。所以对于实时流,我们需要使用CDC方式。方式二:使用FlinkCDC连接器(开源项目:https://github.com/ververica/flink-cdc-connectors)来实现实时捕获MySQL变更。这里我们采用方式二(因为用户需要实时计算),但注意FlinkCDC连接器不是Flink官方发行版的一部分,需要额外添加依赖。步骤分解:1.设置环境,添加依赖(在项目中引入flink-sql-connector-mysql-cdc)2.创建流执行环境3.使用CDC连接器创建MySQL数据源4.对数据流进行转换计算5.将计算结果写入MySQL(使用JDBCSink)由于用户问题中提到了&ldquo;实时计算&rdquo;,我们假设需要计算一些指标,如每5秒的订单总额(假设数据流是订单表)。注意:以下代码示基于Flink1.12和FlinkCDC1.2.0,实际使用请根据版本调整依赖。首先,添加Maven依赖(在pom.xml中):```xml&lt;dependency&gt;&lt;groupId&gt;com.alibaba.ververica&lt;/groupId&gt;&lt;artifactId&gt;flink-connector-mysql-cdc&lt;/artifactId&gt;&lt;version&gt;1.2.0&lt;/version&gt;&lt;/dependency&gt;&lt;!--FlinkJDBCConnector--&gt;&lt;dependency&gt;&lt;groupId&gt;org.apache.flink&lt;/groupId&gt;&lt;artifactId&gt;flink-connector-jdbc_2.11&lt;/artifactId&gt;&lt;version&gt;1.12.0&lt;/version&gt;&lt;/dependency&gt;&lt;dependency&gt;&lt;groupId&gt;mysql&lt;/groupId&gt;&lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;&lt;version&gt;8.0.19&lt;/version&gt;&lt;/dependency&gt;```然后,编写代码:示:实时计算订单表中每5秒的订单总金额,并将结果写入MySQL的另一张表。步骤:1.创建MySQLCDC数据源2.转换数据流,提取订单金额和事件时间(假设表中有金额字段和创建时间字段)3.设置时间特性和水位线4.按5秒时间窗口聚合5.将结果写入MySQL代码示:```javaimportorg.apache.flink.api.common.eventtime.WatermarkStrategy;importorg.apache.flink.streaming.api.datastream.DataStream;importorg.apache.flink.streaming.api.environment.StreamExecutionEnvironment;importorg.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;importorg.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;importorg.apache.flink.streaming.api.windowing.time.Time;importorg.apache.flink.streaming.api.windowing.windows.TimeWindow;importorg.apache.flink.util.Collector;importcom.ververica.cdc.debezium.JsonDebeziumDeserializationSchema;importcom.ververica.cdc.connectors.mysql.source.MySqlSource;importorg.apache.flink.connector.jdbc.JdbcSink;importjava.sql.PreparedStatement;importjava.sql.SQLException;importjava.time.Duration;publicclassFlinkMySQLCDCExample{publicstaticvoidmain(String[]args)throwsException{//创建执行环境StreamExecutionEnvironmentenv=StreamExecutionEnvironment.getExecutionEnvironment();env.setParallelism(1);//创建MySQLCDC源MySqlSource&lt;String&gt;mySqlSource=MySqlSource.&lt;String&gt;builder().hostname(&quot;localhost&quot;).port(3306).databaseList(&quot;testdb&quot;)//设置数据库.tableList(&quot;testdb.orders&quot;)//设置表.username(&quot;root&quot;).password(&quot;password&quot;).deserializer(newJsonDebeziumDeserializationSchema())//使用JSON格式.build();//添加数据源DataStream&lt;String&gt;source=env.fromSource(mySqlSource,WatermarkStrategy.noWatermarks(),&quot;MySQLSource&quot;);//解析JSON字符串(这里简化处理,实际应解析为POJO)DataStream&lt;Order&gt;orderStream=source.map(json-&gt;{//解析JSON,这里需要根据Debezium的JSON格式解析//假设我们解析出id,amount,create_time//由于解析JSON比较复杂,这里用伪代码代替//实际中可以使用Jackson或Gson解析//这里假设解析结果为:id,amount,create_time//创建Order对象returnparseOrderFromJson(json);}).assignTimestampsAndWatermarks(WatermarkStrategy.&lt;Order&gt;forBoundedOutOfOrderness(Duration.ofSeconds(5)).withTimestampAssigner((event,timestamp)-&gt;event.getCreateTime().getTime()));//按5秒窗口计算订单总金额DataStream&lt;OrderSummary&gt;result=orderStream.keyBy(Order::getUserId)//假设按用户分组,如果没有则可以不分组(但通常会有分组).window(TumblingEventTimeWindows.of(Time.seconds(5))).process(newProcessWindowFunction&lt;Order,OrderSummary,Integer,TimeWindow&gt;(){@Overridepublicvoidprocess(Integerkey,Contextcontext,Iterable&lt;Order&gt;elements,Collector&lt;OrderSummary&gt;out){doublesum=0.0;for(Orderorder:elements){sum+=order.getAmount();}out.collect(newOrderSummary(key,context.window().getEnd(),sum));}});//将结果写入MySQLresult.addSink(JdbcSink.sink(&quot;INSERTINTOorder_summary(user_id,window_end,total_amount)VALUES(?,?,?)&quot;,(PreparedStatementps,OrderSummarysummary)-&gt;{ps.setInt(1,summary.getUserId());ps.setTimestamp(2,newjava.sql.Timestamp(summary.getWindowEnd()));ps.setDouble(3,summary.getTotalAmount());},newJdbcConnectionOptions.JdbcConnectionOptionsBuilder().withUrl(&quot;jdbc:mysql://localhost:3306/testdb&quot;).withDriverName(&quot;com.mysql.jdbc.Driver&quot;).withUsername(&quot;root&quot;).withPassword(&quot;password&quot;).build()));env.execute(&quot;FlinkMySQLCDCExample&quot;);}//解析JSON的辅助方法(伪代码)privatestaticOrderparseOrderFromJson(Stringjson){//解析逻辑,返回Order对象returnnewOrder();}//订单类(POJO)publicstaticclassOrder{privateintid;privateintuserId;privatedoubleamount;privatejava.util.DatecreateTime;//gettersandsetterspublicintgetUserId(){returnuserId;}publicdoublegetAmount(){returnamount;}publicjava.util.DategetCreateTime(){returncreateTime;}}//订单汇总结果类publicstaticclassOrderSummary{privateintuserId;privatelongwindowEnd;privatedoubletotalAmount;publicOrderSummary(intuserId,longwindowEnd,doubletotalAmount){this.userId=userId;this.windowEnd=windowEnd;this.totalAmount=totalAmount;}//getterspublicintgetUserId(){returnuserId;}publiclonggetWindowEnd(){returnwindowEnd;}publicdoublegetTotalAmount(){returntotalAmount;}}}```注意:上述代码中解析JSON部分需要根据Debezium产生的JSON格式进行解析,实际格式比较复杂,包含元数据和数据变更内容。通常,我们关心的是`after`字段(代表更新后的数据)和`op`字段(操作类型)。因此,解析时需要提取`after`字段中的内容。另外,FlinkCDC也提供了直接解析为RowData的方式,可以简化处理。但这里为了通用性,使用了JSON格式。如果使用TableAPI,可以更简洁地实现,特别是Flink1.12支持了DDL创建CDC表。使用TableAPI的示SQL风格):```java//创建表环境StreamTableEnvironmenttableEnv=StreamTableEnvironment.create(env);//创建MySQLCDC表tableEnv.executeSql(&quot;CREATETABLEorders(&quot;+&quot;idINT,&quot;+&quot;user_idINT,&quot;+&quot;amountDOUBLE,&quot;+&quot;create_timeTIMESTAMP(3),&quot;+&quot;PRIMARYKEY(id)NOTENFORCED&quot;+&quot;)WITH(&quot;+&quot;&#39;connector&#39;=&#39;mysql-cdc&#39;,&quot;+&quot;&#39;hostname&#39;=&#39;localhost&#39;,&quot;+&quot;&#39;port&#39;=&#39;3306&#39;,&quot;+&quot;&#39;username&#39;=&#39;root&#39;,&quot;+&quot;&#39;password&#39;=&#39;password&#39;,&quot;+&quot;&#39;database-name&#39;=&#39;testdb&#39;,&quot;+&quot;&#39;table-name&#39;=&#39;orders&#39;&quot;+&quot;)&quot;);//创建结果表(用于写入MySQL)tableEnv.executeSql(&quot;CREATETABLEorder_summary(&quot;+&quot;user_idINT,&quot;+&quot;window_endTIMESTAMP(3),&quot;+&quot;total_amountDOUBLE,&quot;+&quot;PRIMARYKEY(user_id,window_end)NOTENFORCED&quot;+&quot;)WITH(&quot;+&quot;&#39;connector&#39;=&#39;jdbc&#39;,&quot;+&quot;&#39;url&#39;=&#39;jdbc:mysql://localhost:3306/testdb&#39;,&quot;+&quot;&#39;table-name&#39;=&#39;order_summary&#39;,&quot;+&quot;&#39;username&#39;=&#39;root&#39;,&quot;+&quot;&#39;password&#39;=&#39;password&#39;&quot;+&quot;)&quot;);//执行查询并插入结果表tableEnv.executeSql(&quot;INSERTINTOorder_summary&quot;+&quot;SELECTuser_id,TUMBLE_END(create_time,INTERVAL&#39;5&#39;SECOND)aswindow_end,SUM(amount)astotal_amount&quot;+&quot;FROMorders&quot;+&quot;GROUPBYuser_id,TUMBLE(create_time,INTERVAL&#39;5&#39;SECOND)&quot;);```以上两种方式(DataStreamAPI和TableAPI)都可以实现需求。TableAPI更简洁,但需要熟悉SQL语法。总结:Flink实时处理MySQL数据流的关键在于使用CDC连接器捕获MySQL的变更数据,然后进行流式处理,最后将结果写入MySQL或其他存储。注意事项:-FlinkCDC连接器需要额外依赖。-实时处理需要事件时间,因此数据中需要包含时间戳字段。-写入MySQL时,要确保结果表存在,且主键设置正确以避免重复。根据用户需求,我们提供了两种实现方式。用户可以根据自己的熟悉程度和项目要求选择。&lt;/think&gt;### Fllink 实时计算 MySQL 数据流处理指南 Flink 处理 MySQL 实时数据流通常包含三个核心环节:**数据源接入 &rarr; 流式处理 &rarr; 结果输出**。以下是完整实现方案: --- #### 一、数据源接入(MySQL &rarr; FlinkFlink 支持两种 MySQL 接入方式: 1. **CDC 变更捕获**(推荐实时场景) ```java // 使用 Flink CDC 连接器 import com.ververica.cdc.debezium.JsonDebeziumDeserializationSchema; import com.ververica.cdc.connectors.mysql.MySqlSource; MySqlSource&lt;String&gt; mySqlSource = MySqlSource.&lt;String&gt;builder() .hostname(&quot;localhost&quot;) .port(3306) .databaseList(&quot;your_db&quot;) .tableList(&quot;your_db.orders&quot;) // 监控表 .username(&quot;user&quot;) .password(&quot;pass&quot;) .deserializer(new JsonDebeziumDeserializationSchema()) .build(); DataStreamSource&lt;String&gt; cdcStream = env.fromSource( mySqlSource, WatermarkStrategy.noWatermarks(), &quot;MySQL CDC Source&quot; ); ``` 2. **JDBC 批连接**(定时轮询) ```java // 创建 JDBC 输入格式 JdbcInputFormat jdbcInput = JdbcInputFormat.buildJdbcInputFormat() .setDrivername(&quot;com.mysql.jdbc.Driver&quot;) .setDBUrl(&quot;jdbc:mysql://localhost:3306/db&quot;) .setUsername(&quot;user&quot;) .setPassword(&quot;pass&quot;) .setQuery(&quot;SELECT * FROM orders WHERE update_time &gt; ?&quot;) .setRowTypeInfo(rowTypeInfo) .finish(); DataStream&lt;Row&gt; jdbcStream = env.createInput(jdbcInput) .assignTimestampsAndWatermarks(...); ``` --- #### 二、流式数据处理 实现实时计算逻辑(示:每5分钟订单总额统计): ```java // 1. 转换数据流 DataStream&lt;Order&gt; orders = cdcStream.flatMap(json -&gt; { // 解析 JSON 变更数据 Order order = parseDebeziumEvent(json); return Collections.singletonList(order); }); // 2. 开窗聚合 DataStream&lt;OrderSummary&gt; result = orders .keyBy(Order::getProductId) .window(TumblingEventTimeWindows.of(Time.minutes(5))) .aggregate(new OrderAggregator()); // 3. 自定义聚合函数 public static class OrderAggregator implements AggregateFunction&lt;Order, OrderAccumulator, OrderSummary&gt; { @Override public OrderAccumulator createAccumulator() { return new OrderAccumulator(); } @Override public OrderAccumulator add(Order order, OrderAccumulator acc) { acc.totalAmount += order.getAmount(); acc.count++; return acc; } @Override public OrderSummary getResult(OrderAccumulator acc) { return new OrderSummary(acc.productId, acc.totalAmount, acc.count); } // ... merge 方法实现 } ``` --- #### 三、结果写入 MySQL 使用 JDBC Sink 输出计算结果: ```java // 创建 JDBC Sink JDBCAppendTableSink sink = JDBCAppendTableSink.builder() .setDrivername(&quot;com.mysql.jdbc.Driver&quot;) .setDBUrl(&quot;jdbc:mysql://localhost:3306/analytics_db&quot;) .setUsername(&quot;flink&quot;) .setPassword(&quot;flink_pass&quot;) .setQuery(&quot;INSERT INTO order_summary (product_id, total_amount, count) VALUES (?,?,?)&quot;) .setParameterTypes(BasicTypeInfo.INT_TYPE_INFO, BasicTypeInfo.DOUBLE_TYPE_INFO, BasicTypeInfo.INT_TYPE_INFO) .build(); // 转换 DataStream 为 Table Table resultTable = tableEnv.fromDataStream(result); tableEnv.registerTableSink(&quot;mysql_sink&quot;, sink.getTableSchema(), sink); // 写入 MySQL resultTable.insertInto(&quot;mysql_sink&quot;); ``` --- #### 四、执行完整任务 ```java public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.enableCheckpointing(60_000); // 启用检查点 // 1. 构建数据源 DataStream&lt;Order&gt; source = buildSource(env); // 2. 实时计算 DataStream&lt;OrderSummary&gt; result = processStream(source); // 3. 结果输出 result.addSink(buildJdbcSink()); env.execute(&quot;MySQL Real-Time Processing&quot;); } ``` --- ### 关键配置建议 1. **容错机制** - 启用检查点:`env.enableCheckpointing(interval)` - JDBC Sink 开启事务:`.setBatchSize(100).setMaxRetries(3)` 2. **性能优化** - 并行度设置:`env.setParallelism(4)` - CDC 参数:`.serverId(&quot;5400-5404&quot;)`(避免集群冲突) 3. **状态管理** - RocksDB 状态后端:适合大状态场景 ```java env.setStateBackend(new RocksDBStateBackend(&quot;hdfs:///checkpoints&quot;)); ``` --- ### 典型应用场景 1. 实时订单分析仪表盘 2. 用户行为实时统计 3. 库存变更实时预警 4. 金融交易实时风控[^1]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值