Flink实战六_直播礼物统计

接上文:Flink实战五_状态机制

1、需求背景

现在网络直播平台非常火爆,在斗鱼这样的网络直播间,经常可以看到这样的总榜排名,体现了主播的人气值。

人气值计算规则:用户发送1条弹幕互动,赠送1个荧光棒免费道具、100个免费鱼丸、亲密度礼物等行为,均可为主播贡献1点及以上人气值。

我们就以这个人气值日榜为例,来设计一个Flink的计算程序。

在这里插入图片描述
对于人气值日榜这样的功能,可以理解为是一个典型的流式计算的场景,强调的是数据的实时处理。因为在这个场景下,必须要及时的累计用户的送礼物数据,才能形成你追我赶的实时效果,提升用户的参与体验。这个场景下的实时性,虽然不要求每一条数据都及时响应,但是整体的数据延迟还是要尽量缩短的。

这种场景下,使用Flink进行流批统一的计算,感觉就非常合适。

2、数据流程设计

在确定了使用Flink进行计算后,首先就需要设计出数据的上下游流程,进行简单的方案可行性评估。

对于数据上游,我们这个人气值日榜统计的业务场景,数据来源自然就是粉丝们的打赏行为。一方面整个平台的打赏行为的数据量是非常大的,另一方面这些打赏行为涉及到账户操作,所以他的作用,更大的是体现在人气值榜功能以外的其他业务过程中。基于这两方面考虑,自然就会想到使用kafka来进行削峰以及解耦。而Flink在DataStream/DataSet API和 Table API&SQL 两个部分都对kafka提供了连接器实现,所以用kafka作为数据接入是可行的。

而对于数据下游,其实可以想象,最终计算出来的数据,最为重要的是要强调查询的灵活性以及时效性,这样才能支持页面的快速查询。如果考虑查询的时效性,HBase和ElasticSearch都是比较理想的大数据存储引擎。但是如果考虑到查询的灵活性,就会想到ElasticSearch会相比HBase更适合。因为我们统计出来的这些粉丝人气值度的结果,不光可以作为每个直播间人气值榜的排名,也应该可以作为以后平台主播年度排名等其他业务场景的数据来源。如果想要兼顾这些查询场景,使用HBase就会对Rowkey产生大量的侵入,而Elasticsearch可以根据任意字段快速查询,就比较有优势。 另外,从官方文档中可以查到,对于HBase,Flink只提供了Table API&SQL 模块的connector支持,而DataStream/DataSet API中没有提供支持,而ElasticSearch则支持更为全面。当然,这跟HBase的具体场景是有关联的,但是也可以从另一个角度认为,使用ElasticSearch的可行性更高。

这样,就初步确定了 kafka-> Flink -> ElasticSearch 这样的大致数据流程。这
也是在实际开发中非常典型的一个组合方式。后续就可以着手搭建kafka集群以及ElasticSearch+Kibana的集群了。搭建的过程就略过了。

确定数据的基础结构
这一步主要是确定入口数据和出口数据的结构。只要这两个数据结构确定了,那
么应用程序模块和大数据计算模块就可以分开进行开发了。是双方主要的解耦方
式。

在数据入口处,可以定义这样的简化的数据结构:

public static class GiftRecord{
private String hostId; //主播ID
private String fansId; //粉丝ID
private long giftCount; //礼物数量
private String giftTime; //送礼物时间。时间格式 yyyy-MM-DD HH:mm:SS
.....
}

在kafka中,确定使用gift作为Topic,MQ的消息格式为 #{hostId},#{fansId},#{giftCount},#{giftTime} 这样的字符串。

在数据出口处,可以定义ES中这样简化的索引结构:

-- 贡献日榜索引
PUT daygiftanalyze
{
"mappings":{"properties": {"windowEnd":{"type": "long"},"hostId": {"type": "keyword"},"fansId": {"type": "keyword"},"giftCount":{"type": "long"}}}
}

