总结kafka

1. 基本概念

名称解释
broker消息中间件处理节点,一个kafka节点就是一个broker,一个或多个broker可以组成一个kafka集群
topickafka根据topic对消息进行归类,发布到kafka集群的每条消息都需要指定一个topic
producer消息生产者,向broker发送消息的客户端
consumer消息消费者,从broker读取消息的客户端
consumer group每个consumer属于一个特定的consumer group,一条消息可以被多个不同的consumer group消费,但是一个consumer group中只有一个consumer能够消费该消息
partition物理上的概念,一个topic可以分为多个partition,每个partition内部消息是有序的

2.基本配置信息、执行命令

2.1 配置文件

config目录下server.properties文件:

  • broker.id kafka集群下 每个broker id是不一样的
  • listeners 放服务器主机地址,后面的9092是kafka提供对外服务的默认端口
  • log.dirs 消息存储文件存放地址
  • zookeeper.connect zk地址

2.2 启动kafka

启动kafka,进入bin目录下:

./kafka-server-start.sh -daemon ../config/server.properties

可以进入zk看节点是否成功启动

ls /brokers/ids

2.3 创建topic

执行以下命令创建名为"test"的topic,这个topic只有一个partition,并且备份因子也设置为1:

./kafka-topics.sh --create --zookeeper (zk地址):2181 --replication-factor 1 --partitions 1 --topic test

–replication-factor 1表示一个副本 --partitions 1表示一个分区

查看当前kafka内有哪些topic:

./kafka-topics.sh --list --zookeeper (zk地址):2181

2.4 发送消息

发送消息 命令行模式(存在log.dirs中topic开头文件夹里面的.log
文件中),生产者将消息发送给broker,broker会将消息保存在本地的日志文件中,消息的保存是有序的,通过offset偏移量来描述消息的有序性:

./kafka-console-producer.sh --broker-list (kafka地址):9092 --topic test

2.5 消费消息

从最后一条消息的偏移量+1开始消费,其实就是不消费历史消息,只消费最新的,即消费者消费消息时也是通过offset来描述当前要消费的那条消息位置:

./kafka-console-consumer.sh --bootstrap-server (kafka地址):9092 --topic test

从头开始消费:

./kafka-console-consumer.sh --bootstrap-server (kafka地址):9092 --from-beginning --topic test

2.6 单播消息

如果多个消费者在同一个消费组,那么只有一个消费者可以收到订阅的同一个topic中的消息,即同一个消费组中只能有一个消费者收到一个topic中的消息,并且一直是这个消费者,不会来回切换;

./kafka-console-consumer.sh --bootstrap-server (kafka地址):9092 --consumer-property group.id=testGroup --topic test

2.7 多播消息

不同的消费组订阅同一个topic,那么不同的消费组中只有一个消费者能收到消息(单播),但是多个消费组中的消费者收到了同一个消息;
单播放保证的是同一个消费者组只有一个消费者可以消费消息,如果需要还想获取该消息,需要再起一个消费者组去消费;

2.8 查看消费组及信息

查看当前主题下有哪些消费者组

./kafka-consumer-groups.sh --bootstrap-server (kafka地址):9092 --list

查看消费组的详细信息:

./kafka-consumer-groups.sh --bootstrap-server (kafka地址):9092 --describe --group testGroup

在这里插入图片描述
current-offset:最后被消费的消息的偏移量,即最后消费到哪里了
log-end-offset:消息总量,即最后一条消息的偏移量
lag:积压了多少条消息,未被消费的量

3.主题、分区的概念

3.1 主题topic

类别名称,逻辑划分,kafka通过topic对消息进行分类,不同的topic会被订阅该topic的消费者消费。
但是有一个问题,如果说这个topic中的消息非常多,需要几个T来保存,因为消息是会被保存到log日志文件中的,为了解决文件过大的问题,kafka提出了partition分区的概念;

3.2 partition分区

一个主题中的消息量是非常大的,因此可以通过分区的设置,来分布式存储这些消息,比如一个topic创建了3个分区,那么topic中的消息就会分别存放在这三个分区中;
在这里插入图片描述

  • 分区存储,可以解决统一存储文件过大问题
  • 提高了读写的吞吐量,即读和写可以同时在多个分区中进行

3.2.1 创建多分区的主题

为一个主题创建分区:

./kafka-topics.sh --create --zookeeper (zk地址):2181 --replication-factor 1 --partitions 2 --topic test1

查看topic的分区信息

./kafka-topics.sh --describe --zookeeper (zk地址):2181 --topic test1

