Flink实时数仓项目—DWD层设计与实现
前言
前面完成了三个功能,最后一个功能是支付宽表,与订单宽表有类似的地方。
一、功能四:支付宽表
1.需求描述
业务数据库中的支付表的粒度是一整条订单,但是这里的需求中有计算某商品的支付情况,因此需要将支付表和订单宽表进行关联,方便后续对某商品的支付情况的统计。
2.实现思路
2.1 思路一
因为在功能三中完成了订单宽表的整合,所以可以选择将订单宽表输出到HBase上,把订单宽表在这里作为一个维度表进行管理,在支付宽表时去查询HBase。
2.2 思路二
第二种方案是以流的方式接受订单宽表,然后进行双流Join进行合并。通常情况下,一个订单的支付时间为15分钟之内,所以这里最好还是使用间隔连结,保证能够跟一个区间内的数据进行匹配。
2.3 思路选择
选择思路二,原因如下:
1)因为本身订单宽表就要输出到Kafka中,又要往HBase里写一份,增加了负担。
2)对于支付表来说,它只需要它15分钟内的订单宽表即可,而HBase中却是保存的所有的数据。
3)使用间隔连结更好写代码。
3.代码实现
3.1 创建支付实体类
创建支付实体类是为了将从Kafka读取出来的支付数据转化为JavaBean对象,然后好进行赋值,代码如下:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PaymentInfo {
//支付表id
Long id;
//订单表id
Long order_id;
//用户表id
Long user_id;
//该订单支付总价格
BigDecimal total_amount;
String subject;
//该订单支付的方式
String payment_type;
//支付的时间
String create_time;
//支付成功时间
String callback_time;
}
3.2 创建支付宽表实体类
支付宽表实体类是我们最终需要的结果,它里面的字段由支付表的字段加上订单宽表的字段组成。同时,需要由一个构造方法,在接受了支付实体和订单宽表实体后能对字段进行赋值,代码如下:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PaymentWide {
//从这里开始——支付表里的字段
Long payment_id;
String subject;
String payment_type;
String payment_create_time;
String callback_time;
//总这里开始——订单宽表的字段
Long detail_id;
Long order_id;
Long sku_id;
BigDecimal order_price;
Long sku_num;
String sku_name;
Long province_id;
String order_status;
Long user_id;
BigDecimal total_amount;
BigDecimal activity_reduce_amount;
BigDecimal coupon_reduce_amount;
BigDecimal original_total_amount;
BigDecimal feight_fee;
BigDecimal split_feight_fee;
BigDecimal split_activity_amount;
BigDecimal split_coupon_amount;
BigDecimal split_total_amount;
String order_create_time;
String province_name;
//查询维表得到
String province_area_code;
String province_iso_code;
String province_3166_2_code;
Integer user_age;
//用户信息
String user_gender;
Long spu_id;
//作为维度数据 要关联进来
Long tm_id;
Long category3_id;
String spu_name;
String tm_name;
String category3_name;
public PaymentWide(PaymentInfo paymentInfo, OrderWide orderWide) {
mergeOrderWide(orderWide);
mergePaymentInfo(paymentInfo);
}
//将传进来的支付表的字段的值进行赋值
public void mergePaymentInfo(PaymentInfo paymentInfo) {
if (paymentInfo != null) {
BeanUtils.copyProperties(this, paymentInfo);
payment_create_time = paymentInfo.create_time;
payment_id = paymentInfo.id;
}
}
//将传进来的订单宽表的字段的值进行赋值
public void mergeOrderWide(OrderWide orderWide) {
if (orderWide != null) {
BeanUtils.copyProperties(this, orderWide);
order_create_time = orderWide.create_time;
}
}
}
3.3 主程序
主程序步骤很明确:
1)从Kafka相应主体读取支付表和订单宽表的数据
2)将读取到的数据转化为JavaBean对象,然后提取时间戳生成水位线
3)将两个流进行间隔连结,将匹配的数据生成对应的支付宽表并返回
4)将生成的支付宽表数据写入到Kafka对应主题中
代码如下:
public class PaymentWideApp {
public static void main(String[] args) throws Exception {
//1、获取流式执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//设置检查点和状态后端
env.setStateBackend(new HashMapStateBackend());
env.getCheckpointConfig().setCheckpointStorage(new FileSystemCheckpointStorage("hdfs://hadoop102:8020/gmall-flink-20220410/ck"));
env.enableCheckpointing(5000L);
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setCheckpointTimeout(10000L);
env.getCheckpointConfig().setMaxConcurrentCheckpoints(2);
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(3000L);
//2、读取Kafka中订单宽表的数据
String groupId = "payment_wide_group";
String paymentInfoSourceTopic = "dwd_payment_info";
String orderWideSourceTopic = "dwm_order_wide";
String paymentWideSinkTopic = "dwm_payment_wide";
//读取dwd层支付事实表的数据
DataStreamSource<String> paymentKafkaDS = env.addSource(MyKafkaUtil.getKafkaSource(paymentInfoSourceTopic, groupId));
//读取dwm层订单宽表的数据
DataStreamSource<String> orderWideKafkaDS = env.addSource(MyKafkaUtil.getKafkaSource(orderWideSourceTopic, groupId));
//3、将String类型数据转化为JavaBean对象类型,然后提取时间戳生成水位线
SingleOutputStreamOperator<PaymentInfo> paymentInfoDS = paymentKafkaDS.map(data -> JSON.parseObject(data, PaymentInfo.class))
.assignTimestampsAndWatermarks(WatermarkStrategy.<PaymentInfo>forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner<PaymentInfo>() {
@SneakyThrows
@Override
public long extractTimestamp(PaymentInfo paymentInfo, long l) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.parse(paymentInfo.getCreate_time()).getTime();
}
}));
SingleOutputStreamOperator<OrderWide> orderWideDS = orderWideKafkaDS.map(data -> JSON.parseObject(data, OrderWide.class))
.assignTimestampsAndWatermarks(WatermarkStrategy.<OrderWide>forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner<OrderWide>() {
@SneakyThrows
@Override
public long extractTimestamp(OrderWide orderWide, long l) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.parse(orderWide.getCreate_time()).getTime();
}
}));
//4、按order_id分组,然后进行间隔连结
SingleOutputStreamOperator<PaymentWide> paymentWideDS = paymentInfoDS.keyBy(data -> data.getOrder_id())
.intervalJoin(orderWideDS.keyBy(data -> data.getOrder_id()))
.between(Time.minutes(-15), Time.seconds(5))
.process(new ProcessJoinFunction<PaymentInfo, OrderWide, PaymentWide>() {
@Override
public void processElement(PaymentInfo paymentInfo, OrderWide orderWide, ProcessJoinFunction<PaymentInfo, OrderWide, PaymentWide>.Context context, Collector<PaymentWide> collector) throws Exception {
collector.collect(new PaymentWide(paymentInfo, orderWide));
}
});
//5、关联完毕,将数据写入Kafka
paymentWideDS.map(data->JSON.toJSONString(data))
.addSink(MyKafkaUtil.getKafkaSink(paymentWideSinkTopic));
//6、启动任务
env.execute();
}
}