flink实践-电商用户行为数据分析-第2章、实时热门商品统计

首先要实现的是实时热门商品统计,我们将会基于 UserBehavior 数据集来进行分析。 项目主体用 Java 编写,采

用 IDEA 作为开发环境进行项目编写,采用 maven 作为项目构建和管理工具。首先我们需要搭建项目框架。

1、创建 Maven 项目

1.1 项目框架搭建

打开 IDEA,创建一个 maven 项目,命名为 UserBehaviorAnalysis。由于包含了多个模块,我们可以以

UserBehaviorAnalysis 作为父项目,并在其下建一个名为HotItemsAnalysis 的子项目,用于实时统计热门 top N

商品。

在 UserBehaviorAnalysis 下 新 建 一 个 maven module 作 为 子 项 目 , 命 名 为 HotItemsAnalysis。

父项目只是为了规范化项目结构,方便依赖管理,本身是不需要代码实现的, 所以 UserBehaviorAnalysis 下的

src 文件夹可以删掉。

1.2 声明项目中工具的版本信息

我们整个项目需要的工具的不同版本可能会对程序运行造成影响,所以应该在最外层的 UserBehaviorAnalysis 中

声明所有子模块共用的版本信息。

pom.xml 中加入以下配置:

UserBehaviorAnalysis/pom.xml

<properties>
    <flink.version>1.10.1</flink.version> 
    <scala.binary.version>2.12</scala.binary.version>
    <kafka.version>2.2.0</kafka.version>
</properties>

1.3 添加项目依赖

对 于 整 个 项 目 而 言 , 所 有 模 块 都 会 用 到 flink 相 关 的 组 件 , 所 以 我们在 UserBehaviorAnalysis 中引

入公有依赖:

UserBehaviorAnalysis/pom.xml