3.3 kafka中消息日志文件中保存的内容

  • 00000.log:这个文件中保存的就是消息
  • _consumer_offsets-49:kafka内部自己创建了_consumer_offsets主题包含了50个分区,这个主题用来存放消费者消费某个主题的偏移量;因为每个消费者都会自己维护消费的主题的偏移量,也就是说每个消费者会把消费的主题的偏移量自主上报给kafka中的默认主题_consumer_offsets,即某一个消费者挂了,启用另一个消费者也知道从何处继续接着消费;kafka为了提升这个主题的并发性,默认设置50个分区,这个可以设置。
  • 提交到那个分区,通过hash函数:hash(consumerGroupId)%_consumer_offsets主题的分区数
  • 提交到该主题中的内容是:key是consumerGroupId+topic+分区号,value就是当前offset的值
  • 文件中保存的消息默认保存7天,7天后被删除

4.kafka集群、副本

4.1 搭建

以一台服务器为例,创建多个配置文件,修改broker.id,listeners,log.dir;

4.2 副本的概念

副本是对分区的备份,在集群中,不同的副本会被部署在不同的broker上,多个副本在kafka集群的多个broker中,会有一个副本作为leader,其他是follower,只有一个节点创建副本没有意义。
创建一个主题,2个分区,3个副本:

./kafka-topic.sh --create --zookeeper (zk地址):2181 --replication-factor 3 --partitions 2 --topic my-replicated-topic

在这里插入图片描述
leader的作用是读和写,副本是leader会将数据同步过来;
生产者向两个leader发消息,消费者消费两个leader;
在这里插入图片描述

  • isr:可以同步和已经同步的节点会被存入到isr集合中,如果isr中节点性能较差,会被踢出isr集合;

集群中有多个broker,创建主题时可以指明主题有多少个分区,把消息拆分到不同分区中存储,可以为分区创建多个副本,不同的副本存放在不同的broker里;

4.3 kafka集群消息的发送

./kafka-console-producer.sh --broker-list (节点1),(节点2),(节点3) --topic my-replicated-topic

4.4 kafka集群消息的消费

./kafka-console-consumer.sh --bootstrap-server (节点1),(节点2),(节点3) --from-beginning --topic my-replicated-topic

消费时带上消费者组

./kafka-console-consumer.sh --bootstrap-server (节点1),(节点2),(节点3) --from-beginning --consumer-property group.id=testGroup --topic my-replicated-topic

使用消费组组消费,通过设置消费组组,当前消费者组不会消费其他消费组组的消息

./kafka-console-consumer.sh --bootstrap-server 地址 --topic test_topic --group test2

在这里插入图片描述
向topic内发送3条消息,可见 test1消费了消息,无挤压,test2没有消费消息,消息挤压了。

在这里插入图片描述
两个broker,每个broker中有多个partition,一个partition只能被一个消费组里的某一个消费者消费,从而保证消费顺序,kafka只在patition范围内保证消息消费的局部顺序性,不能在同一个topic中的多个partition中保证总的消费顺序性,一个消费者可以消费多个partition;
消费组中的消费者的数量不能比一个topic中的partition数量多,否则多出来的消费者消费不到消息,partition的数量决定了消费组中消费者的数量,建议同一个消费组中消费者的数量不要超过partition的数量,否则多的消费者消费不到消息,如果消费者挂了,那么会触发rebalance机制,会让其他消费者来消费;

5 kafka的Java客户端 生产者基本实现

5.1 基本实现

package kafka;