这样,一个简单的设计方案就形成了。应用程序只需要在粉丝发送礼物时往kafka中同步一条消息记录,然后从ES中查询主播的人气值日榜和人气值周榜数据即可。而我们也可以模拟数据格式进行开发了。

3、应用实现

人气值日榜:
基础数据结构:

public static class GiftRecord{private String hostId; //主播IDprivate String fansId; //粉丝IDprivate long giftCount; //礼物数量private String giftTime; //送礼物时间。时间格式 yyyy-MM-DD HH:mm:SS.....
}

在kafka中,确定使用gift作为Topic,MQ的消息格式为 #{hostId},#{fansId},#{giftCount},#{giftTime} 这样的字符串。

ES索引:

PUT daygiftanalyze
{"mappings": {"properties": {"windowEnd": {"type": "long"},"hostId": {"type": "keyword"},"fansId": {"type": "keyword"},"giftCount": {"type": "long"}}}
}

然后运行Flink程序,com.flink.project.flink.DayGiftAna,从kafka中读取数
据。测试数据见giftrecord.txt。计算程序会及时将十分钟内的粉丝礼物统计都存入到ES当中。

giftrecord.txt如下:

1001,3001,100,2021-09-15 15:15:10
1001,3002,321,2021-09-15 15:17:14
1001,3003,234,2021-09-15 15:16:24
1001,3004,15,2021-09-15 15:17:13
1001,3005,264,2021-09-15 15:18:14
1001,3006,678,2021-09-15 15:17:54
1001,3007,123,2021-09-15 15:19:22
1001,3008,422,2021-09-15 15:18:37
1001,3009,566,2021-09-15 15:22:43
1001,3001,76,2021-09-15 15:21:28
1001,3001,88,2021-09-15 15:26:28
1001,3007,168,2021-09-15 15:32:29
1001,3002,157,2021-09-15 15:28:56
1001,3009,567,2021-09-15 15:27:32
1001,3004,145,2021-09-15 15:30:26
1001,3003,1656,2021-09-15 15:31:19
1001,3005,543,2021-09-15 15:36:49
1001,3001,864,2021-09-15 15:38:26
1001,3001,548,2021-09-15 15:45:10
1001,3007,359,2021-09-15 15:52:39
1001,3008,394,2021-09-15 15:59:48

com.flink.project.flink.DayGiftAna,如下:


