一、消息队列
1.解决多个服务之间通信的方法
- 同步
– 网络通信等原因,每个服务之间可能有很大的响应时间和开销
– 整个流程中的服务全部顺利执行完才会成功,用户体验极差 - 异步
– 上游将所需的服务放到消息队列中,下游服务与消息队列采用生产者-消费者模型完成服务
– 上游服务即刻成功并进行反馈,提升系统吞吐量与用户体验
– 即使下游有服务失败,也可以通过分布式事务来解决保证最终一致性
实质:消息队列解决的是通信问题
2.不同消息队列流派间的区别
基本概念
名称 | 解释 |
---|---|
Broker | 消息中间件处理节点,⼀个Kafka节点就是⼀个broker,⼀个或者多个Broker可以组成⼀个Kafka集群 |
Topic | Kafka根据topic对消息进⾏归类,发布到Kafka集群的每条消息都需要指定⼀个topic |
Producer | 消息⽣产者,向Broker发送消息的客户端 |
Consumer | 消息消费者,从Broker读取消息的客户端 |
ConsumerGroup | 每个Consumer属于⼀个特定的Consumer Group,⼀条消息可以被多个不同的Consumer Group消费,但是⼀个Consumer Group中只能有⼀个Consumer能够消费该消息 |
Partition | 物理上的概念,⼀个topic可以分为多个partition,每个partition内部消息是有序的 |
kafka属于有Broker,重Topic
二、kafka的基本使用
安装
官网地址:https://kafka.apache.org/downloads
阿里镜像:http://mirrors.aliyun.com/apache/kafka/
1、下载后解压后创建logs目录
2、修改service.properties
log.dirs=/opt/kafka/data/kafka-logs
listeners=PLAINTEXT://localhost:9092
3、启动zookeeper
zookeeper-server-start.sh -daemon ./config/zookeeper.properties
4、启动kafka
bin/kafka-server-start.sh -daemon ./config/server.properties
5、在zookeeper中查看节点目录可以得知kafka是否启动成功
[zk: localhost:2181(CONNECTED) 0] ls /
[admin, brokers, cluster, config, consumers, controller, controller_epoch, feature, isr_change_notification, latest_producer_id_block, log_dir_event_notification, zookeeper]
启动失败可至kafka/logs/server.log中查看失败原因
创建Topic
- 通过kafka命令向zk中创建一个主题
[root@iZ0jli9zrxt29wd918ru5mZ kafka] bin/kafka-topics.sh --create --topic quickstart-events --bootstrap-server localhost:9092
Created topic quickstart-events.
- zookeeper中查看
[zk: localhost:2181(CONNECTED) 4] ls /
[admin, brokers, cluster, config, consumers, controller, controller_epoch, feature, isr_change_notification, latest_producer_id_block, log_dir_event_notification, zookeeper]
- 通过kafka查看topic的详情
[root@iZ0jli9zrxt29wd918ru5mZ kafka] bin/kafka-topics.sh --describe --topic quickstart-events --bootstrap-server localhost:9092
Topic: quickstart-events TopicId: 7q3jJxRyT_eVbWnFKsAIdQ PartitionCount: 1 ReplicationFactor: 1 Configs: segment.bytes=1073741824
Topic: quickstart-events Partition: 0 Leader: 0 Replicas: Isr: 0
事件
- 生产者将事件写入topic
[root@iZ0jli9zrxt29wd918ru5mZ kafka] bin/kafka-console-producer.sh --topic quickstart-events --bootstrap-server localhost:9092
>This is my first event
>This is my second event
- 消费者消费事件
# --from-beginning从开始读写入的事件(包括连入之前的事件)
[root@iZ0jli9zrxt29wd918ru5mZ bin] ./kafka-console-consumer.sh --topic quickstart-events --from-beginning --bootstrap-server localhost:9092
This is my first event
This is my second event
# 从连入之后产生的事件开始读
[root@iZ0jli9zrxt29wd918ru5mZ bin]# ./kafka-console-consumer.sh --topic quickstart-events --bootstrap-server localhost:9092
三、单播与多播
[root@iZ0jli9zrxt29wd918ru5mZ bin] ./kafka-console-consumer.sh --bootstrap-server localhost:9092 --consumer-property group.id=testGroup --from-beginning --topic quickstart-events
利用“group.id=”来区分是否是同一个组
- 单播
同一个Topic的同一个组,只有一个消费者可以接收到消息 - 多播
同一个Topic的不同组,每组只有一个消费者可以接收到消息
查看消费组的详细信息
[root@iZ0jli9zrxt29wd918ru5mZ bin] ./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group testGroup
Consumer group 'testGroup' has no active members.
GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID
testGroup quickstart-events 0 15 19 4 - - -
- GROUP:组名
- TOPIC:topic组名
- PARTITION:分区
- CURRENT-OFFSET:最后被消费的消息的偏移量
- LOG-END-OFFSET:消息总量
- LAG:挤压消息量
四、kafka中分区与主题的概念
主题:Topic
- 主题是一个逻辑概念,kafka通过topic将消息划分,给不同的消费者。但如果消息量过大,存到log文件中到要用几T才能存下来的时候,为了解决文件过大的问题,kafka提出了分区的概念。
分区:partition
- 通过patition将topic消息分区存储的好处
– 解决统一存储文件过大的问题
– 提供了读写吞吐量,读和写可以同时在多个分区中进行
创建
./kafka-topics.sh --create --zookeeper 172.16.253.35:2181 --replication-factor 1 --partitions 2 --topic test1
kafka消息⽇志⽂件中保存的内容
- 000000000.log中保存的消息
- _consumer_offsets-49:
– kafka中共创建了50个分区,每个消费者都会维护消费的主题的偏移量,也就是说每个消费者会把消费的主题的偏移量⾃主上报给kafka中的默认主题:consumer_offsets。因此kafka为了提升这个主题的并发性,默认设置了50个分区。 - 七天后会被删除
五、kafka集群操作
1.搭建kafka集群(三个broker)
- 创建三个server.properties
# 0 1 2
broker.id=2
# 9092 9093 9094
listeners=PLAINTEXT://192.168.65.60:9094
#kafka-logs kafka-logs-1 kafka-logs-2
log.dir=/usr/local/data/kafka-logs-2
- 启动三台broker
./kafka-server-start.sh -daemon ../config/server.properties
./kafka-server-start.sh -daemon ../config/server1.properties
./kafka-server-start.sh -daemon ../config/server2.properties
- 校验是否成功
ls /brokers/ids
[0, 1, 2]
2.副本
创建带有副本的topic
[root@ecs-219137 bin] ./kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --create --topic my-replicated-topic --partitions 2 --replication-factor 3
Created topic my-replicated-topic.
- –partitions 2 :分区数
- –replication-factor 3:副本数
查看topic
[root@ecs-219137 bin] ./kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --describe --topic my-replicated-topic
Topic: my-replicated-topic TopicId: hfq-8L25T3qqGB41NwjJVw PartitionCount: 2 ReplicationFactor: 3 Configs: segment.bytes=1073741824
Topic: my-replicated-topic Partition: 0 Leader: 2 Replicas: 2,1,0 Isr: 2,1,0
Topic: my-replicated-topic Partition: 1 Leader: 1 Replicas: 1,0,2 Isr: 1,0,2
将消息拆分到两个分区中,每个分区创建三个副本
- 对于分区0来说
– 此时leader是2
– 0和1会从2同步消息 - 对于分区1来说
– 此时leader是1
– 0和2会从1同步消息 - isr:可以同步和已同步的节点
3.集群消费
向集群发送消息
./kafka-console-producer.sh --broker-list 127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 --topic my-replicated-topic
从集群中消费消息(指定消费组)
./kafka-console-consumer.sh --bootstrap-server 127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094 --from-beginning --consumer-property group.id=testGroup1 --topic my-replicated-topic
ps:不允许自动创建未创建的topic,如果报错检查是否已创建topic
消费组中的细节
- 一个partition只能由一个consumer来消费
- 一个group中的consumer数量最好不要超过partition数量,否则多余的consumer消费不到消息
- 如果消费者挂了,那么会触发rebalance机制,会让其他消费者来消费该分
区
六、java客户端实现kafka生产者
引入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.1.0</version>
</dependency>
生产者发消息的基本实现
public class MySimpleProducer {
private final static String TOPIC_NAME = "my-replicated-topic";
public static void main(String[] args) throws ExecutionException,
InterruptedException {
//1.设置参数
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
//把发送的key从字符串序列化为字节数组
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//把发送消息value从字符串序列化为字节数组
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//2.创建⽣产消息的客户端,传⼊参数
Producer<String, String> producer = new KafkaProducer<String, String>(props);
//3.创建消息
//key:作⽤是决定了往哪个分区上发,value:具体要发送的消息内容
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, 0, "mykeyvalue", "hellokafka");
/*//4.发送消息,得到消息发送的元数据并输出
RecordMetadata metadata = producer.send(producerRecord).get();
System.out.println("同步⽅式发送消息结果:" + "topic-" + metadata.topic() + "|partition-" + metadata.partition() + "|offset-" + metadata.offset());*/
//异步回调⽅式发送消息
producer.send(producerRecord, new Callback() {
public void onCompletion(RecordMetadata metadata, Exception
exception) {
if (exception != null) {
System.err.println("发送消息失败:" + exception.getStackTrace());
}
if (metadata != null) {
System.out.println("异步⽅式发送消息结果:" + "topic-" + metadata.topic() + "|partition-" + metadata.partition() + "|offset-" + metadata.offset());
}
}
});
Thread.sleep(10000000L);
}
}
生产者同步发送
如果⽣产者发送消息没有收到ack,⽣产者会阻塞,阻塞到3s的时间,如果还没有收到消息,
会进⾏重试。重试的次数3次。
RecordMetadata metadata = producer.send(producerRecord).get();
System.out.println("同步⽅式发送消息结果:" + "topic-" + metadata.topic() + "|partition-" + metadata.partition() + "|offset-" + metadata.offset());
生产者异步发送
异步发送,⽣产者发送完消息后就可以执⾏之后的业务,broker在收到消息后异步调⽤⽣产
者提供的callback回调⽅法。
producer.send(producerRecord, new Callback() {
public void onCompletion(RecordMetadata metadata, Exception
exception) {
if (exception != null) {
System.err.println("发送消息失败:" +
exception.getStackTrace());
}
if (metadata != null) {
System.out.println("异步⽅式发送消息结果:" + "topic-" +
metadata.topic() + "|partition-" + metadata.partition() + "|offset-" + metadata.offset());
}
}
});
//休眠看到callback的发生
Thread.sleep(10000000L);
生产者中的ack配置
同步发送的前提下,生产者在获得集群返回的ack前会一直阻塞
ack的配置:
- ack=0
– kafka集群不需要任何的broker收到消息,就⽴即返回ack给⽣产者
– 最容易丢消息,效率最高 - ack=1(默认)
– 多副本之间的leader已经收到消息,并把消息写⼊到本地的log中,才会返回ack给⽣产者
– 性能和安全性是最均衡的 - ack=-1/all
– ⾥⾯有默认的配置min.insync.replicas=2(默认为1,推荐配置⼤于等于2),此时就需要leader和⼀个follower同步完后,才会返回ack给⽣产者(此时集群中有2个broker已完成数据的接收)
– 最安全,但性能最差。
props.put(ProducerConfig.ACKS_CONFIG, "1");
/*发送失败会重试,默认重试间隔100ms,重试能保证消息发送的可靠性,但是也可能造
成消息重复发送,⽐如⽹络抖动,所以需要在
接收者那边做好消息接收的幂等性处理*/
props.put(ProducerConfig.RETRIES_CONFIG, 3);
//重试间隔设置
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);
//kafka默认会创建⼀个消息缓冲区,⽤来存放要发送的消息,缓冲区是32m
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
//kafka本地线程会去缓冲区中⼀次拉16k的数据,发送到broker
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
//如果线程拉不到16k的数据,间隔10ms也会将已拉到的数据发到broker
props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
七、java客户端实现kafka消费者
实现
public class MySimpleConsumer {
private final static String TOPIC_NAME = "my-replicated-topic";
private final static String CONSUMER_GROUP_NAME = "testGroup";
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
// 消费分组名
props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
//1.创建⼀个消费者的客户端
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
//2. 消费者订阅主题列表
consumer.subscribe(Arrays.asList(TOPIC_NAME));
while (true) {
/*
* 3.poll() API 是拉取消息的⻓轮询
*/
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
//4.打印消息
System.out.printf("收到消息:partition = %d,offset = %d, key = %s, value = %s %n ", record.partition(), record.offset(), record.key(), record.value());
}
}
}
}
提交offset
- 提交的内容
消费者⽆论是⾃动提交还是⼿动提交,都需要把所属的消费组+消费的某个主题+消费的某个
分区及消费的偏移量,这样的信息提交到集群的_consumer_offsets主题⾥⾯。 - 自动提交
消费者poll下来之后就会自动提交offset
// 是否⾃动提交offset,默认就是true
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
// ⾃动提交offset的间隔时间
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
如果poll下来之后消费者挂了会产生丢消息的情况
- 手动提交
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
– 手动同步提交
在消费完消息后调⽤同步提交的⽅法,当集群返回ack前⼀直阻塞,返回ack后表示提交
成功,执⾏之后的逻辑
while (true) {
/*
* poll() API 是拉取消息的⻓轮询
*/
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息:partition = %d,offset = %d, key = %s, value = %s%n ", record.partition(), record.offset(), record.key(), record.value());
}
//所有的消息已消费完
if (records.count() > 0) {//有消息
// ⼿动同步提交offset,当前线程会阻塞直到offset提交成功
// ⼀般使⽤同步提交,因为提交之后⼀般也没有什么逻辑代码了
consumer.commitSync();//=======阻塞=== 提交成功
}
}
– 手动异步提交
在消息消费完后提交,不需要等到集群ack,直接执⾏之后的逻辑,可以设置⼀个回调⽅
法,供集群调⽤
while (true) {
/*
* poll() API 是拉取消息的⻓轮询
*/
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息:partition = %d,offset = %d, key = %s, value = %s %n", record.partition(), record.offset(), record.key(), record.value());
}
//所有的消息已消费完
if (records.count() > 0) {
// ⼿动异步提交offset,当前线程提交offset不会阻塞,可以继续处理后⾯的程序逻辑
consumer.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());
}
}
});
}
}
长轮询poll消息
默认情况下消费者一次会poll500条消息
//⼀次poll最⼤拉取消息的条数,可以根据消费速度的快慢来设置
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
长轮询时间为1000ms
while (true) {
/*
* poll() API 是拉取消息的⻓轮询
*/
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息:partition = %d,offset = %d, key = %s, value = %s %n", record.partition(), record.offset(), record.key(), record.value());
}
- 如果一次poll500条消息就会进行for循环
- 如果1s内没有poll到500条消息(可累加)也会进行for循环
- 如果两次poll的间隔超过30s,集群会认为该消费者的消费能⼒过弱,该消费者被踢出消
费组,触发rebalance机制,rebalance机制会造成性能开销。可以通过设置这个参数,
减少⼀次poll的消息的数量
//⼀次poll最⼤拉取消息的条数,可以根据消费速度的快慢来设置
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
//如果两次poll的时间如果超出了30s的时间间隔,kafka会认为其消费能⼒过弱,将其踢出消费组。将分区分配给其他消费者。-rebalance
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
消费者健康状态检查
消费者每隔1s向kafka集群发送⼼跳,集群发现如果有超过10s没有续约的消费者,将被踢出
消费组,触发该消费组的rebalance机制,将该分区交给消费组⾥的其他消费者进⾏消费。
//consumer给broker发送⼼跳的间隔时间
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
//kafka如果超过10秒没有收到消费者的⼼跳,则会把消费者踢出消费组,进⾏rebalance,把分区分配给其他消费者。
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);
其他配置
//指定分区消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
//从头消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));
//指定offset消费
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
consumer.seek(new TopicPartition(TOPIC_NAME, 0), 10);
指定时间消费
List<PartitionInfo> topicPartitions = consumer.partitionsFor(TOPIC_NAME);
//从1⼩时前开始消费
long fetchDataTime = new Date().getTime() - 1000 * 60 * 60;
Map<TopicPartition, Long> map = new HashMap<>();
for (PartitionInfo par : topicPartitions) {
map.put(new TopicPartition(TOPIC_NAME, par.partition()), fetchDataTime);
}
Map<TopicPartition, OffsetAndTimestamp> parMap = consumer.offsetsForTimes(map);
for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : parMap.entrySet()) {
TopicPartition key = entry.getKey();
OffsetAndTimestamp value = entry.getValue();
if (key == null || value == null) continue;
Long offset = value.offset();
System.out.println("partition-" + key.partition() + "|offset-" + offset);
System.out.println();
//根据消费⾥的timestamp确定offset
if (value != null) {
consumer.assign(Arrays.asList(key));
consumer.seek(key, offset);
}
}
根据时间,去所有的partition中确定该时间对应的offset,然后去所有的partition中找到该
offset之后的消息开始消费。
新消费组的消费规则
新消费组中的消费者在启动以后,默认会从当前分区的最后⼀条消息的offset+1开始消费(消
费新消息)。
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
- latest:默认从最后一条开始消费
- earliest:第一次从头开始消费,之后都从自己最新的开始消费
八、SpringBoot整合kafka
引入依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
编写配置文件
spring:
kafka:
bootstrap-servers: 127.0.0.1:9092
producer: # ⽣产者
# 设置⼤于0的值,则客户端会将发送失败的记录重新发送
retries: 3
#kafka本地线程会去缓冲区中⼀次拉16k的数据,发送到broker
batch-size: 16384
#kafka默认会创建⼀个消息缓冲区,⽤来存放要发送的消息,缓冲区是32m
buffer-memory: 33554432 #32M
#leader收到消息,并把消息写⼊到本地的log中,返回ack给⽣产者
acks: 1
# 指定消息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
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
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
- auto-offset-reset
– latest:从当前offset开始消费
– earliest:只有第一次从最开始进行消费,之后都从当前offset开始消费(有时会造成消息的重复提醒,不重启服务器的话没有问题)
编写生产者
@RestController
@RequestMapping("/msg")
public class MyKafkaProducer {
private static final String TOPIC_NAME = "my-replicated-topic";
@Autowired
private KafkaTemplate kafkaTemplate;
@RequestMapping("/send")
public String sendMsg() {
kafkaTemplate.send(TOPIC_NAME, 0, "key", "this is a message");
return "success!";
}
}
编写消费者
@Component
public class MyKafkaConsumer {
@KafkaListener(topics = "my-replicated-topic", groupId = "MyGroup1")
public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
String value = record.value();
System.out.println(value);
System.out.println(record);
//⼿动提交offset
ack.acknowledge();
}
}
消费者中的配置
acknowledge的配置:
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
配置分组,分区,偏移量
@Component
public class MyKafkaConsumer {
@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
controller
- 每个集群创建时会像zk创建一个临时序号节点,序号最小的broker会作为集群的broker
- controller功能
– 重新选举leader:选举isr集合中最左面的follower
– 当集群中的broker或partition有变动,同步给其他broker
rebalance机制
- 前提:消费组中消费者没有指明partition
- 触发条件:消费者和分区的关系出现变化(消费者宕机)
- 分配策略
– range:分区数/消费者数
7个分区3个消费者,则每个消费者分别分到3/2/2个分区
– 轮询:轮着给每个消费者指定partition
– sticky:粘合策略
如果需要rebalance会在之前以分配好的基础上将需要分配的partition重新分配,如果不开启会将所有分区重新分配,影响性能。
HW和LEO
- leo:某个副本最后消息的消息位置(log-end-offset)
- HW:已同步完的位置
消息在写入broker时,如果有副本需要往副本同步,若未完成同步则消费者是无法消费leader中最新的这条消息的,如果同步完成之后消费者则可以消费,已完成同步的消息的位置就称之为HW(水位线),目的是防止消息丢失(未完成同步时leader宕机)
十、kafka线上的优化问题
如何防止消息丢失
- 生产者
– 使用同步发送
– ack值设为1或者all
– 设置同步分区数>=2 - 消费者
– 把自动提交改为手动提交
如何防止重复消费
- 原因:生产者将消息发送给broker后由于网络抖动生产者没有收到broker发送的ack确认,此时生产者重新发送消息,造成消费者的重复消费
- 解决方案
– 生产者关闭重试(不建议,会造成消息丢失)
– 消费者解决非幂等消费问题
(幂等性:多次访问的结果是一样的。对于rest请求,get,put,del是幂等,post是非幂等)
在数据库中创建联合索引,防止创造出多条记录
使用分布式锁,以业务id为锁,保证只有一条记录能够成功创建
如何顺序消费
- 生产者:使用同步发送,ack设置为非0
- 消费者:一个topic只能由一个patition,一个group中只能由一个consumer
- kafka对顺序消费没有功能支持,建议使用rocketmq
消息挤压怎么解决
- 原因:消费者消费的速度太慢赶不上生产消息的速度,大量未消费的消息堆积,从而对消费者的寻址造成影响,导致性能极差,可能影响其他服务的访问速度,造成雪崩
- 解决办法:
– 使用多线程,充分利用机器性能
– 优化代码和架构,提升消费者消费能力
– 创建爱你多个消费组,部署到其他机器上
– 创建⼀个消费者,该消费者在kafka另建⼀个主题,配上多个分区,多个分区再配上多个
消费者。该消费者将poll下来的消息,不进⾏消费,直接转发到新建的主题上。此时,新
的主题的多个分区的多个消费者就开始⼀起消费了。——不常⽤
延时队列的实现
- 场景:订单30分钟后自动取消
- 方案:
– 去kafka中创建响应主题
– 消费者轮询消费该主题
– 消费者判断是否超时
超时则改变订单状态
未超时则记录当前offset,并不再继续消费之后的消息,等待一个固定时间后在此拉取该offset及之后的消息。进行判断