本文开头附:Flink 学习路线系列 ^ _ ^
如果我们使用以上两个方案,都是存在明显缺陷的。如果使用 Redis,每次都需要通过网络连接 Redis 服务
,这两个原因:1.网络速度明显比缓存速度慢 2.网络的不稳定性
导致我们不能使用 Redis;如果使用 HashSet,虽然摆脱了网络的原因,但是如果我们将千万、亿级别的数据存入到 HashSet 时,HashSet 底层是通过 Hash 算法来实现的,如果数据越来越多,那么效率也就会大打则扣了
。
BloomFilter,又叫布隆过滤器。它就类似于一个HashSet,用于快速判某个元素是否存在于集合中,其典型的应用场景就是能够快速判断一个key是否存在于某容器
,不存在就直接返回。布隆过滤器的关键就在于 Hash算法
和 容器大小
,但是容器大小这个缺陷你就不用考虑了,因为它可以存放足够大足够大的数据,足以满足你的需求。
你还记得吗?如果你有用到 Redis 的话,我们在解决 Redis 缓存击穿的问题上,也有用到 BloomFilter 布隆过滤器的
。Redis使用布隆过滤器内容,请参考如下链接:Redis缓存穿透、缓存雪崩、redis并发问题分析
还有一个方便的点,那就是 Flink 官方已经替我们考虑到了这一点,它已经为我们整合好了 BloomFilter 布隆过滤器,如下图所示:
接下来,我们就通过一个实例,来学习 Flink 是如何配合布隆过滤器,来实现去重操作的↓↓↓↓↓
(使用 google 提供的 guava18 包中的布隆过滤器,Flink也已经帮我们整合好了)
在我们日常使用的各大购物APP,都会在618、双11 做很多撒红包的游戏来吸引眼球。本例以 JD 为例。比如:某用户在一天中凌晨玩了游戏A,上午也玩了游戏A和游戏B,中午又玩了游戏A,晚上玩了游戏B
。那么需求来了,我们来统计一下该用户参加了 JD 的几个游戏。
数据量偏少,我们通过肉眼都能够分析。该用户一共有 5 次访问记录,共参加了 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
EventTime
作为依据来进行汇总;(用户ID,日期,时间,游戏链接,参加该游戏次数)
(1011504891,2020-02-24,05:23:38,http://192.168.xxx.xxx:8088/v5.3/gameA.html,1)
/**
* 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);
}
}
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
/**
* 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");
}
}
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)
通过任务执行结果发现,结果已经对该用户的 5 条访问日志进行去重,最终返回该用户在 2020-02-24
这一天共玩了 A 和 B 两个游戏。
博主写作不易,来个关注呗
求关注、求点赞,加个关注不迷路 ヾ(◍°∇°◍)ノ゙
博主不能保证写的所有知识点都正确,但是能保证纯手敲,错误也请指出,望轻喷 Thanks♪(・ω・)ノ