import com.roy.flink.project.fansgift.FansGiftResult;
import com.roy.flink.project.fansgift.GiftRecord;
import org.apache.commons.lang.SystemUtils;
import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.api.common.functions.AggregateFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.RichAggregateFunction;
import org.apache.flink.api.common.functions.RuntimeContext;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.contrib.streaming.state.RocksDBStateBackend;
import org.apache.flink.runtime.state.StateBackend;
import org.apache.flink.runtime.state.filesystem.FsStateBackend;
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.windowing.RichWindowFunction;
import org.apache.flink.streaming.api.functions.windowing.WindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.streaming.connectors.elasticsearch.ElasticsearchSinkFunction;
import org.apache.flink.streaming.connectors.elasticsearch.RequestIndexer;
import org.apache.flink.streaming.connectors.elasticsearch7.ElasticsearchSink;
import org.apache.flink.util.Collector;
import org.apache.http.HttpHost;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.Requests;import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.*;import static org.apache.flink.util.Preconditions.checkArgument;
import static org.apache.flink.util.Preconditions.checkNotNull;/*** @desc 贡献日榜计算程序*/
public class DayGiftAna {public static void main(String[] args) throws Exception {final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();env.getConfig().setAutoWatermarkInterval(1000L); //BoundedOutOfOrdernessWatermarks定时提交Watermark的间隔
//        env.setStateBackend(new RocksDBStateBackend("hdfs://hadoop01:8020/dayGiftAna"));// Checkpoint存储到文件if(SystemUtils.IS_OS_WINDOWS){env.setStateBackend(new FsStateBackend("file:///D:/flink_file"));}else{// linuxenv.setStateBackend(new FsStateBackend("file:///home/file_file"));}//使用Socket测试。env.setParallelism(1);final DataStreamSource<String> dataStream = env.socketTextStream("10.86.97.206", 7777);final SingleOutputStreamOperator<FansGiftResult> fansGiftResult = dataStream.map((MapFunction<String, GiftRecord>) value -> {final String[] valueSplit = value.split(",");//SimpleDateFormat 多线程不安全。SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");final long giftTime = sdf.parse(valueSplit[3]).getTime();return new GiftRecord(valueSplit[0], valueSplit[1], Integer.parseInt(valueSplit[2]), giftTime);}).assignTimestampsAndWatermarks(WatermarkStrategy.<GiftRecord>forBoundedOutOfOrderness(Duration.ofSeconds(2)).withTimestampAssigner((SerializableTimestampAssigner<GiftRecord>) (element, recordTimestamp) -> element.getGiftTime()))
//          .keyBy((KeySelector<GiftRecord, String>) value -> value.getHostId() + "_" + value.getFansId()) //按照HostId_FansId分组.keyBy((KeySelector<GiftRecord, String>) value -> value.getHostId()) //按照HostId分组.window(TumblingEventTimeWindows.of(Time.seconds(10)))
//                .allowedLateness(Time.seconds(2)).aggregate(new WinodwGiftRecordAgg(), new AllWindowGiftRecordAgg());//打印结果测试fansGiftResult.print("fansGiftResult");env.execute("DayGiftAna");}//在每个子任务中将窗口期内的礼物进行累计合并//增加状态后端。private static class WinodwGiftRecordAgg implements AggregateFunction<GiftRecord, Long, Long> {@Overridepublic Long createAccumulator() {return 0L;}@Overridepublic Long add(GiftRecord value, Long accumulator) {Long res = accumulator + value.getGiftCount();return res;}@Overridepublic Long getResult(Long accumulator) {return accumulator;}@Overridepublic Long merge(Long a, Long b) {return a + b;}}//对窗口期内的所有子任务进行窗口聚合操作。private static class AllWindowGiftRecordAgg extends RichWindowFunction<Long, FansGiftResult, String, TimeWindow> {ValueState<FansGiftResult> state;@Overridepublic void apply(String s, TimeWindow window, java.lang.Iterable<Long> input, Collector<FansGiftResult> out) throws Exception {final String[] splitKey = s.split("_");String hostId = splitKey[0];String fansId ="";if(splitKey.length>1){fansId=splitKey[1];}final Long giftCount = input.iterator().next();final long windowEnd = window.getEnd();final FansGiftResult fansGiftResult = new FansGiftResult(hostId, fansId, giftCount, windowEnd);out.collect(fansGiftResult);state.update(fansGiftResult);}@Overridepublic void open(Configuration parameters) throws Exception {final ValueStateDescriptor<FansGiftResult> stateDescriptor = new ValueStateDescriptor<>("WinodwGiftRecordAgg", TypeInformation.of(new TypeHint<FansGiftResult>() {}));state = this.getRuntimeContext().getState(stateDescriptor);}}
}

FansGiftResult,代码如下:

public class FansGiftResult {private String hostId;private String fansId;private long giftCount;private long windowEnd;public FansGiftResult() {}public FansGiftResult(String hostId, String fansId, long giftCount, long windowEnd) {this.hostId = hostId;this.fansId = fansId;this.giftCount = giftCount;this.windowEnd = windowEnd;}@Overridepublic String toString() {if(fansId!=null && fansId.length()>0){return "FansGiftResult{" +"hostId='" + hostId + '\'' +", fansId='" + fansId + '\'' +", giftCount=" + giftCount +", windowEnd=" + windowEnd +'}';}else{return "FansGiftResult{" +"hostId='" + hostId + '\'' +", giftCount=" + giftCount +", windowEnd=" + windowEnd +'}';}}public String getHostId() {return hostId;}public void setHostId(String hostId) {this.hostId = hostId;}public String getFansId() {return fansId;}public void setFansId(String fansId) {this.fansId = fansId;}public long getGiftCount() {return giftCount;}public void setGiftCount(long giftCount) {this.giftCount = giftCount;}public long getWindowEnd() {return windowEnd;}public void setWindowEnd(long windowEnd) {this.windowEnd = windowEnd;}
}

GiftRecord,代码如下:


public class GiftRecord {private String hostId; //主播IDprivate String fansId; //粉丝IDprivate int giftCount; //礼物数量private long giftTime; //送礼物时间。原始时间格式 yyyy-MM-DD HH:mm:ss,ssspublic GiftRecord() {}public GiftRecord(String hostId, String fansId, int giftCount, long giftTime) {this.hostId = hostId;this.fansId = fansId;this.giftCount = giftCount;this.giftTime = giftTime;}public String getHostId() {return hostId;}public void setHostId(String hostId) {this.hostId = hostId;}public String getFansId() {return fansId;}public void setFansId(String fansId) {this.fansId = fansId;}public int getGiftCount() {return giftCount;}public void setGiftCount(int giftCount) {this.giftCount = giftCount;}public long getGiftTime() {return giftTime;}public void setGiftTime(long giftTime) {this.giftTime = giftTime;}@Overridepublic String toString() {return "GiftRecord{" +"hostId='" + hostId + '\'' +", fansId='" + fansId + '\'' +", giftCount=" + giftCount +", giftTime='" + giftTime + '\'' +'}';}
}

ES查询语句:

GET daygiftanalyze/_search
{"query": {"bool": {"must": [{"range": {"windowEnd": {"gte": 1631635200000,"lte": 1631721600000}}},{"match": {"hostId": "1001"}}]}},"aggs": {"groupByFans": {"terms": {"field": "fansId","size": 3,"order": {"giftCount": "desc"}},"aggs": {"giftCount": {"sum": {"field": "giftCount"}}}}}
}

ES中的查询结果:
在这里插入图片描述
直播应用就可以根据这个查询结果组织客户端查询代码,最终实现日榜排名的功能。

4、实现效果分析

具体的计算方案参见示例代码,这里就不多做分析了。这里只分析一下在实现过程中需要注意的几个重要的问题:

  • 时间语义分析
    对于网络直播这样的场景,从下午六点到第二天早上六点才是一天的高峰期,所以,在进行统计时,将每一天的统计时间定义为从早上六点到第二天早上六点,这样就能尽量保持高峰期的完整性。很多跟娱乐相关的场景,比如网络游戏,也大都是以这样的范围来定义一天,而不是传统意义上的从0点到24点。

  • 并行度优化
    可以直接使用Flink的开窗机制,待一周的数据收集完整了之后,一次性向ES中输出统计结果,这种场景下要注意累计器的持久化,以及计算程序出错后的重启恢复机制。

  • 后续改进方式
    状态后端、而对于人气值日榜的计算,就不能等一天的数据收集齐了再计算了。这时是有两种解决方案,一种是完全的流处理方式。也就是每来一条数据就往ES中更新结果。另一中方式是采用小批量的流处理方式。以五分钟为单位,将数据拆分成一个一个小窗
    口来进行处理。显然后一种方式对数据处理的压力会比较小一点。虽然数据量会更
    多,但是ES的存储以及快速查询能力可以比较好的弥补数据量的问题。也因此,在
    设计ES数据机构时,将人气值日榜的文档结构设计成了一个一个的小范围。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/673012.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

07-使用Package、Crates、Modules管理项目

上一篇&#xff1a;06-枚举和模式匹配 当你编写大型程序时&#xff0c;组织代码将变得越来越重要。通过对相关功能进行分组并将具有不同功能的代码分开&#xff0c;您可以明确在哪里可以找到实现特定功能的代码&#xff0c;以及在哪里可以改变功能的工作方式。 到目前为止&…

必收藏!第六版CCF推荐会议C类国际学术会议!(中国计算机学会)

中国计算机学会 中国计算机学会&#xff08;CCF&#xff09;是全国性、学术性、非营利的学术团体&#xff0c;由从事计算机及相关科学技术领域的个人和单位自愿组成。作为独立社团法人&#xff0c;CCF是中国科学技术协会的成员之一&#xff0c;是全国一级学会&#xff01; CCF的…

JavaScript基础第四天

JavaScript 基础第四天 今天我们学习js的函数&#xff0c;包括普通函数、匿名函数、箭头函数以及函数作用域。 1. 函数的初体验 1.1. 什么是函数 函数是 JavaScript 中的基本组件之一。一个函数是 JavaScript 过程一组执行任务或计算值的语句。要使用一个函数&#xff0c;你…

Linux下库函数、静态库与动态库

库函数 什么是库 库是二进制文件, 是源代码文件的另一种表现形式, 是加了密的源代码; 是一些功能相近或者是相似的函数的集合体. 使用库有什么好处 提高代码的可重用性, 而且还可以提高程序的健壮性;可以减少开发者的代码开发量, 缩短开发周期. 库制作完成后, 如何给用户…

大模型为什么会有 tokens 限制?

人是以字数来计算文本长度&#xff0c;大语言模型 &#xff08;LLM&#xff09;是以 token 数来计算长度的。LLM 使用 token 把一个句子分解成若干部分。 token 可以是一个单词、一个单词中的一个部分、甚至是一个字符&#xff0c;具体取决于它使用的标记化方法 (tokenization…

为电子表格嵌入数据库,Excel/WPS一键升级为管理系统

将Excel表格转化为管理系统&#xff0c;这款工具能够实现只需导入表格数据&#xff0c;即可自动生成相应的软件和APP。 表格办公的烦恼&#xff0c;有遇到吧&#xff1f; 对于具有一定规模的企业而言&#xff0c;各类表格如同繁星般众多&#xff0c;既有日常使用的常规表格&a…

泰克示波器——TBS2000系列界面整体介绍

目录 1.1 通道区域面板标识1.2 示波器测试输出&#xff08;检测探针与设置的好坏&#xff09;1.3 面板其他快捷按钮1.4 波器整体界面 1.1 通道区域面板标识 在通道面板的下方标识有示波器的通道属性以及参数值&#xff0c;如我使用的型号为“TBS2104X”的示波器&#xff0c;面…

格子表单GRID-FORM | 文档网站搭建(VitePress)与部署(Github Pages)

格子表单/GRID-FORM已在Github 开源&#xff0c;如能帮到您麻烦给个星&#x1f91d; GRID-FORM 系列文章 基于 VUE3 可视化低代码表单设计器嵌套表单与自定义脚本交互文档网站搭建&#xff08;VitePress&#xff09;与部署&#xff08;Github Pages&#xff09; 效果预览 格…

如何使用VMware分享出来的虚拟机系统(OVF文件)

前言 这几天看到很多小伙伴都在安装虚拟机&#xff0c;但成不成就不知道了。 所以小白准备把自己安装完成的系统打包分享给小伙伴。 如果你需要已经安装完成的虚拟系统&#xff0c;可以获取哦&#xff01;打开即用&#xff01; 虚拟机系统包括&#xff1a; Win10 专业版 Wi…

anaconda+pytorch+pycharm安装总结

1.下载最新的Anaconda,目前是python3.11适用 anaconda官网 安装教程 卸载并重新安装的教程 &#xff08;如果找不到火绒清理注册表垃圾的位置可以拉到文章底部查看&#xff09; 2.pytorch安装&#xff0c;注意python版本、cuda版本和pytorch版本的适配 安装教程 3.pycharm安装和…

C++: 模板初阶

目录 引子&#xff1a; 函数模板 格式&#xff1a; 例子&#xff1a; 函数模板的实例化&#xff1a; 隐形实例化&#xff1a;让编译器根据实参推演模板参数的实际类型 显式实例化&#xff1a; 在函数名后的<>中指定模板参数的实际类型 模板参数的匹配原则 类模…

ssm+vue的校园一卡通密钥管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的校园一卡通密钥管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系…

C++入门学习(二十五)do-while循环

do { // 代码块&#xff0c;至少会执行一次 } while (条件); 对比一下while和do-while循环&#xff1a; 因为while循环先判断条件&#xff0c;所以数字10直接就没有进入for循环里&#xff0c;卡在了判断条件这一步&#xff0c;所以就没有输出数据&#xff1b; do-while循环是…

clickhouse计算前后两点间经纬度距离

问题 计算如图所示前后两点经纬度的距离&#xff1f; 方法 1、用开窗函数将如图所示数据下移一行 selectlongitude lon1,latitude lat1,min(longitude) over(order by time1 asc rows between 1 PRECEDING and 1 PRECEDING) lon2,min(latitude) over(order by time1 asc row…

Vagrant 虚拟机工具基本操作指南

Vagrant 虚拟机工具基本操作指南 ​#虚拟机 #​ ​#vargant#​ ​#ubuntu#​ ‍ 虚拟机virtualbox ,VMWare及WSL等大家都很了解了&#xff0c;那Vagrant是什么东西&#xff1f; 它是一组命令行工具&#xff0c;可以象Docker管理容器一样管理虚拟机&#xff0c;这样快速创…

鸿蒙OS导入项目报错不能运行 @ohos\hvigor\bin\hvigor.js‘

在自学HarmonyOS时&#xff0c;想在DevEco Studio导入官方示例代码&#xff1a;待办列表&#xff08;ArkTS&#xff09;报错 C:\Users\woods\Downloads\test01\ToDoListArkTS\node_modules\ohos\hvigor\bin\hvigor.js --mode module -p moduleentrydefault -p productdefault …

03-抓包_封包_协议_APP_小程序_PC应用_WEB应用

抓包_封包_协议_APP_小程序_PC应用_WEB应用 一、参考工具二、演示案例&#xff1a;2.1、WEB应用站点操作数据抓包-浏览器审查查看元素网络监听2.2、APP&小程序&PC抓包HTTP/S数据-Charles&Fiddler&Burpsuite2.3、程序进程&网络接口&其他协议抓包-WireSh…

YOLO部署实战(2):使用OpenCV优化视频转图片流程并设置帧数

在计算机视觉和图像处理领域&#xff0c;OpenCV是一个强大的开源库&#xff0c;它为处理图像和视频提供了丰富的工具和功能。本文将介绍如何使用OpenCV将视频文件转换为一系列图片&#xff0c;并演示如何通过设置转换的帧数来优化这一过程。 1 Win10配置OpenCV 在Windows操作…

数据结构与算法之美学习笔记:50 | 索引:如何在海量数据中快速查找某个数据?

目录 前言为什么需要索引&#xff1f;索引的需求定义构建索引常用的数据结构有哪些&#xff1f;总结引申 前言 本节课程思维导图&#xff1a; 在第 48 节中&#xff0c;我们讲了 MySQL 数据库索引的实现原理。MySQL 底层依赖的是 B 树这种数据结构。留言里有同学问我&#xff…

提示由于找不到msvcp120dll无法继续执行此代码怎么办

在计算机系统中&#xff0c;MSVCP120.dll是一个至关重要的动态链接库文件&#xff0c;它是Microsoft Visual C Redistributable Package的一部分&#xff0c;对于许多基于Windows的应用程序运行至关重要。当系统提示“msvcp120dll丢失”时&#xff0c;意味着该文件可能由于误删…