第四篇章:Springboot中使用Kafka
1.引入依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
2.配置文件:
server:
port: 8080
spring:
kafka:
bootstrap-servers: 192.168.56.10:9092,192.168.56.10:9093,192.168.56.10:9094
producer: # 生产者
retries: 3 # 重试配置:设置大于 0 的值,则客户端会将发送失败的记录重新发送
batch-size: 16384
buffer-memory: 33554432 #★ 消息缓冲区配置
acks: 1 #ack配置
# 指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: default-group
enable-auto-commit: false #手动提交
auto-offset-reset: earliest # 新消费组的消费offset规则
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
max-poll-records: 500 #一次最多拉500条
listener:
# 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
# RECORD
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
# BATCH
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
# TIME
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
# COUNT
# TIME | COUNT 有一个条件满足时提交
# COUNT_TIME
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
# MANUAL
# 手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种
# MANUAL_IMMEDIATE
ack-mode: MANUAL_IMMEDIATE
# redis:
# host: 172.16.253.21
3. Controller撰写
消息生产者:
@RestController
@RequestMapping("/msg")
public class MyKafkaController {
private final static String TOPIC_NAME = "my-replicated-topic";
@Autowired
private KafkaTemplate<String, String> kafkaTemplate; //如果自动注入错误,可能是springboot版本错误,去pom.xml中修改springboot版本,可改成 <version>2.3.7.RELEASE</version>
@RequestMapping("/send")
public String sendMsg(){
kafkaTemplate.send(TOPIC_NAME, 0, "my_key", "this is a message");
return "send success";
}
}
当调用localhost:8080/msg/send 消息就发送了,发送到了my-replicated-topic主题
4.消息消费者:
@KafkaListener(topics = "my-replicated-topic",groupId = "MyGroup1")
public void listenGroup(ConsumerRecord<String, String> record,Acknowledgment ack) { //也可以ConsumerRecords<String, String> records 一次性poll的多条消息进行操作
String value = record.value();
System.out.println(value);
System.out.println(record);
//手动提交offset
ack.acknowledge();
}
设置消费组、多topic、指定分区、指定偏移量消费及设置消费者个数:
@KafkaListener(groupId = "testGroup", topicPartitions = {
@TopicPartition(topic = "topic1", partitions = {"0", "1"}),
@TopicPartition(topic = "topic2", partitions = "0", partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))}
, concurrency = "3")//concurrency就是同组下的消费者个数,就是并发消费数,建议小于等于分区总数
public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
String value = record.value();
System.out.println(value);
System.out.println(record);
//手动提交offset
ack.acknowledge();
}
Kafka集群Controller、Rebalance和HW,LEO:
Controller:
Kafka集群中的broker在zk中创建临时序号节点,序号最小的节点(最先创建的节点)将作为集群的controller,负责管理整个集群中的所有分区和副本的状态:
当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。
检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。
当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责让新分区被其他节点感知到。
Rebalance机制:
前提是:消费者没有指明分区消费。当消费组里消费者和分区的关系发生变化,那么就会触发rebalance机制。
这个机制会重新调整消费者消费哪个分区。
在触发rebalance机制之前,消费者消费哪个分区有三种策略:
range:通过公示来计算某个消费者消费哪个分区
轮询:大家轮着消费
sticky:在触发了rebalance后,在消费者消费的原分区不变的基础上进行调整。
HW,LEO:
LEO(log-end-offset)是某个副本最后消息的位置。
HW(HighWatermark)是已完成同步的位置。leader和follower都会保存有自己的HW。消息写入leader且每个副本都同步完这个消息后,hw才会变化,在这之前消费者是消费不到这条消息的。同步完成之后,HW才会更新,消费者才能消费这个最新消息,这是为了防止消息的重复消费(消费者消费完最新的还没有同步到其它broker的消息后,leader把消息同步给其它副本,然后leader挂了,其它副本成为新的leader,消费者就会重复读取这个最新的消息)
第五篇章:优化相关问题,消息队列内容面试常问问题:
★1.如何防止消息丢失
生产者:使用同步发送,把ack设为1或者all,并且设置同步的分区数>=2;
消费者:设置为手动提交。
★2. 如何防止消息的重复消费
一条消息被消费者消费多次。如果为了消息的不重复消费,而把生产端的重试机制关闭、消费端的手动提交改成自动提交,这样反而会出现消息丢失,那么可以直接在防治消息丢失的手段上再加上消 费消息时的幂等性保证,就能解决消息的重复消费问题。
幂等性如何保证:
mysql 插入业务id作为主键,主键是唯一的,所以一次只能插入一条
使用redis或zk的分布式锁(主流的方案) (使用redis判重:使用redis存放已经消费的消息的order_id,再次消费时直接判断redis是否包含该order_id)
视频图片中老师针对联合主键讲解有误
(联合主键 :联合主键就是用2个或2个以上的字段组成主键。
用这个主键包含的字段作为主键,这个组合在数据表中是唯一,且加了主键索引。
可以这么理解,比如,你的订单表里有很多字段,一般情况只要有个订单号bill_no做主键就可以了,但是,现在要求可能会有补 充订单,使用相同的订单号,
那么这时单独使用订单号就不可以了,因为会有重复。那么你可以再使用个订单序列号bill_seq来 作为区别。把bill_no和bill_seq设成联合主键。
即使bill_no相同,bill_seq不同也是可以的。
商品品牌 商品型号
诺基亚 920
三星 NOTE2
诺基亚 8088
建立方法如下:
create table product( pro_name varchar(20), pro_type varchar(20), primary key (pro_name,pro_type) ); )
★3.如何做到顺序消费RocketMQ
发送方:在发送时将ack不能设置 0 ,使用同步发送(保证消息按顺序发送,且消息不丢失),等到发送成功再发送下一条。确保消息是顺序发送的。
接收方:消息是发送到一个分区中,只能有一个消费组的消费者来接收消息。因此,kafka的顺序消费会牺牲掉性能。
★4. 消息积压问题:
消费积压:消费者的消费速度远小于生产者生产消息的速度,导致kafka中有大量的数据没有被消费。随着没有被消费的数据积累越多,消费者寻址的性能会越来越差,最后导致整个kafka对外提供的 服务性能很差,从而造成其它服务也访问变慢,造成服务雪崩。
解决方案:
在这个消费者中使用多线程,充分利用机器的性能进行消费消息;
通过业务的架构设计,提升业务层面消费的性能;
创建多个消费组,多个消费者,部署到其它机器上,一起消费,提高消费者的消费速度
创建一个消费者,该消费者在kafka另建一个主题,配上多个分区,多个分区再配上多个消费者。该消费者将poll下来的消息,不进行消费,直接转发到新建的主题上,此时,新的主题的多个分区的多个消费者就开始一起消费了-----不常用
★5.延迟队列
场景:订单超过30分钟未支付,自动取消订单。
kafka中创建相应的主题
消费者轮询消费该主题的消息
消费者消费时判断该消息的创建时间和当前时间是否超过30分钟(前提是订单未支付)
如果是,去数据库中修改订单状态未已取消
如果否,记录当前消息的offset,并不再继续消费之后的消息。等待1分钟后,再次向kafka中拉取该offset及之后的消息,继续进行判断,依次反复。
第六篇章:Kafka-eagle监控平台
1.去kafka-eagle官网下载安装包
2.在虚拟机中安装(要有jdk)
3.配置环境变量
vi /etc/profile
#添加下面这两条
export KE_HOME=/usr/local/kafka-eagle
export PATH=$PATH:$KE_HOME/bin
//刷新配置
source /etc/profile
4.修改kafka-eagle内部的配置文件:
vi system-config.properties
修改里面的zk地址和mysql地址
5.进入到bin中,启动
./ke.sh start