Flink 结合 布隆过滤器(BloomFilter) 实现去重

本文开头附:Flink 学习路线系列 ^ _ ^

1.实时系统去重方案

  1. 使用Redis,将实时系统每条数据都去和 Redis 进行判断;
  2. 使用 HashSet,因为 HashSet 本身就是无序不重复的;

      如果我们使用以上两个方案,都是存在明显缺陷的。如果使用 Redis每次都需要通过网络连接 Redis 服务,这两个原因:1.网络速度明显比缓存速度慢 2.网络的不稳定性导致我们不能使用 Redis;如果使用 HashSet,虽然摆脱了网络的原因,但是如果我们将千万、亿级别的数据存入到 HashSet 时,HashSet 底层是通过 Hash 算法来实现的,如果数据越来越多,那么效率也就会大打则扣了

2.布隆过滤器

      BloomFilter,又叫布隆过滤器。它就类似于一个HashSet,用于快速判某个元素是否存在于集合中,其典型的应用场景就是能够快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于 Hash算法容器大小但是容器大小这个缺陷你就不用考虑了,因为它可以存放足够大足够大的数据,足以满足你的需求。

       你还记得吗?如果你有用到 Redis 的话,我们在解决 Redis 缓存击穿的问题上,也有用到 BloomFilter 布隆过滤器的。Redis使用布隆过滤器内容,请参考如下链接:Redis缓存穿透、缓存雪崩、redis并发问题分析

       还有一个方便的点,那就是 Flink 官方已经替我们考虑到了这一点,它已经为我们整合好了 BloomFilter 布隆过滤器,如下图所示:
在这里插入图片描述

3.Flink中布隆过滤器的使用

       接下来,我们就通过一个实例,来学习 Flink 是如何配合布隆过滤器,来实现去重操作的↓↓↓↓↓(使用 google 提供的 guava18 包中的布隆过滤器,Flink也已经帮我们整合好了)

3.1 场景

       在我们日常使用的各大购物APP,都会在618、双11 做很多撒红包的游戏来吸引眼球。本例以 JD 为例。比如:某用户在一天中凌晨玩了游戏A,上午也玩了游戏A和游戏B,中午又玩了游戏A,晚上玩了游戏B那么需求来了,我们来统计一下该用户参加了 JD 的几个游戏。

       数据量偏少,我们通过肉眼都能够分析。该用户一共有 5 次访问记录,共参加了 2 款游戏。

3.2 日志数据结构

       代表数据之间的分隔符(\001)。如下数据分别代表:IP、参与时间、用户ID、游戏链接。(以下数据均为模拟)

192.168.86.22020-02-24 05:23:381011504891http://192.168.xxx.xxx:8088/v5.3/gameA.html
......//其他玩游戏的用户的日志
192.168.86.22020-02-24 08:52:331011504891http://192.168.xxx.xxx:8088/v5.3/gameA.html
192.168.86.22020-02-24 09:00:291011504891http://192.168.xxx.xxx:8088/v5.3/gameB.html
192.168.86.22020-02-24 
12:08:161011504891http://192.168.xxx.xxx:8088/v5.3/gameA.html
......//其他玩游戏的用户的日志
192.168.86.22020-02-24 20:53:461011504891http://192.168.xxx.xxx:8088/v5.3/gameB.html

3.3 需求

  1. 日志数据来源于 Kafka;
  2. 使用日志产生时间EventTime作为依据来进行汇总;
  3. 统计一天内单个用户参加了 JD 的几个游戏。

3.4 返回数据结构

(用户ID,日期,时间,游戏链接,参加该游戏次数)

