Flink热门电影实时统计(模仿双十一实时大屏统计)

本实验根据累计观影人数,判断电影的热门程度

实验要求:

  1. 实时计算出三天内截止到当前时间的观影人数
  2. 计算出电影观影人数的Top3,并输出对应电影
  3. 每秒钟更新一次统计结果

附加:实时计算出电影的平均评分,如果电影人数相同,则输出分数高的

实验过程:

  1. 数据源会源源不断地产生数据流,根据电影ID,对电影的观影人数进行累加,并对电影评分求平均。
  2. 每秒钟收集一次窗口结果数据,将聚合结果打包成对象,该对象中包含了打包时间,使用这个打包时间代替事件发生时间
  3. 按照时间分组,每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("-------------");

        }
    }

}

实验结果:

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
使用Scala的Flink和Kafka实时来一条统计PV的方法如下: 首先,我们需要创建一个Flink的流处理任务。在任务中,我们可以使用Flink提供的Kafka Consumer来消费Kafka中的消息流,并使用Flink的处理函数对消息进行处理。 在处理函数中,我们可以将消费到的每条消息的PV字段进行累加。假设每条消息中包含一个PV字段(表示Page Views,即页面访问量),我们可以定义一个累加器,并使用Flink的MapState来保存当前的PV值。 下面是一个简单的示例代码: ``` import org.apache.flink.api.common.functions.MapFunction import org.apache.flink.api.common.state.MapStateDescriptor import org.apache.flink.api.common.typeinfo.{TypeHint, TypeInformation} import org.apache.flink.streaming.api.scala._ import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer object PVStatistics { def main(args: Array[String]): Unit = { val env = StreamExecutionEnvironment.getExecutionEnvironment val kafkaProps = new Properties() kafkaProps.setProperty("bootstrap.servers", "localhost:9092") kafkaProps.setProperty("group.id", "pv-consumer-group") val inputTopic = "pv-topic" val kafkaConsumer = new FlinkKafkaConsumer[String](inputTopic, new SimpleStringSchema(), kafkaProps) val stream = env.addSource(kafkaConsumer) // 定义累加器和MapStateDescriptor val pvAccumulator = new IntCounter val pvStateDescriptor = new MapStateDescriptor[String, Int]("pv-state", TypeInformation.of(new TypeHint[String]{}), TypeInformation.of(new TypeHint[Int]{})) val pvStream = stream.map(new MapFunction[String, Int] { override def map(value: String): Int = { pvAccumulator.add(1) pvAccumulator.getLocalValue } }).keyBy(_ => "pv-key") .mapWithState[(String, Int), MapState[String, Int]] { // 更新PV值并返回累加结果 case (value, state: MapState[String, Int]) => val pv = state.get("pv") val newPv = pv + value state.put("pv", newPv) ((inputTopic, newPv), state) } pvStream.print() env.execute("PV Statistics") } } ``` 在上述代码中,我们定义了一个`pvAccumulator`作为累加器,并通过`pvStateDescriptor`创建了一个MapState来保存每个topic的PV值。 然后,我们使用`FlinkKafkaConsumer`创建了一个Kafka Consumer,并从指定的topic `pv-topic`中消费消息流。接着,我们使用`map`函数将每一条消息的PV字段累加到累加器中,并将累加结果输出为`(topic, pv)`的元组形式。 最后,我们使用`execute`方法执行Flink任务,即开始实时统计PV。 以上是使用Scala的Flink和Kafka实时统计PV的一个简单示例。实际情况中,你可能需要根据具体需求进行更详细的配置和调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值