<dependencies>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-java</artifactId>
        <version>${flink.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka_${scala.binary.version}</artifactId>
        <version>${kafka.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-kafka_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>
</dependencies>
<build>
    <plugins>
        <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>

在 HotItemsAnalysis 子模块中,我们并没有引入更多的依赖,所以不需要改动pom 文件。

1.4 数据准备

将数据文件 UserBehavior.csv 复制到资源文件目录 src/main/resources 下,我们 将从这里读取数据。 至此,我们的准备工作都已完成,接下来可以写代码了。

2、模块代码实现

我们将实现一个“实时热门商品”的需求,可以将“实时热门商品”翻译成程序员更好理解的需求:每隔 5 分钟输

出最近一小时内点击量最多的前 N 个商品。将这个需求进行分解我们大概要做这么几件事情:

• 抽取出业务时间戳,告诉 Flink 框架基于业务时间做窗口

• 过滤出点击行为数据

• 按一小时的窗口大小,每 5 分钟统计一次,做滑动窗口聚合(Sliding Window)

• 按每个窗口聚合,输出每个窗口中点击量前 N 名的商品

2.1 程序主体

在 src/main/java/beans 下定义 POJOs:UserBehavior 和 ItemViewCount。创建类HotItems,在 main 方

法中创建 StreamExecutionEnvironment 并做配 置,然后从 UserBehavior.csv 文件中读取数据,并包装成

UserBehavior 类型。代码如下:

HotItemsAnalysis/src/main/java/HotItems.java

public class HotItems {
    public static void main(String[] args) throws Exception {
        // 1. 创建环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        // 2. 读取数据,创建 DataStream
        DataStream<String> inputStream = env.readTextFile("..\\UserBehavior.csv");
        // 3. 转换为 POJO,并分配时间戳和 watermark
        DataStream<UserBehavior> dataStream = inputStream.map(line -> {
            String[] fields = line.split(",");
            return new UserBehavior(new Long(fields[0]), new Long(fields[1]), new Integer(fields[2]), fields[3],
                    new Long(fields[4]));
        }).assignTimestampsAndWatermarks(new AscendingTimestampExtractor<UserBehavior>() {
            @Override
            public long extractAscendingTimestamp(UserBehavior element) {
                return element.getTimestamp() * 1000L;
            }
        });
        env.execute("hot items");
    }
}

这里注意,我们需要统计业务时间上的每小时的点击量,所以要基于 EventTime 来处理。那么如果让 Flink

按照我们想要的业务时间来处理呢?这里主要有两件事情要做。

第一件是告诉 Flink 我们现在按照 EventTime 模式进行处理,Flink 默认使用ProcessingTime 处理,所以我们要

显式设置如下:

env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

第二件事情是指定如何获得业务时间,以及生成 Watermark。Watermark 是用来追踪业务事件的概念,可以理解

EventTime 世界中的时钟,用来指示当前处理到什么时刻的数据了。由于我们的数据源的数据已经经过整理,

没有乱序,即事件的时间戳是单调递增的,所以可以将每条数据的业务时间就当做 Watermark。这里我们用

AscendingTimestampExtractor 来实现时间戳的抽取和 Watermark 的生成。

.assignTimestampsAndWatermarks(new AscendingTimestampExtractor<UserBehavior>() {
    @Override
    public long extractAscendingTimestamp(UserBehavior element) {
        return element.getTimestamp() * 1000L;
    }
});

这样我们就得到了一个带有时间标记的数据流了,后面就能做一些窗口的操作。

2.2 过滤出点击事件

在开始窗口操作之前,先回顾下需求“每隔 5 分钟输出过去一小时内点击量最多的前 N 个商品”。由于原始数

据中存在点击、购买、收藏、喜欢各种行为的数据,但是我们只需要统计点击量,所以先使用 filter 将点击行为数

据过滤出来。

.filter( data -> "pv".equals(data.getBehavior()) )

2.3 设置滑动窗口,统计点击量

由于要每隔 5 分钟统计一次最近一小时每个商品的点击量,所以窗口大小是一小时,每隔 5 分钟滑动一次。

即分别要统计[09:00, 10:00), [09:05, 10:05), [09:10, 10:10)…等窗口的商品点击量。是一个常见的滑动窗口需求

(Sliding Window)。

.keyBy("itemId")
 .timeWindow(Time.minutes(60), Time.minutes(5))
 .aggregate(new CountAgg(), new WindowResultFunction());

我们使用.keyBy("itemId")对商品进行分组,使用.timeWindow(Time size, Time slide) 对每个商品做滑动窗

口( 1 小时窗口, 5 分钟滑动一次)。然后我们使用 .aggregate(AggregateFunction af, WindowFunction wf)

做增量的聚合操作,它能使用 AggregateFunction 提 前 聚 合 掉 数 据 , 减 少 state 的 存 储 压 力 。 较之

.apply(WindowFunction wf) 会将窗口中的数据都存储下来,最后一起计算要高效地多。这里的

CountAgg 实现了 AggregateFunction 接口,功能是统计窗口中的条数, 即遇到一条数据就加一。

// 自定义预聚合函数类,每来一个数据就 count 加 1
 public static class ItemCountAgg implements AggregateFunction<UserBehavior, Long, Long>{
    @Override
    public Long createAccumulator() {
        return 0L;
    }
 
    @Override
    public Long add(UserBehavior value, Long accumulator) {
        return accumulator + 1;
    }
    
    @Override
    public Long getResult(Long accumulator) {
        return accumulator;
    }
    
    @Override
    public Long merge(Long a, Long b) {
        return a + b;
    }
 }

聚合操作 .aggregate(AggregateFunction af, WindowFunction wf) 的第二个参数WindowFunction 将每个

key 每个窗口聚合后的结果带上其他信息进行输出。我们这里实现的 WindowResultFunction 将 < 主键商品 ID ,

窗 口 , 点 击 量 > 封装成了ItemViewCount 进行输出。

代码如下:

// 自定义窗口函数,结合窗口信息,输出当前 count 结果
 public static class WindowCountResult implements WindowFunction<Long, 
ItemViewCount, Tuple, TimeWindow>{
 	@Override
 	public void apply(Tuple tuple, TimeWindow window, Iterable<Long> input, 
Collector<ItemViewCount> out) throws Exception {
 		Long itemId = tuple.getField(0);
 		Long windowEnd = window.getEnd();
 		Long count = input.iterator().next();
 		out.collect( new ItemViewCount(itemId, windowEnd, count) );
 	}
 }

现在我们就得到了每个商品在每个窗口的点击量的数据流。

2.4 计算最热门 Top N 商品

为了统计每个窗口下最热门的商品,我们需要再次按窗口进行分组,这里根据 ItemViewCount 中的

windowEnd 进行 keyBy()操作。然后使用 ProcessFunction 实现一个自定义的 TopN 函数 TopNHotItems

来计算点击量排名前 3 名的商品,并将排名结果格式化成字符串,便于后续输出。

.keyBy("windowEnd")
.process(new TopNHotItems(3)); // 求点击量前 3 名的商品

ProcessFunction 是 Flink 提供的一个 low-level API,用于实现更高级的功能。它主要提供了定时器 timer 的

功能(支持 EventTime 或 ProcessingTime)。

本案例中我们将利用 timer 来判断何时收齐了某个 window 下所有商品的点击量数据。由于Watermark 的 进

度 是 全 局 的 , 在 processElement 方 法 中 , 每 当 收 到 一 条 数 据 ItemViewCount,我们就注册一个

windowEnd+1 的定时器(Flink 框架会自动忽略同一时间的重复注册)。windowEnd+1 的定时器被触发时,意

味着收到了 windowEnd+1 的 Watermark,即收齐了该 windowEnd 下的所有商品窗口统计值。我们在onTimer()

中处理将收集的所有商品及点击量进行排序,选出 TopN,并将排名信息格式化成字符串后进行输出。

这里我们还使用了 ListState<ItemViewCount>来存储收到的每条 ItemViewCount 消息,保证在发生故障

时,状态数据的不丢失和一致性。

ListState 是 Flink 提供的类似 Java List 接口的 State API,它集成了框架的 checkpoint 机制,自动做到了

exactly-once 的语义保证。

// 求某个窗口中前 N 名的热门点击商品,key 为窗口时间戳,输出为 TopN 的结果字符串
public static class TopNHotItems extends KeyedProcessFunction<Tuple, ItemViewCount, String>{
 	private Integer topSize;
 	
 	public TopNHotItems(Integer topSize) {
 		this.topSize = topSize;
 	}
 	
 	// 定义状态,所有 ItemViewCount 的 List
 	ListState<ItemViewCount> itemViewCountListState;
 	
 	@Override
 	public void open(Configuration parameters) throws Exception {
 		itemViewCountListState = getRuntimeContext().getListState(new
 		ListStateDescriptor<ItemViewCount>("item-count-list", ItemViewCount.class));
	}
	
 	@Override
 	public void processElement(ItemViewCount value, Context ctx, Collector<String> out) 	throws Exception {
 		itemViewCountListState.add(value);
 		ctx.timerService().registerEventTimeTimer(value.getWindowEnd() + 1L);
 	}
 	
 	@Override
 	public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws 		Exception {
 		ArrayList<ItemViewCount> itemViewCounts = 
		Lists.newArrayList(itemViewCountListState.get().iterator());
 		
 		// 排序
		itemViewCounts.sort(new Comparator<ItemViewCount>() {
			@Override
			public int compare(ItemViewCount o1, ItemViewCount o2) {
				return o2.getCount().intValue() - o1.getCount().intValue();
			}
		});
 
 	// 将排名信息格式化成 String
 	StringBuilder result = new StringBuilder();
 	result.append("====================================\n");
 	result.append("窗口结束时间: ").append(new Timestamp(timestamp - 1)).append("\n");
 	for( int i = 0; i < topSize; i++ ){
 		ItemViewCount currentItemViewCount = itemViewCounts.get(i);
 		result.append("No").append(i+1).append(":")
 			.append(" 商品 ID=")
			.append(currentItemViewCount.getItemId())
 			.append(" 浏览量=")
			.append(currentItemViewCount.getCount())
 			.append("\n");
 	}
 	
 	result.append("====================================\n\n");
 	// 控制输出频率,模拟实时滚动结果
 	Thread.sleep(1000);
 	out.collect(result.toString());
 }

最后我们可以在 main 函数中将结果打印输出到控制台,方便实时观测: .print();

至此整个程序代码全部完成,我们直接运行 main 函数,就可以在控制台看到不断输出的各个时间点统计出的热门

商品。

2.5 完整代码

最终完整代码如下:

public class HotItems {
	public static void main(String[] args) throws Exception {
		// 1. 创建环境
		StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
		env.setParallelism(1);
		env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
		// 2. 读取数据,创建 DataStream
		DataStream<String> inputStream = env.readTextFile("..\\UserBehavior.csv");
		// 3. 转换为 POJO,并分配时间戳和 watermark
		DataStream<UserBehavior> dataStream = inputStream.map(line -> {
			String[] fields = line.split(",");
			return new UserBehavior(new Long(fields[0]), new Long(fields[1]), new Integer(fields[2]), fields[3],
					new Long(fields[4]));
		}).assignTimestampsAndWatermarks(new AscendingTimestampExtractor<UserBehavior>() {
			@Override
			public long extractAscendingTimestamp(UserBehavior element) {
				return element.getTimestamp() * 1000L;
			}
		});
		// 4. 分组开窗聚合,得到每个窗口内各个商品的 count 值
		DataStream<ItemViewCount> windowAggStream = dataStream.filter(data -> "pv".equals(data.getBehavior()))
				.keyBy("itemId").timeWindow(Time.hours(1), Time.minutes(5))
				.aggregate(new ItemCountAgg(), new WindowCountResult());
		// 5. 收集同一窗口的所有商品 count 值,排序输出 Top N
		DataStream<String> resultStream = windowAggStream.keyBy("windowEnd").process(new TopNHotItems(5));
		resultStream.print();
		env.execute("hot items");
	}

	// 自定义预聚合函数类,每来一个数据就 count 加 1
	public static class ItemCountAgg implements AggregateFunction<UserBehavior, Long, Long> {
		@Override
		public Long createAccumulator() {
			return 0L;
		}

		@Override
		public Long add(UserBehavior value, Long accumulator) {
			return accumulator + 1;
		}

		@Override
		public Long getResult(Long accumulator) {
			return accumulator;
		}

		@Override
		public Long merge(Long a, Long b) {
			return a + b;
		}
	}

	// 自定义窗口函数,结合窗口信息,输出当前 count 结果
	public static class WindowCountResult implements WindowFunction<Long, ItemViewCount, Tuple, TimeWindow> {
		@Override
		public void apply(Tuple tuple, TimeWindow window, Iterable<Long> input, Collector<ItemViewCount> out)
				throws Exception {
			Long itemId = tuple.getField(0);
			Long windowEnd = window.getEnd();
			Long count = input.iterator().next();
			out.collect(new ItemViewCount(itemId, windowEnd, count));
		}
	}

	public static class TopNHotItems extends KeyedProcessFunction<Tuple, ItemViewCount, String> {
		private Integer topSize;

		public TopNHotItems(Integer topSize) {
			this.topSize = topSize;
		}

		// 定义状态,所有 ItemViewCount 的 List
		ListState<ItemViewCount> itemViewCountListState;

		@Override
		public void open(Configuration parameters) throws Exception {
			itemViewCountListState = getRuntimeContext()
					.getListState(new ListStateDescriptor<ItemViewCount>("item-count-list", ItemViewCount.class));
		}

		@Override
		public void processElement(ItemViewCount value, Context ctx, Collector<String> out) throws Exception {
			itemViewCountListState.add(value);
			ctx.timerService().registerEventTimeTimer(value.getWindowEnd() + 1L);
		}

		@Override
		public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
			ArrayList<ItemViewCount> itemViewCounts = Lists.newArrayList(itemViewCountListState.get().iterator());
			// 排序
			itemViewCounts.sort(new Comparator<ItemViewCount>() {
				@Override
				public int compare(ItemViewCount o1, ItemViewCount o2) {
					return o2.getCount().intValue() - o1.getCount().intValue();
				}
			});
			// 将排名信息格式化成 String
			StringBuilder result = new StringBuilder();
			result.append("====================================\n");
			result.append("窗口结束时间: ").append(new Timestamp(timestamp - 1)).append("\n");
			for (int i = 0; i < Math.min(itemViewCounts.size(), topSize); i++) {
				ItemViewCount currentItemViewCount = itemViewCounts.get(i);
				result.append("No").append(i + 1).append(":").append(" 商品 ID=").append(currentItemViewCount.getItemId())
						.append(" 浏览量=").append(currentItemViewCount.getCount()).append("\n");
			}
			result.append("====================================\n\n");
			// 控制输出频率,模拟实时滚动结果
			Thread.sleep(1000);
			out.collect(result.toString());
		}
	}
}

2.6 更换 Kafka 作为数据源

实际生产环境中,我们的数据流往往是从 Kafka 获取到的。如果要让代码更贴近生产实际,我们只需将

source 更换为 Kafka 即可:

Properties properties = new Properties();
	properties.setProperty("bootstrap.servers", "localhost:9092");
	properties.setProperty("group.id", "consumer-group");
	properties.setProperty("key.deserializer", 
		"org.apache.kafka.common.serialization.StringDeserializer");
	properties.setProperty("value.deserializer",
		"org.apache.kafka.common.serialization.StringDeserializer");
	properties.setProperty("auto.offset.reset", "latest");
	
DataStream<String> inputStream = env.addSource(new FlinkKafkaConsumer<String>("hotitems",
	new SimpleStringSchema(), properties));

当然,根据实际的需要,我们还可以将 Sink 指定为 Kafka、ES、Redis 或其它存储,这里就不一一展开实现了。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
针对这个问题,可以通过以下步骤来实现: 1. 从 Flink 电商平台的用户行为记录中筛选出购买行为记录。 2. 将购买行为记录按照用户 ID 进行分组。 3. 对每个用户的购买记录进行聚合,统计每种商品的购买次数。 4. 找出每个用户购买次数最多的商品,输出结果。 具体实现的代码可以参考以下示例: ``` DataStream<Tuple2<String, String>> input = ...; // 输入流,格式为 (user_id, behavior) DataStream<Tuple2<String, String>> buy = input.filter(b -> b.f1.equals("buy")); // 过滤出购买行为记录 DataStream<Tuple2<String, Tuple2<String, Integer>>> userItemCounts = buy .map(b -> Tuple2.of(b.f0, b.f2)) // 转化为 (user_id, item_id) .keyBy(b -> b.f0) // 按照 user_id 进行分组 .window(TumblingProcessingTimeWindows.of(Time.minutes(5))) // 设置窗口大小为 5 分钟 .aggregate(new AggregateFunction<Tuple2<String, String>, Map<String, Integer>, Map<String, Integer>>() { @Override public Map<String, Integer> createAccumulator() { return new HashMap<>(); } @Override public Map<String, Integer> add(Tuple2<String, String> value, Map<String, Integer> accumulator) { accumulator.put(value.f1, accumulator.getOrDefault(value.f1, 0) + 1); return accumulator; } @Override public Map<String, Integer> getResult(Map<String, Integer> accumulator) { return accumulator; } @Override public Map<String, Integer> merge(Map<String, Integer> a, Map<String, Integer> b) { b.forEach((k, v) -> a.merge(k, v, Integer::sum)); return a; } }) // 对每个用户的购买记录进行聚合,统计每种商品的购买次数 .map(b -> Tuple2.of(b.f0, b.f1.entrySet().stream().max(Map.Entry.comparingByValue()).orElse(null))) // 找出购买次数最多的商品 .filter(b -> b.f1 != null) .map(b -> Tuple2.of(b.f0, b.f1.getKey())); userItemCounts.print(); // 输出结果,格式为 (user_id, item_id) ``` 以上代码中使用了 Flink 中的 window 计算机制,将每个用户的购买记录按照 5 分钟为窗口进行统计聚合。具体的聚合操作使用了 Flink 支持的 AggregateFunction,可以自定义实现聚合逻辑并返回结果。最后使用了 stream 的 max 函数来找出每个用户购买次数最多的商品

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

步道师就是我

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值