(1011504891,2020-02-24,05:23:38,http://192.168.xxx.xxx:8088/v5.3/gameA.html,1)

3.5 代码

1.FlinkUtils 工具类
/**
 * TODO FlinkUtils工具类
 *
 * @author liuzebiao
 * @Date 2020-2-18 9:11
 */
public class FlinkUtils {

    private static StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

    /**
     * 返回 Flink流式环境
     * @return
     */
    public static StreamExecutionEnvironment getEnv(){
        return env;
    }

    /**
     * Flink 从 Kafka 中读取数据(满足Exactly-Once)
     * @param parameters
     * @param clazz
     * @param 
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    public static <T> DataStream<T> createKafkaStream(ParameterTool parameters,String topics,String groupId, Class<? extends DeserializationSchema> clazz) throws IllegalAccessException, InstantiationException {

        //设置全局参数
        env.getConfig().setGlobalJobParameters(parameters);
        //1.只有开启了CheckPointing,才会有重启策略
        //设置Checkpoint模式(与Kafka整合,一定要设置Checkpoint模式为Exactly_Once)
        env.enableCheckpointing(parameters.getLong("checkpoint.interval",5000L),CheckpointingMode.EXACTLY_ONCE);
        //2.默认的重启策略是:固定延迟无限重启
        //此处设置重启策略为:出现异常重启3次,隔5秒一次(你也可以在flink-conf.yaml配置文件中写死。此处配置会覆盖配置文件中的)
        env.getConfig().setRestartStrategy(RestartStrategies.fixedDelayRestart(10, Time.seconds(20)));
        //系统异常退出或人为 Cancel 掉,不删除checkpoint数据
        env.getCheckpointConfig().enableExternalizedCheckpoints(CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);

        /**2.Source:读取 Kafka 中的消息**/
        //Kafka props
        Properties properties = new Properties();
        //指定Kafka的Broker地址
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, parameters.getRequired("bootstrap.server"));
        //指定组ID
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        //如果没有记录偏移量,第一次从最开始消费
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, parameters.get("auto.offset.reset","earliest"));
        //Kafka的消费者,不自动提交偏移量
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, parameters.get("enable.auto.commit","false"));

        List<String> topicList = Arrays.asList(topics.split(","));

        FlinkKafkaConsumer<T> kafkaConsumer = new FlinkKafkaConsumer(topicList, clazz.newInstance(), properties);

        return env.addSource(kafkaConsumer);
    }
}
2.配置文件config.properties
bootstrap.server=192.168.204.210:9092,192.168.204.211:9092,192.168.204.212:9092
auto.offset.reset=earliest
enable.auto.commit=false
3.任务代码
/**
 * TODO 用户参与游戏数分析
 *
 * @author liuzebiao
 * @Date 2020-2-18 16:18
 */
public class UserJoinGameAnalysis {

    public static void main(String[] args) throws Exception{

        StreamExecutionEnvironment env = FlinkUtils.getEnv();

        //设置EventTime作为时间标准
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        ParameterTool parameters = ParameterTool.fromPropertiesFile("配置文件路径");

        DataStream<String> kafkaStream = FlinkUtils.createKafkaStream(parameters,"h5_log","groupA", SimpleStringSchema.class);

        SingleOutputStreamOperator<String> streamOperator = kafkaStream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<String>(Time.seconds(0)) {
            @Override
            public long extractTimestamp(String log) {
                String[] split = log.split("\001");
                String time = split[1];
                //截取时间(转换为时间戳)
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                LocalDateTime parse = LocalDateTime.parse(time, formatter);
                return LocalDateTime.from(parse).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
            }
        });

        //Transformation
        SingleOutputStreamOperator<Tuple4<String, String, String, String>> tuple4Operator = streamOperator.flatMap((String log, Collector<Tuple4<String, String, String, String>> out) -> {
            String[] logSplit = log.split("\001");
            //访问时间
            String dateTime = logSplit[1];

            String[] dateTimeSplit = dateTime.split(" ");

            //date
            String date = dateTimeSplit[0];
            //time
            String time = dateTimeSplit[1];

            //用户ID(Session)
            String userId = logSplit[2];
            //游戏链接
            String url = logSplit[3];

            out.collect(Tuple4.of(userId, date, time, url));
        }).returns(Types.TUPLE(Types.STRING, Types.STRING, Types.STRING, Types.STRING));

