背景
阿里的雙11銷量大屏可以說是每年雙十一的一道特殊的風景線。實時大屏(real-time dashboard)正在被越來越多的企業採用,用來及時呈現關鍵的數據指標。並且在實際操作中,肯定也不會僅僅計算一兩個維度。由於Flink的“真·流式計算”這一特點,它比Spark Streaming要更適合大屏應用。本文將結合實際工作經驗抽象出簡單的模型,並簡要敘述計算流程(當然大部分都是源碼)。
由於大屏的最大訴求是實時性,等待遲到數據顯然不太現實,因此我們採用處理時間作爲時間特徵,接下來以計算PV爲例,給大家展示具體的計算流程
需求:計算每一天的PV,並且每秒更新一次PV值
注意:爲了展示,計算一天數據沒法展示測試結果,這裏的demo是計算一分鐘內的pv,每秒更新一次
數據準備:
{
"userId": 234567,
"orderId": 2902306918400,
"subOrderId": 2902306918401,
"siteId": 10219,
"siteName": "site_blabla",
"cityId": 101,
"cityName": "北京市",
"warehouseId": 636,
"merchandiseId": 187699,
"price": 299,
"quantity": 2,
"orderStatus": 1,
"isNewOrder": 0,
"timestamp": 1572963672217
}
使用用戶的行爲日誌作爲我們計算的數據,線上日誌數據存在kafka中,這裏爲了演示。本地使用nc -lp 模擬用戶數據 userId +其他行爲日誌(這裏用1代替)
邏輯代碼:
使用userId進行分組,開10s中的處理時間窗口(模擬線上1天的窗口),並同時設定ContinuousProcessingTimeTrigger觸發器,以1秒週期觸發計算去更新最新的PV值
public class PVTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime);
DataStreamSource<String> source = env.socketTextStream("127.0.0.1", 10080);
SingleOutputStreamOperator<UserInfo> userStream = source.map(new MapFunction<String, UserInfo>() {
@Override
public UserInfo map(String s) throws Exception {
String[] split = s.split(",");
UserInfo subOrderDetail = new UserInfo();
subOrderDetail.setOrderId(split[1]);
subOrderDetail.setUserId(split[0]);
return subOrderDetail;
}
});
userStream
.keyBy("userId")
.window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
.trigger(ContinuousProcessingTimeTrigger.of(Time.seconds(1)))
.aggregate(new RsesultAggregateFunc())
.print("每秒執行一次---------");
env.execute("test");
}
計算PV的窗口函數
public static class RsesultAggregateFunc implements AggregateFunction<UserInfo,ResultInfo,ResultInfo>{
//初始化計數器:就是給你要計算指標賦予初始值
@Override
public ResultInfo createAccumulator() {
ResultInfo resultInfo = new ResultInfo();
return resultInfo;
}
//指標計算:指標增長的邏輯 比如pv userID一樣加一
@Override
public ResultInfo add(UserInfo userInfo, ResultInfo resultInfo) {
if(userInfo.getUserId().equals(resultInfo.getUserId())){
resultInfo.setOrderId( resultInfo.count+=1);
}else {
resultInfo.setUserId(userInfo.getUserId());
resultInfo.setOrderId(1);
}
return resultInfo;
}
@Override
public ResultInfo getResult(ResultInfo resultInfo) {
return resultInfo;
}
//指標結果合併
@Override
public ResultInfo merge(ResultInfo resultInfo1, ResultInfo resultInfo2) {
resultInfo2.setOrderId(resultInfo2.getOrderId()+resultInfo1.getOrderId());
return resultInfo2;
}
}
public static class UserInfo implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private String orderId;
public UserInfo() {
}
public UserInfo(String userId, String orderId) {
this.userId = userId;
this.orderId = orderId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
}
public static class ResultInfo implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private int count;
public ResultInfo() {
}
public ResultInfo(String userId, int count) {
this.userId = userId;
this.count = count;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public int getOrderId() {
return count;
}
public void setOrderId(int count) {
this.count = count;
}
@Override
public String toString() {
return "ResultInfo{" +
"userId='" + userId + '\'' +
", count=" + count +
'}';
}
}
}
注意事項:
1秒內有數據變化的站點並不多,而ContinuousProcessingTimeTrigger每次觸發都會輸出窗口裏全部的聚合數據,對於線上大數據量的情況下,這樣做了很多無用功,並且還會增大Redis的壓力。所以,我們可以在聚合結果後再接一個ProcessFunction,ProcessFuntcion的使用以及其他窗口函數的使用可以參考我的上篇文章https://blog.csdn.net/aA518189/article/details/85250713
結果展示:
通過結果我們看到10s內(線上是1天)的pv,每秒更新一次。計算uv,實時訂單也是和pv一樣的流程,只是具體邏輯改改而已,還看什麼自己實現一下試試!!!
掃一掃加入大數據公衆號,瞭解更多大數據技術,還有免費資料等你哦
掃一掃加入大數據公衆號,瞭解更多大數據技術,還有免費資料等你哦
掃一掃加入大數據公衆號,瞭解更多大數據技術,還有免費資料等你哦