0.需求
實時輸出當天的搜索詞排名,即實時呈現熱點搜索詞
1.數據源讀取
對接Kafka數據源,將消息轉成實體類(實體類屬性主要是關鍵詞和搜索時間)
public class KafkaUtil {
public static FlinkKafkaConsumerBase<String> text(String topic) throws IOException {
return text(topic, "kafka.properties");
}
/**
* @param topic 主題名稱
* @param configPath Kafka屬性配置文件路徑
*/
public static FlinkKafkaConsumerBase<String> text(String topic,String configPath) throws IOException {
//1.加載Kafka屬性
Properties prop=new Properties();
//Class.getClassLoader.getResourceAsStream 默認是從ClassPath根下獲取,path不能以“/"開頭
InputStream in=KafkaUtil.class.getClassLoader().getResourceAsStream(configPath);
prop.load(in);
//2.構造FlinkKafkaConsumer
FlinkKafkaConsumerBase<String> consumer=new FlinkKafkaConsumer011<>(topic,new SimpleStringSchema(),prop);
//todo 可以進行消費者的相關配置
// 本地debug不提交offset consumer.setCommitOffsetsOnCheckpoints(false);
return consumer;
}
}
env.addSource(KafkaUtil.text("topic"));
2.搜索詞統計
在進行窗口內統計時,首先需要根據yyyy-MM-dd的維度對搜索詞消息進行keyby操作,形成KeyedStream,進一步調用實現了抽象類KeyedProcessFunction的方法。
2.1 KeyedProcessFunction介紹
顧名思義,KeyedProcessFunction,是針對具有相同key的stream進行元素處理的方法。
public abstract class KeyedProcessFunction<K, I, O> extends AbstractRichFunction
三個泛型分別是key的類型,流輸入類型,流輸出類型。
<1> 方法public abstract void processElement(I value, Context ctx, Collector<O> out) throws Exception;
處理流中的每一個元素,即每有一個元素進來,就會執行一次processElement方法。
該方法可以通過參數Collector<O> out來產生0個或者多個元素;
也可以通過Context ctx來更新state或者設置定時器timers。
ctx通過調用timerService()可以注冊定時器。
Context的屬性方法:
public abstract class Context {
/**
* Timestamp of the element currently being processed or timestamp of a firing timer.
*
* <p>This might be {@code null}, for example if the time characteristic of your program
* is set to {@link org.apache.flink.streaming.api.TimeCharacteristic#ProcessingTime}.
*/
public abstract Long timestamp();
/**
* A {@link TimerService} for querying time and registering timers.
*/
public abstract TimerService timerService();
/**
* Emits a record to the side output identified by the {@link OutputTag}.
*
* @param outputTag the {@code OutputTag} that identifies the side output to emit to.
* @param value The record to emit.
*/
public abstract <X> void output(OutputTag<X> outputTag, X value);
/**
* Get key of the element being processed.
*/
public abstract K getCurrentKey();
}
對于timestamp()方法,需要注意的是注釋上說明可能會返回為null,如果設置的時間類型是ProcessingTime。
<2>方法public void onTimer(long timestamp, OnTimerContext ctx, Collector<O> out) throws Exception {}
當通過某個定時器timer通過TimerService設置后,會調用onTimer方法。
2.2 具體實現
因為需要實時統計當天的搜索詞熱度,所以在KeyedProcessFunction實現方法中,需要使用到狀態。
private MapState<String, DashboardKeyword> keywordState;
Map的鍵就是搜索詞,值就是輸出實體類(兩個屬性分別是關鍵詞和搜索次數)
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
StateTtlConfig retainOneDay = StateTtlUtil.retainOneDay();
MapStateDescriptor<String, DashboardKeyword> keywordStateDescriptor = new MapStateDescriptor<>(
"keyword",
Types.STRING,
Types.POJO(DashboardKeyword.class)
);
keywordStateDescriptor.enableTimeToLive(retainOneDay);
this.keywordState = getRuntimeContext().getMapState(keywordStateDescriptor);
}
KeyedProcessFunction繼承了AbstractRichFunction,因此可以使用到RuntimeContext。也進一步可以使用到狀態,在open方法里面定義State并且設置State的存活時間為1天。
@Override
public void processElement(Search search, Context context, Collector<Tuple2<Integer, DashboardKeyword>> collector) throws Exception {
Result result = DicAnalysis.parse(search.getKeyword());
for (Term term: result.getTerms()) {
String key = term.getName();
if (key.trim().length() < 2) continue;
DashboardKeyword value;
if (this.keywordState.get(key) != null) {
value = this.keywordState.get(key);
} else {
value = new DashboardKeyword();
value.setKeyword(key);
value.setFrequency(0L);
}
value.setFrequency(value.getFrequency() + 1L);
this.keywordState.put(key, value);
}
long coalescedTime = ((System.currentTimeMillis() + 5000) / 5000) * 5000;
context.timerService().registerProcessingTimeTimer(coalescedTime);
}
processElement里面的代碼就是很常見的套路,如果MapState中已經存在當前搜索詞了,即獲取對應的value,并將其頻次增加;如果MapState中并沒有當前的搜索詞,則將該關鍵詞及對應value添加到Map當中。
這里是給定時器添加5s的時長,這里的寫法((System.currentTimeMillis() + 5000) / 5000) * 5000
是計時器合并的目的,Flink對于每個鍵和時間戳都只會維護一個計時器(計時器太多會影響性能),需要通過降低計時器的精度來合并計時器,從而減少計時器的數量。
假設現在是15s,那么定時器為20s,利用上面的寫法,16s,17s,18s,19s的定時器都是20s。
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<Tuple2<Integer, DashboardKeyword>> collector) throws Exception {
java.util.stream.Collector<DashboardKeyword, ?, List<DashboardKeyword>> top50Collector = Comparators.greatest(
50,
Comparator.comparingLong(DashboardKeyword::getFrequency)
);
List<DashboardKeyword> top50 = Streams.stream(this.keywordState.values()).collect(top50Collector);
for (int rank = 0; rank < top50.size(); rank++) {
collector.collect(Tuple2.of(rank, top50.get(rank)));
}
}
統計前50搜索詞。
3.數據存儲
存儲到redis中,
注意flink1.7版本之后,官方沒有redis sink,可以去http://bahir.apache.org/(flink以及spark擴展庫),
粘貼源碼實現redis sink。