import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class MyProducer {

    public final static String TOPIC_NAME = "my_test_topic";

    public static void main(String[] args) throws InterruptedException {
        Properties props = new Properties();
        //集群用逗号隔开
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "172.16.253.21:9093,172.16.253.21:9094");
        /**
         *前提是同步发送:发出消息的,持久化机制参数
         * acks=0,表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息,性能最高,但是容易丢消息
         * ack=1,表示至少要等待leader已经成功将数据写入到本地log,但是不需要等待所有follower是否写入成功,就可以继续发送下一条消息了,
         * 这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,数据会丢失
         * ack=-1或all,需要等待min,insync.replicas(默认为1,为1其实和ack=1一样,为2则表示leader和一个follower同步完成之后,返回返回ack给生产者,
         * 表示有多少个partition已经收到并写入了消息)这个参数配置的副本个数都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据,
         * 这是最强的数据保证,一般除非是金融级别等场景才需要使用这种配置;
         */
        props.put(ProducerConfig.ACKS_CONFIG, "1");
        /**
         *没有收到ack:
         * 发送失败会重试,默认重试间隔100ms,重试能保证消息发送的可靠性,但是也可能造成消息的重复发送,比如网络抖动,所以需要在接受者这边做好
         * 消息接收的幂等性处理
         */
        props.put(ProducerConfig.RETRIES_CONFIG, 3);
        //重试间隔设置
        props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);
        //设置发送消息的本地缓冲区,如果设置了改缓冲区,消息会先发送到本地缓冲区,可以提高消息发送的性能,默认值是33554432,即32MB
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
        /**
         *kafka本地线程会从缓冲区取数据,批量发送到broker,设置批量发送消息的大小,默认是16384,即16kb,
         * 就是说一个branch满了16kb就发送出去
         */
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        /**
         *默认值是0,意思就是消息必须立刻被发送,但这样会影响性能,一般设置10ms左右,即消息发送完后会进入本地的一个batch,如果10ms内,
         * 这个batch满了16kb就会随batch一起被发送出去,没满,也会发送出去,不能让消息发送延迟时间太长
         */
        props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
        //把发送的key从字符串序列化为字节数组
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //把发送的value从字符串序列化为字节数组
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //发消息的客户端
        Producer<String, String> producer = new KafkaProducer<>(props);
        int msgNum = 5;
        final CountDownLatch countDownLatch = new CountDownLatch(msgNum);
        for (int i = 0; i < 10000000000l; i++) {
            Order order = new Order((long) i, i);
            //指定发送分区
//            ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, 0,
//                    order.getOrderId().toString(), JSON.toJSONString(order));
            //未指定发送分区,具体发送的分区计算公式,hash(key)%partitionNum,order.getOrderId().toString()是key,
            // key的作用是指明往哪个分区发

            ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME,
                    order.getOrderId().toString(), JSON.toJSONString(order));

            //同步发送消息
            try {
                RecordMetadata recordMetadata = producer.send(producerRecord).get();
                //阻塞
                System.out.println("topic" + recordMetadata.topic());
                System.out.println("partition" + recordMetadata.partition());
                System.out.println("offset" + recordMetadata.offset());
            } catch (InterruptedException e) {
                e.printStackTrace();
                //日志  人工处理等
            } catch (ExecutionException e) {
                throw new RuntimeException(e);
            }

            //异步回调发送发送消息
            producer.send(producerRecord, new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (Objects.nonNull(e)) {
                        System.out.println("消息发送失败");
                    }
                    if (Objects.nonNull(recordMetadata)) {
                        System.out.println("topic" + recordMetadata.topic());
                        System.out.println("partition" + recordMetadata.partition());
                        System.out.println("offset" + recordMetadata.offset());
                    }
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.close();
    }
}

@Data
@AllArgsConstructor
class Order {
    private Long orderId;
    private int count;
}

5.2 同步发送

如果生产者发送消息没有收到ack,生产者会阻塞,阻塞3s,如果还没有收到ack,会进行重试,重试的次数3次;

5.2 异步发送

异步发送,生产者发送完消息后就可以执行之后的业务,broker在收到消息后异步调用生产者提供的callback回调方法。

5.3 参数部分

在这里插入图片描述

6 Java客户端 消费者的实现细节

6.1 消费者的基本实现

package kafka;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;

public class MyConsumer {

    public final static String TOPIC_NAME = "my_test_topic";
    public final static String CONSUMER_GROUP_NAME = "testGroup";

    public static void main(String[] args) {
        Properties props = new Properties();
        //集群用逗号隔开
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "172.16.253.21:9093,172.16.253.21:9094");
        //消费分组名
        props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
        //是否自动提交offset,默认就是true
//        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
        //自动提交offset时间间隔
        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        //手动提交
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        /**
         * 当消费主题的是一个新的消费组,或者指定offset的消费发送,offset不存在,那么应该如何消费
         * latest(默认):只消费自己启动之后发送到主题的消息
         * earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer.seekToBeginning(每次都开始从头消费)
         */
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        //consumer给broker发送心跳的时间间隔
        props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
        //kafka如果超过10秒没有收到消费者的心跳,则会把消费者踢出消费组,进行rebalance,把分区分配给其他消费者
        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);
        //一次poll最大拉取消息的条数,可以根据消费速度的快慢来设置
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
        //如果两次poll的时间超出了30s的时间间隔,kafka会认为其消费能力过弱,将其踢出消费组,将分区分配给其他消费者
        props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);

        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        //消费者订阅主题列表,这里可以订阅多个主题
        consumer.subscribe(Arrays.asList(TOPIC_NAME));
        //指定分区消费
