目录
定时计算和实时计算
定时计算:在Day10中,使用xxl-job工具实现热点文章的定时更新,每天凌晨2点读取MySQL数据库中数据并根据加权计算对应文章的热度,即分值score,根据分值排序选取对应的文章将其缓存到Redis数据库中,对App端用户进行数据推荐。
实时计算:在用户访问浏览的同时根据用户对应的行为更新响应文章的score,从而实现更加精准的向用户推荐相关文章。
定时计算(批量计算)与实时计算(流式计算)的对比图示:
Kafka Strem相关概念
源处理器(Source Processor):源处理器是一个没有任何上游处理器的特殊类型的流处理器。它从一个或多个kafka主题生成输入流。通过消费这些主题的消息并将它们转发到下游处理器。
Sink处理器:sink处理器是一个没有下游流处理器的特殊类型的流处理器。它接收上游流处理器的消息发送到一个指定的Kafka主题。
KStream:Kafka Stream主要依赖的数据结构。
相关概念链接:Kafka入门实战教程(7):Kafka Streams-腾讯云开发者社区-腾讯云 (tencent.com)
Kafka Stream应用执行逻辑
Kafka Streams宣称自己实现了精确一次处理语义(Exactly Once Semantics, EOS,以下使用EOS简称),所谓EOS,是指消息或事件对应用状态的影响有且只有一次。其实,对于Kafka Streams而言,它天然支持端到端的EOS,因为它本来就是和Kafka紧密相连的。下图展示了一个典型的Kafka Streams应用的执行逻辑:
一个Kafka Strem需要执行5个步骤:
- 读取最新处理消息的位移。
- 读取消息数据。
- 执行处理逻辑。
- 将处理结果写回Kafka。
- 保存位置信息。
这五步的执行必须是原子性的,否则无法实现精确一次处理语义。而在设计上,Kafka Streams在底层大量使用了Kafka事务机制和幂等性Producer来实现多分区的写入,又因为它只能读写Kafka,因此Kafka Streams很easy地就实现了端到端的EOS。
Kafka Stream和Kafka的简单对比
Kafka使用图示:
Kafka Stream使用图示:
Kafka使用只需要一个Topic,一个Producer生产消息发送到Topic对应的分片,随后Consumer根据Topic消费对应的消息。
Kafka Strem在Kafka的基础上多了一个Topic相当于两个Kafka使用的顺序集成。多个Producer向第一个Topic发送任务,任务在Kafka Stream中进行流式处理,随后将处理后的消息发送给第二个Topic,Comsumer订阅第二个Topic进行相关任务的消费。
项目中如何使用Kafka Strem实现实时更新热点数据
发送微服务创建KafkaTemplate对象并发送对应数据到相关Topic
@Service
@Transactional
@Slf4j
public class ApLikesBehaviorServiceImpl implements ApLikesBehaviorService {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Override
public ResponseResult like(LikesBehaviorDto dto) {
// 创建kafka发送对象
UpdateArticleMess mess = new UpdateArticleMess();
mess.setArticleId(dto.getArticleId());
mess.setType(UpdateArticleMess.UpdateArticleType.LIKES);
//......
//......
mess.setAdd(1);
kafkaTemplate.send(HotArticleConstants.HOT_ARTICLE_SCORE_TOPIC, JSON.toJSONString(mess));
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
}
接收微服务配置KafkaStreamConfig配置类对象
@Setter
@Getter
@Configuration
@EnableKafkaStreams
@ConfigurationProperties(prefix="kafka")
public class KafkaStreamConfig {
private static final int MAX_MESSAGE_SIZE = 16* 1024 * 1024;
private String hosts;
private String group;
@Bean(name = KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME)
public KafkaStreamsConfiguration defaultKafkaStreamsConfig() {
Map<String, Object> props = new HashMap<>();
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, hosts);
props.put(StreamsConfig.APPLICATION_ID_CONFIG, this.getGroup()+"_stream_aid");
props.put(StreamsConfig.CLIENT_ID_CONFIG, this.getGroup()+"_stream_cid");
props.put(StreamsConfig.RETRIES_CONFIG, 10);
props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
return new KafkaStreamsConfiguration(props);
}
}
需要在配置文件中指定hosts和group
kafka:
hosts: 192.168.200.130:9092
group: ${spring.application.name}
创建流式处理模式对数据进行处理并发送
@Configuration
@Slf4j
public class HotArticleStreamHandler {
@Bean
public KStream<String, String> kStream(StreamsBuilder streamsBuilder){
// 接收消息
KStream<String, String> stream = streamsBuilder.stream(HotArticleConstants.HOT_ARTICLE_SCORE_TOPIC);
// 聚合流式处理
stream.map((key, value) -> {
// 解析JSON字符串获得对象
UpdateArticleMess mess = JSON.parseObject(value, UpdateArticleMess.class);
// 重置消息的key和value key:1234343434 和 value: likes:1
return new KeyValue<String, String>(mess.getArticleId().toString(), mess.getType().name() + ":" + mess.getAdd());
})
// 按照文章Id进行聚合
.groupBy((key, value) -> key)
// //时间窗口
.windowedBy(TimeWindows.of(Duration.ofSeconds(10)))
// 自行进行聚合的运算
.aggregate(new Initializer<String>() {
/**
* 初始方法,返回值消息的value
* @return
*/
@Override
public String apply() {
return "COLLECTION:0,COMMENT:0,LIKES:0,VIEWS:0";
}
/**
* 真正的聚合计算,返回的是消息的value
*/
}, new Aggregator<String, String, String>() {
@Override
public String apply(String key, String value, String aggValue) {
if(StringUtils.isBlank(value)){
return aggValue;
}
String[] aggAry = aggValue.split(",");
int col = 0,com=0,lik=0,vie=0;
for (String agg : aggAry) {
String[] split = agg.split(":");
/**
* 获得初始值,也是时间窗口内计算之后的值
*/
switch (UpdateArticleMess.UpdateArticleType.valueOf(split[0])){
case COLLECTION:
col = Integer.parseInt(split[1]);
break;
case COMMENT:
com = Integer.parseInt(split[1]);
break;
case LIKES:
lik = Integer.parseInt(split[1]);
break;
case VIEWS:
vie = Integer.parseInt(split[1]);
break;
}
}
/**
* 累加操作
*/
String[] valAry = value.split(":");
switch (UpdateArticleMess.UpdateArticleType.valueOf(valAry[0])){
case COLLECTION:
col += Integer.parseInt(valAry[1]);
break;
case COMMENT:
com += Integer.parseInt(valAry[1]);
break;
case LIKES:
lik += Integer.parseInt(valAry[1]);
break;
case VIEWS:
vie += Integer.parseInt(valAry[1]);
break;
}
String formatStr = String.format("COLLECTION:%d,COMMENT:%d,LIKES:%d,VIEWS:%d", col, com, lik, vie);
System.out.println("文章的id:"+key);
System.out.println("当前时间窗口内的消息处理结果:"+formatStr);
return formatStr;
}
}, Materialized.as("hot-atricle-stream-count-001"))
.toStream()
// 发送消息
.map((key,value)->{
return new KeyValue<>(key.key().toString(),formatObj(key.key().toString(),value));
})
.to(HotArticleConstants.HOT_ARTICLE_INCR_HANDLE_TOPIC);
return stream;
}
}
创建KafkaListener监听Stream处理后的数据并执行后续操作
@Component
@Slf4j
public class ArticleIncrHandleListener {
@Autowired
private ApArticleService apArticleService;
@KafkaListener(topics = HotArticleConstants.HOT_ARTICLE_INCR_HANDLE_TOPIC)
public void onMessage(String mess){
if(StringUtils.isNotBlank(mess)){
ArticleVisitStreamMess articleVisitStreamMess = JSON.parseObject(mess, ArticleVisitStreamMess.class);
log.info("监听到的消息为:{}", articleVisitStreamMess);
apArticleService.updateScore(articleVisitStreamMess);
}
}
}