本实验根据累计观影人数,判断电影的热门程度
实验要求:
- 实时计算出三天内截止到当前时间的观影人数
- 计算出电影观影人数的Top3,并输出对应电影
- 每秒钟更新一次统计结果
附加:实时计算出电影的平均评分,如果电影人数相同,则输出分数高的
实验过程:
- 数据源会源源不断地产生数据流,根据电影ID,对电影的观影人数进行累加,并对电影评分求平均。
- 每秒钟收集一次窗口结果数据,将聚合结果打包成对象,该对象中包含了打包时间,使用这个打包时间代替事件发生时间
- 按照时间分组,每1s更新截至当前时间的累计总人数和平均分,并给出top3的人们电影(按照观看人数排序)。
实验代码:
package com.nhbd.flink.homework;
import com.nhbd.flink.ch4.action.DoubleElevenBigScreem;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple;
import org.apache.flink.api.java.tuple.Tuple1;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.core.fs.FileSystem;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.triggers.ContinuousProcessingTimeTrigger;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
public class Top3{
public static Filmpaser paser = new Filmpaser();
public static void main(String[] args) throws Exception{
//编码步骤:
//1.env
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);//学习测试方便观察
//2.source
//模拟实时订单信息
DataStreamSource<UserMovie> sourceDS = env.addSource(new MySource());
DataStream<Tuple2<Integer,Tuple2<Integer,Float> >> wordAndOnesDS = sourceDS.map(new MapFunction<UserMovie, Tuple2<Integer,Tuple2<Integer,Float>>>() {
@Override
public Tuple2<Integer,Tuple2<Integer,Float> > map(UserMovie value) throws Exception {
//value就是进来一个个的单词
return Tuple2.of(value.movieid,Tuple2.of(1,value.rating));
}
});
SingleOutputStreamOperator< CategoryPojo> tempAggResult = wordAndOnesDS.keyBy(t->t.f0)
//3.1定义大小为一天的窗口,第二个参数表示中国使用的UTC+08:00时区比UTC时间早
/*
of(Time 窗口大小, Time 带时间校准的从哪开始)源码中有解释:
如果您居住在不使用UTC±00:00时间的地方,例如使用UTC + 08:00的中国,并且您需要一个大小为一天的时间窗口,
并且窗口从当地时间的每00:00:00开始,您可以使用of(Time.days(1),Time.hours(-8))
注意:该代码如果在11月11日运行就会从11月11日00:00:00开始记录直到11月11日23:59:59的1天的数据
注意:我们这里简化了没有把之前的Watermaker那些代码拿过来,所以直接ProcessingTime
*/
.window(TumblingProcessingTimeWindows.of(Time.days(3), Time.hours(-8)))//仅仅只定义了一个窗口大小
//3.2定义一个1s的触发器
.trigger(ContinuousProcessingTimeTrigger.of(Time.seconds(1)))
.aggregate(new PriceAggregate(), new WindowResult());
//3.4看一下初步聚合的结果
tempAggResult.print("初步聚合结果");
/*
注意:需求如下:
-1.实时计算出11月11日00:00:00零点开始截止到当前时间的销售总额
-2.计算出各个分类的销售额top3
-3.每1秒钟更新一次统计结果
*/
//4.使用上面初步聚合的结果,实现业务需求,并sink
tempAggResult.keyBy("dateTime")//按照时间分组是因为需要每1s更新截至到当前时间的销售总额
//每秒钟更新一次统计结果
//Time size 为1s,表示计算最近1s的数据
.window(TumblingProcessingTimeWindows.of(Time.seconds(1)))
//在ProcessWindowFunction中实现该复杂业务逻辑,一次性将需求1和2搞定
//-1.实时计算出11月11日00:00:00零点开始截止到当前时间的销售总额
//-2.计算出各个分类的销售额top3
//-3.每1秒钟更新一次统计结果
.process(new WindowResultProcess()).writeAsText("/home/hadoop/Documents/top3", FileSystem.WriteMode.OVERWRITE).setParallelism(2);
;
//5.execute
env.execute();
}
/**
* 自定义数据源实时产生订单数据Tuple2<分类, 金额>
*/
@Data//lombok可以直接解析注解,这里就实现了无参构造和所有参数的构造
@NoArgsConstructor
@AllArgsConstructor
public static class UserMovie {
private int userid;
private int movieid;
private String title;
private float rating;
private Long createTime;
}
public static class MySource implements SourceFunction<UserMovie>{
private boolean flag = true;
private Random random = new Random();
@Override
public void run(SourceContext<UserMovie> ctx) throws Exception {
while (flag){
int userid =random.nextInt(5);
int movieid=random.nextInt(20);
String[] str = paser.mmap.get(movieid);
String title = str[0];
Float rate = random.nextFloat() * 10;
long createTime = System.currentTimeMillis();
ctx.collect(new UserMovie(userid,movieid,title,rate,createTime));
Thread.sleep(20);
}
}
@Override
public void cancel() {
flag = false;
}
}
private static class PriceAggregate implements AggregateFunction<Tuple2<Integer,Tuple2<Integer,Float>>, Tuple2<Integer,Float>, Tuple2<Integer,Float>> {
//初始化累加器为0
@Override
public Tuple2<Integer,Float> createAccumulator() {
return Tuple2.of(0,0F) ; //D表示Double,L表示long
}
//把price往累加器上累加
@Override
public Tuple2<Integer,Float> add(Tuple2<Integer,Tuple2<Integer,Float>> value, Tuple2<Integer,Float> accumulator) {
return Tuple2.of(value.f1.f0 + accumulator.f0,(value.f1.f1+accumulator.f1));
}
//获取累加结果
@Override
public Tuple2<Integer,Float> getResult(Tuple2<Integer,Float> accumulator) {
return Tuple2.of(accumulator.f0,accumulator.f1/accumulator.f0);
}
@Override
public Tuple2<Integer, Float> merge(Tuple2<Integer, Float> integerDoubleTuple2, Tuple2<Integer, Float> acc1) {
return Tuple2.of(integerDoubleTuple2.f0+acc1.f0,(integerDoubleTuple2.f1+acc1.f1));
}
}
/**
* 用于存储聚合的结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class CategoryPojo {
private int movieid;//电影名称
private float rating;//电影平均评分
private int num;//观影数量
private String dateTime;// 截止到当前时间的时间,本来应该是EventTime,但是我们这里简化了直接用当前系统时间即可
}
/**
* 自定义WindowFunction,实现如何收集窗口结果数据
* interface WindowFunction<IN, OUT, KEY, W extends Window>
* interface WindowFunction<Double, CategoryPojo, Tuple的真实类型就是String就是分类, W extends Window>
*/
private static class WindowResult implements WindowFunction<Tuple2<Integer, Float>, CategoryPojo, Integer, TimeWindow> {
//定义一个时间格式化工具用来将当前时间(双十一那天订单的时间)转为String格式
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void apply(Integer tuple, TimeWindow window, Iterable<Tuple2<Integer,Float>> input, Collector<CategoryPojo> out) throws Exception {
int category = tuple;
int num = input.iterator().next().f0;
Float rating= input.iterator().next().f1;
//为了后面项目铺垫,使用一下用Bigdecimal来表示精确的小数
BigDecimal bigDecimal = new BigDecimal(rating);
//setScale设置精度保留2位小数,
double roundPrice = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();//四舍五入
long currentTimeMillis = System.currentTimeMillis();
String dateTime = df.format(currentTimeMillis);
CategoryPojo categoryPojo = new CategoryPojo(category, (float) roundPrice,num, dateTime);
out.collect(categoryPojo);
}
}
/**
* 实现ProcessWindowFunction
* abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window>
* abstract class ProcessWindowFunction<CategoryPojo, Object, Tuple就是String类型的dateTime, TimeWindow extends Window>
*
* 把各个分类的总价加起来,就是全站的总销量金额,
* 然后我们同时使用优先级队列计算出分类销售的Top3,
* 最后打印出结果,在实际中我们可以把这个结果数据存储到hbase或者redis中,以供前端的实时页面展示。
*/
private static class WindowResultProcess extends ProcessWindowFunction<CategoryPojo, Object, Tuple, TimeWindow> {
@Override
public void process(Tuple tuple, Context context, Iterable<CategoryPojo> elements, Collector<Object> out) throws Exception {
String dateTime = ((Tuple1<String>)tuple).f0;
//Java中的大小顶堆可以使用优先级队列来实现
//https://blog.csdn.net/hefenglian/article/details/81807527
//注意:
// 小顶堆用来计算:最大的topN
// 大顶堆用来计算:最小的topN
Queue<CategoryPojo> queue = new PriorityQueue<>(3,//初始容量
//正常的排序,就是小的在前,大的在后,也就是c1>c2的时候返回1,也就是小顶堆
(c1, c2) -> (c1.getNum() >= c2.getNum()) ? 1 : -1);
//在这里我们要完成需求:
// * -1.实时计算出11月11日00:00:00零点开始截止到当前时间的销售总额,其实就是把之前的初步聚合的price再累加!
Float totalPrice = 0F;
Double roundPrice = 0D;
int totalnum=0;
Iterator<CategoryPojo> iterator = elements.iterator();
for (CategoryPojo element : elements) {
Float rating = element.rating;//某个分类的总销售额
int num=element.num;
totalPrice += rating;
totalnum+=num;
BigDecimal bigDecimal = new BigDecimal(totalPrice);
roundPrice = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();//四舍五入
// * -2.计算出各个分类的销售额top3,其实就是对各个分类的price进行排序取前3
//注意:我们只需要top3,也就是只关注最大的前3个的顺序,剩下不管!所以不要使用全局排序,只需要做最大的前3的局部排序即可
//那么可以使用小顶堆,把小的放顶上
// c:80
// b:90
// a:100
//那么来了一个数,和最顶上的比,如d,
//if(d>顶上),把顶上的去掉,把d放上去,再和b,a比较并排序,保证顶上是最小的
//if(d<=顶上),不用变
if (queue.size() < 3) {//小顶堆size<3,说明数不够,直接放入
queue.add(element);
}else{//小顶堆size=3,说明,小顶堆满了,进来一个需要比较
//"取出"顶上的(不是移除)
CategoryPojo top = queue.peek();
if(element.getNum() >top.getNum()){
//queue.remove(top);//移除指定的元素
queue.poll();//移除顶上的元素
queue.add(element);
}else if(element.getNum()==top.getNum()){
if(element.rating>top.rating){
queue.poll();//移除顶上的元素
queue.add(element);
}
}
}
}
// * -3.每1秒钟更新一次统计结果,可以直接打印/sink,也可以收集完结果返回后再打印,
// 但是我们这里一次性处理了需求1和2的两种结果,不好返回,所以直接输出!
//对queue中的数据逆序
//各个分类的销售额top3
List<String> top3Result = queue.stream()
.sorted((c1, c2) -> (c1.getNum() > c2.getNum()) ? -1 : 1)//逆序
.map(c -> "(热门电影:" +"ID:" +c.getMovieid()+ " title:"+paser.mmap.get(c.getMovieid())[0] + " 电影评分:" + c.getRating() + " 观影人数:"+c.getNum()+")")
.collect(Collectors.toList());
System.out.println("时间 : " + dateTime + " top3:\n" + StringUtils.join(top3Result, ",\n"));
System.out.println("-------------");
}
}
}