//        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> partitionInfos = consumer.partitionsFor(TOPIC_NAME);
        //从一小时前开始消费
        long fetchDateTime = new Date().getTime() - 1000 * 60 * 60;
        HashMap<TopicPartition, Long> map = Maps.newHashMap();
        for (PartitionInfo par : partitionInfos) {
            map.put(new TopicPartition(TOPIC_NAME, par.partition()), fetchDateTime);
        }
        //根据时间拿到偏移量
        Map<TopicPartition, OffsetAndTimestamp> parMap = consumer.offsetsForTimes(map);
        for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : parMap.entrySet()) {
            //partition
            TopicPartition key = entry.getKey();
//            offset
            OffsetAndTimestamp value = entry.getValue();
            if (Objects.isNull(key) || Objects.isNull(value)) {
                continue;
            }
            long offset = value.offset();
            if (Objects.nonNull(value)) {
                consumer.assign(Arrays.asList(key));
                consumer.seek(key, offset);
            }
        }
        
        while (true) {
            //poll() API 是拉取消息的长轮询
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
            for (ConsumerRecord<String, String> record : records) {
                System.out.println(record.offset());
                System.out.println(record.partition());
                System.out.println(record.key());
                System.out.println(record.value());
            }
            
            //所有的消息已消费完
            if (records.count() > 0) {   //有消息
                //手动提交offset,当前线程会阻塞直到offset提交成功,一般使用同步提交,因为提交之后也没有什么逻辑代码了
                //方法1  手动同步提交 这里会阻塞 直到broker返回ack
//                consumer.commitSync();
                //方法2 异步提交offset 不会阻塞 kafka集群调用callback方法
                consumer.commitAsync(new OffsetCommitCallback() {
                    @Override
                    public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                        if (Objects.nonNull(e)){
                            System.out.println(map);
                        }
                    }
                });
            }
        }
    }
}

6.2 消费者的自动提交和手动提交offset

消费者无论是自动提交还是手动提交,都需要把所属的消费组+消费的某个主题+消费的某个分组及消费的偏移量,这样的信息提交到集群的_consumer_offsets主题里面。
自动提交:消费者poll消息下来以后就会自动提交offset,注意:自动提交会丢消息,因为消费者在消费前提交offset,也可能提交完后还没有消费时消费者挂了;
手动提交:同步:在消费完消息后调用同步提交的方法,当集群返回ack前一直阻塞,返回ack后表示提交成功,执行之后的逻辑;
异步:在消息消费完之后提交,不需要等到集群ack,直接执行之后的逻辑,可以设置一个回调方法,供给集群调用;

6.3 长轮询poll消息

默认情况下,消费者一次会poll 500条消息,代码中设置了长轮询的时间是1000毫秒,意味着:1.如果一次poll到500条,就直接执行for循环;2.如果这一次没有poll到500条,且时间在1s内,那么长轮询继续poll,要么到500条,要么到1s;3.如果多次poll都没达到500条,且1s时间达到了,那么直接执行for循环;
如果两次poll的时间间隔超过30s,集群会认为该消费者的消费能力过弱,该消费者被踢出消费组,触发rebalance机制,rebalance机制会造成性能开销,可以通过设置这个参数,让一次poll的消息条数少一点。

6.4 消费者的健康状态检查

消费者每隔1s向kafka集群发送心跳,集群发现如果有超过10s没有续约的消费者,将被踢出消费组,触发该消费者的rebalance机制,将该分区交给消费组里的其他消费者进行消费;

7 spring boot中使用kafka

pom依赖

        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>2.8.0</version>
        </dependency>

consumer

@Component
public class consumer {
    @KafkaListener(topics = "my_test_topic", groupId = "group1")
    public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        //手动提交offset  不手动提交消息会被重复消费
        ack.acknowledge();
    }

    @KafkaListener(topics = "my_test_topic", groupId = "group2")
    public void listenGroups(ConsumerRecords<String, String> records, Acknowledgment ack) {
        //需要对records进行遍历

        //手动提交offset
        ack.acknowledge();
    }

    @KafkaListener(groupId = "group3", topicPartitions = {
            @TopicPartition(topic = "topic1", partitions = {"0", "1"}),
            @TopicPartition(topic = "topic2", partitions = "0",
                    partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))  //消费1号分区从100偏移量开始
    }, concurrency = "3")  //concurrency就是同组下的消费者个数,就是并发消费数,建议小于等于分区总数
    public void listenGroup1(ConsumerRecords<String, String> records, Acknowledgment ack) {
        //需要对records进行遍历

        //手动提交offset
        ack.acknowledge();
    }
}