        KeyedStream<Tuple4<String, String, String, String>, Tuple> keyedStream = tuple4Operator.keyBy(0, 1);

        /**************************分组后,使用布隆过滤器开始去重(start)***********************/
        SingleOutputStreamOperator<Tuple5<String, String, String, String, Long>> distinctStream = keyedStream.map(new RichMapFunction<Tuple4<String, String, String, String>, Tuple5<String, String, String, String, Long>>() {

            //使用 KeyedState(用于任务失败重启后,从State中恢复数据)
            //1.记录游戏的State
            private transient ValueState<BloomFilter> productState;
            //2.记录次数的State(因为布隆过滤器不会计算它里面到底存了多少数据,所以此处我们创建一个 countState 来计算次数)
            private transient ValueState<Long> countState;

            @Override
            public void open(Configuration parameters) throws Exception {
                //定义一个状态描述器[BloomFilter]
                ValueStateDescriptor<BloomFilter> stateDescriptor = new ValueStateDescriptor<BloomFilter>(
                        "product-state",
                        BloomFilter.class
                );
                //使用RuntimeContext获取状态
                productState = getRuntimeContext().getState(stateDescriptor);

                //定义一个状态描述器[次数]
                ValueStateDescriptor<Long> countDescriptor = new ValueStateDescriptor<Long>(
                        "count-state",
                        Long.class
                );
                //使用RuntimeContext获取状态
                countState = getRuntimeContext().getState(countDescriptor);
            }

            @Override
            public Tuple5<String, String, String, String, Long> map(Tuple4<String, String, String, String> tuple) throws Exception {
                //获取点击链接
                String url = tuple.f3;
                BloomFilter bloomFilter = productState.value();
                if (bloomFilter == null) {
                    //初始化一个bloomFilter
                    bloomFilter = BloomFilter.create(Funnels.unencodedCharsFunnel(), 100000);
                    countState.update(0L);
                }
                //BloomFilter 可以判断一定不包含
                if (!bloomFilter.mightContain(url)) {
                    //将当前url加入到bloomFilter
                    bloomFilter.put(url);
                    countState.update(countState.value() + 1);
                }

                //更新 productState
                productState.update(bloomFilter);
                return Tuple5.of(tuple.f0, tuple.f1, tuple.f2, tuple.f3, countState.value());
            }
        });

        /**************************分组后,使用布隆过滤器开始去重(end)***********************/
        
        distinctStream.print("distinctStream");

        env.execute("UserJoinGameAnalysis");

    }
}
4.任务返回结果
distinctStream:6> (1011504891,2020-02-24,05:23:38,http://192.168.xxx.xxx:8088/v5.3/gameA.html,1)
distinctStream:6> (1011504891,2020-02-24,08:52:33,http://192.168.xxx.xxx:8088/v5.3/gameA.html,1)
distinctStream:6> (1011504891,2020-02-24,09:00:29,http://192.168.xxx.xxx:8088/v5.3/gameB.html,2)
distinctStream:6> (1011504891,2020-02-24,12:08:16,http://192.168.xxx.xxx:8088/v5.3/gameA.html,2)
distinctStream:6> (1011504891,2020-02-24,20:53:46,http://192.168.xxx.xxx:8088/v5.3/gameB.html,2)

3.6 结果分析

       通过任务执行结果发现,结果已经对该用户的 5 条访问日志进行去重,最终返回该用户在 2020-02-24这一天共玩了 A 和 B 两个游戏。


博主写作不易,来个关注呗

求关注、求点赞,加个关注不迷路 ヾ(◍°∇°◍)ノ゙

博主不能保证写的所有知识点都正确,但是能保证纯手敲,错误也请指出,望轻喷 Thanks♪(・ω・)ノ

你可能感兴趣的:(Flink)