一 为什么要使用消息队列
1.使用同步的通信方法来解决多个服务之间的通信
"/>
同步的通信方式存在性能和稳定的问题
2.使用异步通信
针对于同步通信方法来说,异步可以让上游快速成功,极大提高了系统的吞吐量.而且在分布式系统中,通过下游多个服务的分布式事务的保障,也能保障业务执行之后的最终一致性.
消息队列解决的最终问题就是----通信问题!
二 消息队列的流派
三、Kafka的基本知识
1.Kafka的安装
-
部署一台zookeeper服务器
-
安装jdk
-
下载kafka安装包(http://kafka.apache.org/downloads)
-
部署kafka /usr/local/kafka
进入config目录,修改server.properties(h)
-
进入bin目录.执行命令启动kafka服务器
./kafka-server-start.sh -daemon ../config/server.properties
-
校验kafka是否启动成功 :
进入zk内查看是否有kafka的节点: /brokers/ids/0
在zk的bin目录下 ./zkCli.sh
2.kafka中的一些基本概念
kafka是重topic,而topic是一个逻辑上的概念,目的就是区分消息的类别,不同消息到不同的topic,再到相应的队列!
名称 | 解释 |
---|---|
Broker | 消息处理的中间件节点,一个kafka节点就是一个broker,一个或者多个broker就可以组成一个kafka集群 |
Topic | kafka通过topic对消息进行归类,发布到kafka的每条消息都需要指定一个topic |
Producer | 消息生产者,向broker发送消息的客户端 |
Consumer | 消息消费者,向broker读取消息的客户端 |
3.创建topic
向zookeeper创建topic,replication-factor副本,partitions分区
./kafka-topics.sh --create --zookeeper 10.12.156.160:2181 --replication-factor 1 --partitions 1 --topic test
查看当前zk内有哪些topic
./kafka-topics.sh --list --zookeeper 10.12.156.160:2181
4.发送消息
kafka自带一个producer和consumer,可以从本地文件中读取内容,或者我们也可以在命令行中直接输入内容,并将这些内容以消息的形式发送到kafka集群.默认情况下,每一行都会做成一个独立的消息.
使用kafka的发送消息的客户端,指定发送到的kafka的服务器地址和topic
./kafka-console-producer.sh --broker-list 10.12.156.247:9092 --topic test
5.消费消息
利用自带的consumer,会将获取到的内容在命令行中进行输出,默认是最新的消息.使用kafka的消费者客户端,从指定kafka服务器的指定的topic中消费消息
- 方式一: 从最后一条消息的偏移量+1开始
./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092 --topic test
- 方式二: 从头开始消费
./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092 --from-beginning --topic test
6.关于消息消费的细节
-
生产者将消息发送给broker,broker会将信息保存在本地的日志文件中
/usr/local/kafka/data/kafka-logs/主题-分区/0000000.log
-
消息的保存是有序的, 通过offset偏移量来描述消息的有序性
-
消费者消费消息时也是通过offset来描述当前要消费的那条消息的位置
7.单播消息
在一个kafka的topic中,启动两个消费者,一个生产者,问:生产者发送消息,这条信息是否同时会被两个消费者消费?
如果多个消费者在同一个消费组,那么只有一个消费组可以收到订阅的topic中的信息.换言之,同一个消费组中只能有一个消费者收到一个topic中的信息.
./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092 --consumer-property group.id=testGroup --topic test
8.多播消息
不同的消费者订阅同一个topic,那么不同的消费组中只有一个消费者收到消息.实际上也是多个消费组中的多个消费组收到了同一个消息
消费组1
./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092 --consumer-property gruop.id=testGroup1 --topic test
消费组2
./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092 --consumer-property gruop.id=testGroup2 --topic test
多播和单播的区别
9.查看消费组的详细信息
通过以下命令可以查看到消费组的详细信息
查看消费组列表
./kafka-consumer-groups.sh --bootstrap-server 10.12.156.247:9092 --list
消费组的详细描述
./kafka-consumer-groups.sh --bootstrap-server 10.12.156.247:9092 --describe --group testGroup
重点关注以下几个信息:
- current-offset:最后被消费的消息的偏移量
- log-end-offset:消息总量(最后一次消息的偏移量)
- lag:积压的消息,未被消费的消息
四、主题和分区
1.主题topic
主题topic在kafka中是一个逻辑的概念,kafka通过topic将消息进行分类.不同的topic会被订阅该topic的消费者消费.实现逻辑上对消息的划分.
但是有一个问题,如果说某个topic中的消息非常非常多,多到需要几T来存,因为消息会被保存代log日志文件中.为了解决这这文件过大的问题,kafka提出了Partition分区的概念.
2.分区Partition
2.1分区的概念
通过partition将一个topic中的信息分区来存储.这样的好处有多个:
- 分区存储,可以解决统一存储文件过大的问题
- 提高了读写的吞吐量: 读和写可以并发执行
2.2创建多分区的主题
./kafka-topic.sh --create --zookeeper 10.12.156.160:2181 --replication-factor 1 --partitions 2 --topic test1
3.kafka中消息日志文件中保存的内容
-
0000000.log: 这个文件保存的就是消息
-
_consumer_offsets_49:
kafka内部自己创建了_consumer_offsets主题包含了50个分区(可以通过offsets.topic.num.partitions设置).这个主题用来存放消费者消费某个主题的偏移量.因为每个消费者都会维护着消费的主题的偏移量,也就是说每个消费者都会把消费的主题的偏移量自主上报给kafka中的默认主题:_ __consumer_offset. 因此kafka为了提升这个主题的并发性,默认设置了50个分区.
- 至于提交到那个分区: 通过hash函数: hash(consumerGroupId)%__consumer_offsets计算出
- 提交到主题的内容是: key是consumerGroupId+topic+分区号,value就是当前offset的值
-
文件中保存的消息,默认保存7天.七天后自动删除消息.
五、kafka集群操作
1.搭建kafka集群(三个broker)
-
创建三个server.properties文件
# 0 1 2 broker.id = 2 # 9092 9093 9094 listerners=PLAINTEXT://172.16.253.38:9092 # kafka-logs-1 kafka-logs-2 kafka-logs-3 log.dir=/usr/local/data/kafka-logs-1
-
通过命令启动三台broker
./kafka-server-start.sh -daemon ../config/server0.properties ./kafka-server-start.sh -daemon ../config/server1.properties ./kafka-server-start.sh -daemon ../config/server2.properties
-
校验是否启动成功
进入到zk中查看/brokers/ids中是否有三个znode(0,1,2)
2.副本的概念
在创建主题时,除了指明主题的分区数以外,还指明了副本数,那么副本是一个什么概念呢?
副本是为了给主题中的分区创建多个备份,多个副本在kafka集群的多个broker中,会有一个副本作为leader,其他是follower.
./kafka-topics.sh --create --zookeeper 10.12.156.160:2181 --replication-factor 3 --partitions 2 --topic my-replicated-topic
- leader: kafka的读和写都发生在leader上.leader负责把数据同步给follower.当leader挂了,经过主从选举,从多个 follower中选举产生一个新的leader
- follower:接收leader的同步的数据
- isr: 可以同步和已同步的节点会被存入到isr集合中. 细节:如果isr中的节点性能较差,会被踢出isr集合
此时,broker、主题、分区、副本这些概念梳理完成
集群中有多个broker,创建主题时可以指定主题游多个分区(把消息拆分到不到的分区中存储),可以为分区创建多个副本,不同的副本存放在不同的broker中
3.kafka集群收发消息
发消息
./kafka-console-producer.sh --broker-list 10.12.156.247:9092,10.12.156.247:9093,10.12.156.247:9094 --topic my-replicated-topic
收消息
./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092,10.12.156.247:9093,10.12.156.247:9094 --from-beginning --topic my-replicated-topic
指定消费组消费消息
./kafka-console-consumer.sh --bootstrap-server 10.12.156.247:9092,10.12.156.247:9093,10.12.156.247:9094 --from-begining --consumer-property group.id=testGroup1 --topic my-replicated-topic
关于分区分消费组的集群消费信息
- 一个partition只能被一个消费组的一个消费者消费(保证了局部消息消费的顺序性,也就是说多个partition的多个消费者消费的总的顺序性不能被保证,如何做到总的顺序性?)
- partition的数量决定了消费组中消费者的数量,建议同一个消费组中消费者的数量不超过partition的数量,否则多的消费者消费不到消息
- 如果消费者挂了,那么会出发rebalance机制,会让其他消费者来消费该分区
六、kafka的Java客户端的连接
1.引入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.4.1</version>
</dependency>
2.生成者发送消息的基本实现
public class MySimpleProvider {
private final static String TOPIC_NAME = "my-replicated-topic";
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.设置参数
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.12.156.247:9092,10.12.156.247:9093,10.12.156.247:9094");
//把发送的key从字符串序列化为字节数组
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//把发送的value从字符串序列化为字节数组
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//2.创建生产消息的客户端,传入参数
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
//3.创建消息
//key决定当前消息发送到那个分区(hash(key)%分区数),value就是消息的内容
ProducerRecord<String,String> producerRecord = new ProducerRecord<>(TOPIC_NAME,"MyKey","kafka java client");
//4.发送消息,得到消息发送的数据并输出
RecordMetadata metadata = kafkaProducer.send(producerRecord).get();
System.out.println("同步方式发送结果:"+"topic-"+metadata.topic()+" | partition-"+metadata.partition()
+ " | offset-"+metadata.offset());
}
}
3.创建消息的构造方法
3.1指定发送的分区
ProducerRecord<String,String> producerRecord = new ProducerRecord<>(TOPIC_NAME,0,"MyKey","kafka java client");
4.生产者同步发送消息
如果生产者接收不到消息的确认ack,生产者就会阻塞,阻塞时间是3s.如果还没有收到消息,会进行重试,重试次数是3次.
//4.发送消息,得到消息发送的数据并输出
RecordMetadata metadata = kafkaProducer.send(producerRecord).get();
System.out.println("同步方式发送结果:"+"topic-"+metadata.topic()+" | partition-"+metadata.partition()
+ " | offset-"+metadata.offset());
5.生产者异步发送消息
异步发送,生成者发送完消息后就可以执行之后的业务,broker在收到消息后异步调用生产者提供的callback回调方法
//5.异步发送消息
kafkaProducer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null){
System.out.println("消息发送失败"+exception.getMessage());
}
if (metadata != null){
System.out.println("同步方式发送结果:"+"topic-"+metadata.topic()+" | partition-"+metadata.partition()
+ " | offset-"+metadata.offset());
}
}
});
//如果不设置,则主线程直接结束,callback方法来不及执行
Thread.sleep(100000);
6.生产中的ack配置
在同步发送的前提下,生产者在获得集群返回的ack之前会一致阻塞.那么集群什么时候返回ack呢?此时根据_ack的值来确定:
-
-ack = 0 kafka-cluster不需要任何的broker收到消息,就立即返回ack给生产者,最容易丢消息的,效率是最高的
-
-ack = 1 多副本之间的leader已经收到消息,并把消息写入到本地的log中,才会返回ack给生产者,性能和安全性是最均衡的
-
-ack = -1/all 里面有默认的配置min.insync.replicas=2(默认为1,推荐配置大于等于2),此时就需要leader和一个follower同步完成后,才会返回ack给生产者(此时集群中有两个broker已经完成数据的接受),这种方式最安全,但性能最差.
6.1关于ack和重试(如果没有收到ack,就开始重试)的配置
//ack的类别
properties.put(ProducerConfig.ACKS_CONFIG,"1");
//如果ack失败,则进行重试,重试能保证消息发送的可靠性,但是也可能造成消息的重复发送
//比如网络抖动,所以需要在接受者那边做好消息接收的幂等性
properties.put(ProducerConfig.RETRIES_CONFIG,3);
//重试间隔设置,默认100ms
properties.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG,300);
7.关于消息发送的缓冲区
- kafka默认会创建一个消息缓冲区, 用来存放要发送的消息,缓冲区默认是32m
- kafka本地线程回去缓冲区中 一次拉取16kb的数据,发送到broker
- 如果线程拉不到16kb的数据,间隔10ms也会将已拉到的数据发送到broker
//设置发送消息的本地缓冲区,如果设置了该缓冲区,消息会先将消息发送到本地缓存
//可以提高发送消息的性能,默认是33554432,即32MB
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,3355432);
//kafka本地线程会从缓冲区拉取数据,批量发送到broker
//设置批量发送消息的大小,默认是16384,即16KB,就是说一个batch满了16KB就发出去
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
//是否立即发送
//默认是0,即立即发送,但这样会影响性能
//一般设置10ms左右,就是说这个消息发送完后会进入本地的一个batch,如果10ms内,这个batch满了16kb,就会随batch一起发送出去
//如果10ms内,batch没满,那么也必须把消息发送出去,不能让消息的发送延迟时间太长
properties.put(ProducerConfig.LINGER_MS_CONFIG,10);
七、Java消费者客户端
1.消费者的基本实现
public class MyConsumer {
private final static String TOPIC_NAME = "my-replicated-topic";
private final static String CONSUMER_GROUP_NAME = "testGroup";
public static void main(String[] args) {
Properties prop = new Properties();
prop.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.12.156.247:9092,10.12.156.247:9093,10.12.156.247:9094");
//消费分组名
prop.put(ConsumerConfig.GROUP_ID_CONFIG,CONSUMER_GROUP_NAME);
prop.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
prop.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
KafkaConsumer<String,String> kafkaConsumer = new KafkaConsumer(prop);
//消费者订阅主题列表
kafkaConsumer.subscribe(Arrays.asList(TOPIC_NAME));
while (true){
/**
* poll() 拉取消息的长轮询
*/
ConsumerRecords<String, String> poll = kafkaConsumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : poll) {
System.out.printf("收到消息:partition = %d,offset = %d,key = %s,value = %s%n",record.partition(),record.offset(),record.key(),record.value());
}
}
}
}
2.关于消费者的自动提交和手动提交offset
2.1提交的内容
消费者无论是自动提交还是手动提交,都需要把所属的消费者+消费的主题+消费的某个分区及消费的偏移量,提交到集群的_consumer_offset主题中.
2.2自动提交
消费者poll消息下来之后就会自动提交offset
//手动提交与自动提交,默认自动提交,true
prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");
//自动提交的时间间隔
prop.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");
值得注意的是,自动提交会丢消息.因为消费者在消费前提交offset,有可能提交完后还没消费消费者就挂了.
2.3手动提交
//手动提交与自动提交,默认自动提交,true
prop.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");
手动提交又分为两种:
-
同步提交
在消息消费完后调用同步提交的方法,当集群返回ack前一致阻塞,返回ack后表示提交成功,执行之后的逻辑
while (true){ /** * poll() 拉取消息的长轮询 */ //Duration.ofMillis(1000) 当前长轮询像是一个通道,只维持1s的时间,在这个时间内 //不管是否拉取到500条消息,当时间后都断开通道,开始消费消息 ConsumerRecords<String, String> poll = kafkaConsumer.poll(Duration.ofMillis(1000)); for (ConsumerRecord<String, String> record : poll) { System.out.printf("收到消息:partition = %d,offset = %d,key = %s,value = %s%n",record.partition(),record.offset(),record.key(),record.value()); } if (poll.count() > 0){ //手动同步提交offset,当前线程会阻塞知道offset提交成功 //一般使用同步提交,因为提交之后也没有什么逻辑代码了 kafkaConsumer.commitSync();====阻塞==== 提交成功 }
-
异步提交
在消息消费完之后提交,不需要等待集群ack,直接执行之后的逻辑,可以设置一个回调方法,供集群调用
while (true){ /** * poll() 拉取消息的长轮询 */ ConsumerRecords<String, String> poll = kafkaConsumer.poll(Duration.ofMillis(1000)); for (ConsumerRecord<String, String> record : poll) { System.out.printf("收到消息:partition = %d,offset = %d,key = %s,value = %s%n",record.partition(),record.offset(),record.key(),record.value()); } if (poll.count() > 0){ //手动同步提交offset,当前线程会阻塞知道offset提交成功 //手动异步提交offset,当前线程不会阻塞,可以继续执行后面的代码逻辑 kafkaConsumer.commitAsync(new OffsetCommitCallback() { @Override public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) { if (exception != null){ System.err.println("commit failed for "+offsets); System.err.println("commit failed exception "+exception.getStackTrace()); } } }); }
3.长轮询poll消息
-
默认情况下爱,消费者一次会poll500条消息
//一次poll最大拉取消息的条数,可以根据消费消息的快慢来设置 prop.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,500);
-
代码中设置了长轮询的时间是1s
ConsumerRecords<String, String> poll = kafkaConsumer.poll(Duration.ofMillis(1000));
意味着:
- 如果一次poll到500条,就直接执行for循环
- 如果这一次没有poll到500条,且时间在1s内,那么长轮询继续poll,要么到500条,要么到1s
- 如果多次poll都没有达到500条,且1s时间到了,那么直接执行for循环
-
如果两次poll的间隔超过30s,集群会认为该消费者的消费能力过弱,该消费者会被踢出消费组,触发rebaalance机制,rebalance机制会造成性能开销.可以通过设置这个参数,让一次poll的消息条数少一点
//一次poll最大拉取消息的条数,可以根据消费消息的快慢来设置
prop.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG,500);
//如果两次poll的间隔超过30s,集群会认为该消费者的消费能力过弱,该消费者会被踢出消费组,
// 触发rebaalance机制,rebalance机制会造成性能开销.可以通过设置这个参数,让一次poll的消息条数少一点
prop.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG,30 * 1000);
4.消费者的健康状态检查
消费者每个1s向kafka集群发送心跳,集群发现如果有超过10s没有续约的消费者,将被踢出消费组,触发该消费组的 rebalance机制,将该分区交给消费组里的其他消费者进行消费.
//consumer向broker发送心跳的间隔时间
prop.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG,1000);
//kafka如果超过10s还没有收到消费者的心跳,则会把消费者踢出消费者,进行rebalance,把分区分配给其他消费者
prop.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG,10*1000);
5.指定分区消费
//指定分区消费
kafkaConsumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));
6.消息的回溯消费
就是从当前主题的当前分区,从第一个偏移量开始消费
//消息的回溯消费
kafkaConsumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));
kafkaConsumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));
7.指定offset偏移量进行消费
//指定偏移量进行消费
kafkaConsumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));
kafkaConsumer.seek(new TopicPartition(TOPIC_NAME,0),10);
8.指定时间消费
根据时间,去所有的partition中确定该时间对应的offset,然后去所有的partition中找到对应的该offset之后的消息开始消费
//指定时间消息
//根据时间,去所有的partition中确定该时间对应的offset,
// 然后去所有的partition中找到对应的该offset之后的消息开始消费
List<PartitionInfo> partitionInfos = kafkaConsumer.partitionsFor(TOPIC_NAME);
//从一小时前开始消费
long fetchDataTime = new Date().getTime() - 1000 * 60 * 60;
//获取一小时前的所有offset
Map<TopicPartition,Long> map = new HashMap<>();
for (PartitionInfo partitionInfo : partitionInfos) {
map.put(new TopicPartition(TOPIC_NAME,partitionInfo.partition()),fetchDataTime);
}
//根据时间获得offset后,在根据offset获取到具体的消息
Map<TopicPartition, OffsetAndTimestamp> parMap = kafkaConsumer.offsetsForTimes(map);
for (Map.Entry<TopicPartition, OffsetAndTimestamp> parMapEntry : parMap.entrySet()) {
//消息的键
TopicPartition key = parMapEntry.getKey();
//消息的值
OffsetAndTimestamp value = parMapEntry.getValue();
if (key != null || value != null) continue;
long offset = value.offset();
System.out.println("partition-"+key.partition()+"| offset " + offset);
//根据消息里的Timestamp确定offset
if (value != null){
kafkaConsumer.assign(Arrays.asList(key));
kafkaConsumer.seek(key,offset);
}
}
9.新消费组的消费offset规则
//当消费主题的是一个新的消费组,或者指定offset的消费方式,而offset不存在,那么应该如何消费
//latest(默认): 只消费自己启动后发送到主题的消息
//earliest: 第一次从头开始消费,以后按照消费offset记录继续消费,注意区别seekToBeginning(每次从头开始)
prop.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
八、Springboot使用kafka
1.引入依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
2.配置文件
server:
port: 8080
spring:
kafka:
bootstrap-servers: 10.12.156.247:9093
producer: # 生产者
retries: 3 #重发次数
batch-size: 16384
buffer-memory: 33554432
acks: 1
# 指定消息key和value的编码解码格式
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
key-deserializer: org.apache.kafka.common.serialization.StringSerializer
value-deserializer: org.apache.kafka.common.serialization.StringSerializer
max-poll-records: 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: 124.70.180.216
3.编写消息生产者
@Controller
@RequestMapping("/msg")
public class MyKafkaProducer {
private final static String TOPIC_NAME = "my-replicated-topic";
@Autowired
private KafkaTemplate<String,String> kafkaTemplate;
@RequestMapping("/send")
public String sendMsg(){
kafkaTemplate.send(TOPIC_NAME,0,"key","this is a message");
return "send success";
}
}
4.编写消息消费者
@Component
public class MyKafkaConsumer {
@KafkaListener(topics = "my-replicated-topic",groupId = "MyGroup1")
public void listenerGroup(ConsumerRecord<String,String> record, Acknowledgment ack){
String value = record.value();
System.out.println(value);
System.out.println(record);
//手动提交offset
ack.acknowledge();
}
// @KafkaListener(topics = "my-replicated-topic",groupId = "MyGroup2")
// public void listenerGroup2(ConsumerRecords<String,String> records, Acknowledgment ack){
// for (ConsumerRecord<String, String> record : records) {
// String value = record.value();
// System.out.println(value);
// System.out.println(record);
// //手动提交offset
// ack.acknowledge();
// }
//
// }
}
5.消费者中配置主题,分区和偏移量
@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 listenerGroupPro(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
1.controller
-
集群中谁来充当controller
每个broker启动时会向zk创建一个临时序号节点,获得的序号最小的那个broker将会作为集群中的controller,负责管理整个集群中分区和副本的状态
-
当某个分区的**leader副本出现故障时,**由控制器负责为该分区选举新的leader副本.(ISR的顺序)
-
当检测到某个分区的ISR集合发生变化时(新增或减少broker),由控制器负责通知所有broker更新其元数据信息
-
当使用kafka-topics.sh脚本为某个topic**(增加或减少)分区数量时**,同样还是由控制器负责让新分区被其他节点感知到
2.Rebalance机制
前提是: 消费者没有指定消费的分区. 当消费组消费者和分区的情况发生变化的时候,就会触发rebalance机制
这和机制会重新调整消费者消费哪个分区
在触发rebalance机制之前,消费者消费哪个分区有三种策略:
- range: 通过公式来计算某个分区消费哪个分区 前面的消费者是分区总数/消费者数量 +1,之后的消费者是分区总数/消费者数量
- 轮询: 大家轮流消费
- sticky: 在触发rebalance后,在消费者消费的原分区不变的基础是上进行调整.(在rebalance之前用那种,就继续用那种,如果这个策略没有开,则就要进行全部的重新分配.建议开启)
3.HW和LEO
LEO是某个副本最后消息的消息位置(log-end-offset)
HW是已经完成同步的位置.消息在写入broker时.且每个broker完成这条消息的同步后,HW才会变化.在这之前消费者是消费不到这条消息的.在消息同步完成之后,HW更新之后,消费者才能消费到这条消息,这样的目的是防止消息的丢失(5被消费之后,leader在未完成备份之前,挂掉了,后面发现消费不到消息.因为消费之后offset发生了变化)和重复消费(如 broker1完成备份之后,leader挂了,且新的leader是broker1,所以这样就让消息重复消费了).
十、kafka中的优化问题
1.如何防止消息丢失
- 生产者: 1.使用同步发送. 2.把ack设为1或者all ,并且设置同步分区数 >=2
- 消费者: 把自动提交改为手动提交
2.如何防止消息的重复消费
在防止消息丢失的方案中,如果生产发送完消息后,因为网络抖动,没有收到ack,但实际上broker已经收到了.
此时生产者会进行重试,于是broker就会收到多条相同的消息,而造成消息的重复消费
-
生产者关闭重试: 会造成丢消息(不建议)
-
消费者解决非幂等性消费问题
所谓的幂等性: 多次访问的结果是一样的.对于rest的请求(get(幂等)、post(非幂等)新增、put(幂等)修改、delete(幂等)删除)
- 在数据库中创建联合主键,防止相同的主键 创建出多条记录
- 使用分布式锁,以业务id为锁.确保只有一条记录能够创建成功
3.如何做到消息的顺序消息
- 生产者: 保证消息按顺序发送,且消息不丢失—使用同步发送,ack非0
- 消费者: 主题只能设置一个分区,消费组中只能有一个消费者
kafka的顺序消费使用场景不多,因为牺牲掉了性能,但是比如rocketmq在这一块有专门的功能已经设计好了
4.kafka的消息积压问题
4.1消息积压问题的出现
消息的消费者的消费速度远赶不上生产者的生成消息的速度,导致kafka中有大量的消息没有被消息.随着没有被消费的数据堆积越来越多,消费者寻址的性能会越来越差,最后导致整个kafka对外提供的服务性能很差,从而造成其他服务也访问很慢,造成服务雪崩.
4.2消息积压的解决方案
- 在消费者中,使用多线程,充分利用机器的性能进行消费消息.
- 通过业务的架构设计,提升业务层面消费的性能.
- 创建多个消费组,多个消费者,部署到其他机器上,一起消费,提高消费者的消费速度
- 创建一个消费者,该消费者在kafka另建一个主题,配上多个分区,多个分区再配上多个消费者,该消费者将poll下来的消息,不进行消费,直接转发到新建的主题上.此时,新的主题的多个分区的多个消费者就开始一起消费了.(不常用)
5.延迟队列
5.1应用场景
订单创建后,超过30分钟没有支付,则需要取消订单,这种场景可以通过延时队列来实现
5.2具体方案
- kafka中创建相应的主题
- 消费者消费该主题的消息(轮询)
- 消费者消费消息时判断消息的创建时间和当前时间是否超过30分钟(前提是订单没支付)
- 如果是: 去数据库中修改订单状态为已取消
- 如果否: 记录当前消息的offset,并不在继续消费之后的消息.等待一分钟后,再次向kafka拉取该offset及之后的消息,进行判断,以此往复
十一、kafka-eagle监控平台
1.搭建
-
去kafka eagle官网下载
http://kafka.apache.org
-
分配一台虚拟机
-
虚拟机中安装jdk
-
接压缩kafka-eagle的压缩包
-
给kafka-eagle配置环境变量
vi /etc/profile
export KE_HOME=/usr/local/tools/kafka-eagle/ export PATH=$PATH:$KE_HOME/bin
-
修改system-config.properties配置文件
配置zk
注掉原来的轻量级数据库
配置自己的数据库
-
bin目录下启动
./ke.sh start
kafka.eagle.driver=org.sqlite.JDBC
kafka.eagle.url=jdbc:sqlite:/hadoop/kafka-eagle/db/ke.db
kafka.eagle.username=root
kafka.eagle.password=www.kafka-eagle.org