producer

@RestController
@RequestMapping("/msg")
@RequiredArgsConstructor
public class KafkaController {

    private final static String TOPIC_NAME = "";

    private final KafkaTemplate<String, String> kafkaTemplate;

    @RequestMapping("/send")
    public String sendMessage() {
        kafkaTemplate.send(TOPIC_NAME, 0, "key", "message");
        return "success";
    }

}

手动提交配置

spring:
  kafka:
    ack-mode:MANUAL_IMMEDIATE   #手动提交调用ack.acknowledge()之后立即提交,一般用这种  还有别的配置,可自行查阅

8 kafka集群controller、rebalance和hw

8.1 controller

集群中谁来充当controller:每个broker启动时会向zk创建一个临时序号节点,获得的序号最小的那个broker将会作为集群中的controller,负责这么几件事:

  • 当集群中有一个副本的leader挂掉,需要在集群中选举出一个新的leader,选举的规则是从isr集合中最左边获得;
  • 当集群中有broker新增或减少,controller会同步信息给其他broker
  • 当集群中有分区新增或减少,controller会同步信息给其他broker

8.2 rebalance

前提:消费组中的消费者没有指明分区来消费
触发的条件:当消费组中的消费者和分区的关系发生变化的时候
分区分配的策略:在rebalance之前,分区的怎么分配会有这么三种策略

  • range:根据公式计算得到每个消费者消费哪几个分区,前面的消费者是分区总数/消费者数量+1,之后的消费者是分区总数/消费者数量
  • 轮询:大家轮着来
  • sticky:粘合策略,如果需要rebalance,会在之前已分配的基础上调整,不会改变之前的分配情况,如果这个策略没有开,那么就要进行全部的重新分配,建议开启。

8.3 HW和LEO

LEO是某个副本最后消息的消息位置,log_end_offset;
HW是已完成同步的位置,消息在写入broker时,且每个broker完成这条消息的同步后,hw才会变化,在这之前消费者是消费不到这条消息的,在同步完成之后,hw更新之后,消费者才能消费到这条消息,这样的目的是防止消息的丢失;
hw相当于一条线,等所有的leo到达这条线的时候,hw才下来,可以消费;

9 kafka线上问题优化

9.1 如何防止消息丢失

生产者:

  • 使用同步发送
  • 把ack设置成1或者all,并且设置同步的分区数>=2
    消费者:
  • 把自动提交改成手动提交
    broker:设置多个副本

9.2 如何防止消息重复消费

在防止消息丢失的方案中,如果生产者发送完消息后,因为网络抖动,没有收到ack,但实际上broker已经收到了,此时生产者会进行重试,于是broker就会收到多条相同的消息,从而造成消息的重复消费。
解决方法:

  • 生产者关闭重试:会造成丢消息,不建议使用
  • 消费者端解决非幂等性消费问题:在数据库中创建联合主键,防止相同的主键 创建出多条记录;使用分布式锁,以业务id为锁,保证只有一条记录能够创建成功;

9.3 如何做到顺序消费

生产者:保证消息顺序消费,且消息不丢失——使用同步发送,ack设置成非0的值
消费者:主题只能设置一个分区,消费组中只能有一个消费组
kafka的顺序消费使用场景不多,因为牺牲掉了性能,但是比如rocketmq在这一块有专门的功能已设计好;

9.4 解决消息积压问题

原因:消息的消费者的消费速度远赶不上生产者的生产消息速度,导致kafka中有大量的数据没有被消费,随着没有被消费的数据堆积越多,消费者寻址的性能也会越来越差,最后导致整个kafka对外提供服务的性能越来越差,从而造成其他服务的访问速度变慢,造成服务雪崩;
解决:

  • 在消费者中使用多线程,充分利用机器的性能进行消费消息;
  • 创建多个消费组,多个消费者,部署到其他机器上,一起消费,提高消费者的速度;

9.5 延迟队列

应用场景:订单创建后,超过30分钟没有支付,则需要取消订单,这种场景可以通过延时队列来实现;
具体方案:

  • kafka中创建相应的主题,:topic_30ms
  • 消费者消费该主题的消息,通过轮询的方式
  • 消费者消费消息时判断消息的创建时间和当前时间有没有超过30ms,前提时订单没支付,是则更改订单状态,否则记录当前的offset,并且不再继续消费消息,等待一分钟后,再次向kafka拉取该offset及之后的消息,继续进行判断,以此反复;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值