|
java
package com.ecommerce;
import com.ecommerce.entity.GoodsInfo;
import com.ecommerce.entity.Order;
import com.ecommerce.entity.OrderWithGoods;
import com.ecommerce.entity.OrderWindowResult;
import com.ecommerce.source.GoodsSource;
import com.ecommerce.source.OrderSource;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.state.MapStateDescriptor;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.connector.jdbc.JdbcConnectionOptions;
import org.apache.flink.connector.jdbc.JdbcExecutionOptions;
import org.apache.flink.connector.jdbc.JdbcSink;
import org.apache.flink.streaming.api.datastream.BroadcastStream;
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.streaming.api.functions.co.BroadcastProcessFunction;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.math.BigDecimal;
import java.time.Duration;
import java.sql.PreparedStatement;
public class EcommerceOrderJob {
// MySQL数据库连接参数(根据自己环境修改)
private static final String MYSQL_URL = "jdbc:mysql://localhost:3306/flink_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true";
private static final String MYSQL_USER = "root";
private static final String MYSQL_PASSWORD = "123456";
public static void main(String[] args) throws Exception {
// 1. 初始化流环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 开启Checkpoint,保障Exactly-Once一致性(数据库入库必须开启)
env.enableCheckpointing(3000);
// 2. 订单流 + 事件时间水印(处理2秒内乱序数据)
DataStream<Order> orderStream = env.addSource(new OrderSource())
.assignTimestampsAndWatermarks(
WatermarkStrategy.<Order>forBoundedOutOfOrderness(Duration.ofSeconds(2))
.withTimestampAssigner((order, ts) -> order.getOrderTime().getTime())
);
// 3. 数据清洗:过滤无效订单、用户ID脱敏
SingleOutputStreamOperator<Order> cleanOrderStream = orderStream
.filter(order -> order.getAmount() > 0)
.map(new MapFunction<Order, Order>() {
@Override
public Order map(Order order) {
String desensitizeUserId = order.getUserId().substring(0, 5) + "****";
order.setUserId(desensitizeUserId);
return order;
}
});
// 4. 商品维表广播配置
MapStateDescriptor<Long, GoodsInfo> goodsStateDesc = new MapStateDescriptor<>(
"goods_broadcast_state",
Types.LONG,
Types.POJO(GoodsInfo.class)
);
DataStream<GoodsInfo> goodsStream = env.addSource(new GoodsSource());
BroadcastStream<GoodsInfo> broadcastGoodsStream = goodsStream.broadcast(goodsStateDesc);
// 5. 订单流关联广播维表,生成订单商品宽表
SingleOutputStreamOperator<OrderWithGoods> orderWideStream = cleanOrderStream.connect(broadcastGoodsStream)
.process(new BroadcastProcessFunction<Order, GoodsInfo, OrderWithGoods>() {
@Override
public void processElement(Order order, ReadOnlyContext ctx, Collector<OrderWithGoods> out) throws Exception {
GoodsInfo goods = ctx.getBroadcastState(goodsStateDesc).get(order.getGoodsId());
if (goods != null) {
OrderWithGoods wide = new OrderWithGoods();
wide.setOrderId(order.getOrderId());
wide.setUserId(order.getUserId());
wide.setGoodsId(order.getGoodsId());
wide.setGoodsName(goods.getGoodsName());
wide.setCategory(goods.getCategory());
wide.setAmount(order.getAmount());
wide.setStatus(order.getStatus());
wide.setOrderTime(order.getOrderTime());
out.collect(wide);
}
}
@Override
public void processBroadcastElement(GoodsInfo goods, Context ctx, Collector<OrderWithGoods> out) throws Exception {
ctx.getBroadcastState(goodsStateDesc).put(goods.getGoodsId(), goods);
}
});
// 6. 逐条打印实时订单明细
orderWideStream.map(order -> {
System.out.println("【单条订单明细】" + order);
return order;
});
// 7. 5秒事件时间滚动窗口,按类目聚合GMV
SingleOutputStreamOperator<OrderWindowResult> windowAggStream = orderWideStream
.keyBy(OrderWithGoods::getCategory)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
// 增量聚合 + 窗口函数组合(高性能+获取真实窗口时间)
.aggregate(new OrderAggFunc(), new OrderWindowFunc());
// 8. 自定义Sink控制台格式化打印
windowAggStream.addSink(new CustomConsoleSink());
// 9. 新增:Flink JdbcSink 实时写入MySQL(生产级入库)
String insertSql = "INSERT INTO category_gmv_stat (category, window_start, window_end, total_order_count, pay_order_count, total_gmv) VALUES (?,?,?,?,?) " +
"ON DUPLICATE KEY UPDATE total_order_count=VALUES(total_order_count),pay_order_count=VALUES(pay_order_count),total_gmv=VALUES(total_gmv)";
// 配置JDBC批量执行参数
JdbcExecutionOptions executionOptions = JdbcExecutionOptions.builder()
.withBatchSize(100) // 每100条批量写入
.withBatchIntervalMs(1000) // 最大1秒刷写一次
.withMaxRetries(3) // 失败重试3次
.build();
// 构建JdbcSink
SinkFunction<OrderWindowResult> mysqlSink = JdbcSink.sink(
insertSql,
(PreparedStatement statement, OrderWindowResult result) -> {
statement.setString(1, result.getCategory());
statement.setLong(2, result.getWindowStart());
statement.setLong(3, result.getWindowEnd());
statement.setLong(4, result.getTotalOrderCount());
statement.setLong(5, result.getPayOrderCount());
statement.setBigDecimal(6, new BigDecimal(result.getTotalGmv()).setScale(2, BigDecimal.ROUND_HALF_UP));
},
executionOptions,
new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
.withUrl(MYSQL_URL)
.withDriverName("com.mysql.cj.jdbc.Driver")
.withUsername(MYSQL_USER)
.withPassword(MYSQL_PASSWORD)
.build()
);
// 绑定MySQL入库Sink
windowAggStream.addSink(mysqlSink);
env.execute("电商实时类目GMV统计任务-MySQL入库版");
}
/**
* 增量聚合函数:轻量化累加计算
* 累加器:Tuple2<Tuple2<总订单数,总GMV>, 支付订单数>
*/
public static class OrderAggFunc implements AggregateFunction<OrderWithGoods, Tuple2<Tuple2<Long, Double>, Long>, Tuple2<Tuple2<Long, Double>, Long>> {
@Override
public Tuple2<Tuple2<Long, Double>, Long> createAccumulator() {
return Tuple2.of(Tuple2.of(0L, 0.0D), 0L);
}
@Override
public Tuple2<Tuple2<Long, Double>, Long> add(OrderWithGoods order, Tuple2<Tuple2<Long, Double>, Long> acc) {
acc.f0.f0 += 1;
acc.f0.f1 += order.getAmount();
if (order.getStatus() == 1) {
acc.f1 += 1;
}
return acc;
}
@Override
public Tuple2<Tuple2<Long, Double>, Long> getResult(Tuple2<Tuple2<Long, Double>, Long> acc) {
return acc;
}
@Override
public Tuple2<Tuple2<Long, Double>, Long> merge(Tuple2<Tuple2<Long, Double>, Long> acc1, Tuple2<Tuple2<Long, Double>, Long> acc2) {
return Tuple2.of(
Tuple2.of(acc1.f0.f0 + acc2.f0.f0, acc1.f0.f1 + acc2.f0.f1),
acc1.f1 + acc2.f1
);
}
}
/**
* 窗口函数:获取真实事件时间窗口,修复系统时间BUG
*/
public static class OrderWindowFunc implements WindowFunction<Tuple2<Tuple2<Long, Double>, Long>, OrderWindowResult, String, TimeWindow> {
@Override
public void apply(String category,
TimeWindow window,
Iterable<Tuple2<Tuple2<Long, Double>, Long>> input,
Collector<OrderWindowResult> out) throws Exception {
Tuple2<Tuple2<Long, Double>, Long> acc = input.iterator().next();
OrderWindowResult result = new OrderWindowResult();
result.setCategory(category);
// 核心:使用事件时间窗口原生时间,不依赖机器系统时间
result.setWindowStart(window.getStart());
result.setWindowEnd(window.getEnd());
result.setTotalOrderCount(acc.f0.f0);
result.setPayOrderCount(acc.f1);
result.setTotalGmv(acc.f0.f1);
out.collect(result);
}
}
/**
* 自定义控制台Sink:格式化输出窗口统计结果
*/
public static class CustomConsoleSink implements SinkFunction<OrderWindowResult> {
@Override
public void invoke(OrderWindowResult result, Context context) throws Exception {
System.out.println("\n====================【类目5秒GMV汇总】====================");
System.out.printf("商品类目:%s%n", result.getCategory());
System.out.printf("窗口时间:%d ~ %d%n", result.getWindowStart(), result.getWindowEnd());
System.out.printf("总下单订单数:%d 笔%n", result.getTotalOrderCount());
System.out.printf("成功支付订单:%d 笔%n", result.getPayOrderCount());
System.out.printf("区间累计GMV:%.2f 元%n", result.getTotalGmv());
System.out.println("【数据已实时写入MySQL数据库】");
System.out.println("=========================================================\n");
}
}
} |
所有评论(0)