消息队列中间件——Kafka

1. 什么是消息队列

消息队列的英文全称是 Message Queue,简称 MQ。MQ 的作用是通过将消息的发送和接收分离来实现应用程序的异步和解耦。但 MQ 的真正目的是为了通信,屏蔽底层复杂的通讯协议,定义了一套应用层的、更加简单的通信协议。
MQ 在这些协议之上构建一个简单的“协议”———生成者/消费者模型。它定义两个对象,发送消息的叫生产者,接收消息的叫消费者。提供一个 SDK 让我们可以定义自己的生产者和消费者实现消息通讯而无视底层通讯协议。

2. Kafka的基本概念

kafka 是一个分布式的、分区的消息服务。

相关的术语介绍:

  1. Broker:消息中间件处理节点,一个 kafka 节点就是一个 broker,一个或者多个 broker 可以组成一个 kafka 集群
  2. Topic:kafka 根据 topic 对消息进行归类,发布到kafka的消息都需要指定 topic
  3. Producer:消息生产者,向 broker 发送消息的客户端
  4. Consumer:消息消费者,从 broker 读取消息的客户端
  5. Consumer Group:每个 Consumer 都属于一个特定的 Consumer Group,一条消息可以被多个不同的 Consumer Group 消费,但是一个 Consumer Group 中只能有一个 Consumer 能够消费该消息
  6. Partition:物理上的概念,一个 topic 可以分为多个 partition,每个 partition 内部消息是有序的

3. Kafka的Java生产者

3.1 配置并创建生产者对象

Properties properties = new Properties();

// 配置 kafka 集群的 broker 的 ip 和 port
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "ip:port, ip:port");

// 把发送的 key 字符串序列化成字节数组
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

// 把发送的 value 字符串序列化成字节数组
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());

// ack 参数配置,默认是 1
// 0:生产者发送完消息后,kafka 立刻给生产者返回 ack,这种容易丢失消息,效率最高
// 1:多副本中的 leader 收到消息,并写入到本地的 log 文件中,才给生产者返回 ack,安全性和性能比较均衡
// -1:消息需要在所有 ISR 都同步成功后,才给生产者返回 ack,最安全,效率最低
properties.put(ProducerConfig.ACKS_CONFIG, 1);

// 指定重试的次数
properties.put(ProducerConfig.RETRIES_CONFIG, 3);

// 指定重试的间隔
properties.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 100);

// kafka 默认会创建一个消息缓存区,用来存放要发送的消息,默认大小是 32M
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);

// kafka 本地线程会从缓存区中一次拉取 16K 数据,发送给 broker
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);

// 如果拉取不到 16K 数据,会间隔 10ms 后发送给 broker,默认是 0ms,这样影响性能,一般建议设置 10ms
properties.put(ProducerConfig.LINGER_MS_CONFIG, 10);

// 创建生产者客户端
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);

3.2 同步发送消息

// 创建一条消息,指定主题,消息的key,消息的内容,不指定分区是通过 key 的哈希运算计算出发到那个分区
ProducerRecord<String, String> record = new ProducerRecord<String, String>("topic", "key", "value");

// 发送消息并阻塞等待结果
try {
    // 发送消息
    Future<RecordMetadata> future = kafkaProducer.send(record);
    // get() 会阻塞直到 kafka 返回结果,或者抛异常
    RecordMetadata recordMetadata = future.get();
    System.out.printf("topic = %s, partition = %d, offset = %d%n",
            recordMetadata.topic(), recordMetadata.partition(), recordMetadata.offset());
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
    System.out.println("消息发送失败");
}

3.3 异步发送消息

// 创建一条消息,指定主题,消息的key,消息的内容,不指定分区是通过 key 的哈希运算计算出发到那个分区
ProducerRecord<String, String> record = new ProducerRecord<String, String>("topic", "key", "value");

// 发送消息,指定回调方法获取结果,不会阻塞后面的逻辑
kafkaProducer.send(record, new Callback() {
    @Override
    public void onCompletion(RecordMetadata recordMetadata, Exception e) {
        if (e != null) {
            System.out.println("发送消息失败");
        }
        if (recordMetadata != null) {
            System.out.printf("topic = %s, partition = %d, offset = %d%n",
                    recordMetadata.topic(), recordMetadata.partition(), recordMetadata.offset());
        }
    }
});

4. Kafka的Java消费者

4.1 配置并创建消费者对象

Properties properties = new Properties();

// 指定 kafka 集群中 broker 的 ip 和 port
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "ip:port, ip:port");

// 指定消费者的消费组名
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "groupName");

// 把消费的 key 反序列化
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

// 把消费的 value 反序列化
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

// 是否开启自动提交 offset ,默认是 true
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

// 设置自动提交 offset 的间隔时长
// properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");

// 消费者给 broker 发送心跳的间隔时长
properties.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);

// kafka 如果超过 10s 没有收到消费者的心跳,则会把消费者从消费组中踢出,进行rebalance,把分区分给其他消费者
properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);

// 一次 poll 最多拉取的消息条数,根据消费的速度快慢来决定
properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);

// 如果两次 poll 的时间间隔超过了 30s,kafka 会认为这个消费者消费能力过弱,则会把消费者从消费组中踢出,进行rebalance,把分区分给其他消费者
properties.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);

// 创建一个消费者客户端
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);

4.2 消费消息并手动同步的提交 offset

// 订阅主题,不指定分区
kafkaConsumer.subscribe(List.of("topic"));

while(true) {
    // poll 拉取消息
    ConsumerRecords<String, String> records = kafkaConsumer.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());
    }

    // 当前线程会一直阻塞到 offset 提交成功
	// 一般会使用同步提交,因为提交后没有什么逻辑代码了
	kafkaConsumer.commitSync();
}

4.3 消费消息手动异步的提交 offset

// 订阅主题,不指定分区
kafkaConsumer.subscribe(List.of("topic"));

while(true) {
    // poll 拉取消息
    ConsumerRecords<String, String> records = kafkaConsumer.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());
    }

    // 消费完消息后调用异步提交方法,不需要等待集群返回 ack,继续执行后面的逻辑。设置一个回调方法,供集群调用
	kafkaConsumer.commitAsync(new OffsetCommitCallback() {
    @Override
    public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
    }
});
}

5. Spring Boot整合Kafka

5.1 添加依赖

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>

5.2 生产者的配置

spring:
  kafka:
    producer:
        # 配置 kafka 集群中的 broker 的 ip:port
        bootstrap-servers: ip:port,ip:port
        # 发送消息缓存区大小
        buffer-memory: 33554432
        # kafka 本地线程会从缓存区中一次拉取 16K 数据,发送给 broker
        batch-size: 16384
        # 指定重试的次数
        retries: 3
        # 把发送的 key 字符串序列化成字节数组
        key-serializer: org.apache.kafka.common.serialization.StringSerializer
        # 把发送的 value 字符串序列化成字节数组
        value-serializer: org.apache.kafka.common.serialization.StringSerializer
        # ack 参数配置,默认是 1
        # 0:生产者发送完消息后,kafka 立刻给生产者返回 ack,这种容易丢失消息,效率最高
        # 1:多副本中的 leader 收到消息,并写入到本地的 log 文件中,才给生产者返回 ack,安全性和性能比较均衡
        # -1:消息需要在所有 ISR 都同步成功后,才给生产者返回 ack,最安全,效率最低
        acks: 1

5.3 生产者代码

@Autowired
private KafkaTemplate<String, String> kafkaTemplate;

public void sendMessage() {
    ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send("topic", "key", "data");
    future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
        @Override
        public void onFailure(Throwable ex) {
            System.out.println("消息发送失败!");
        }

        @Override
        public void onSuccess(SendResult<String, String> result) {
            System.out.println("消息发送成功!");
        }
    });
}

5.4 消费者的配置

spring:
    consumer:
      # 配置 kafka 集群中的 broker 的 ip:port
      bootstrap-servers: ip:port,ip:port
      # 关闭自动提交 offset
      enable-auto-commit: false
      # 新加入的消费组从头开始消费
      auto-offset-reset: earliest
      # 一次拉取的消息数
      max-poll-records: 500
      # 反序列化
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      # 反序列化
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer;
    listener:
      # manual:消费完 spring.kafka.consumer.max-poll-records 这么多的数据后,提交 ack
      # manual_immediate:消费一条数据完后,立即提交 ack
      ack-mode: manual_immediate

5.5 消费者的代码

/**
    * 可以指定订阅多个主题
    * 可以指定消费的分区
    * 可以指定 offset 开始消费
    * concurrency:指定一个消费组有多少个消费者
    */
@KafkaListener(groupId = "消费组名称", topicPartitions = {
        @TopicPartition(topic = "主题1", partitions = {"0", "1"}),
        @TopicPartition(topic = "主题2", partitions = "0",
                partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))
}, concurrency = "3")
public void handleMessage(ConsumerRecord<String, String> record, Acknowledgment ack) {
    // 消息内容
    record.value();
    // 手动提交 offset
    ack.acknowledge();
}

6. Kafka常见面试题

6.1 Kafka消息保存地方

  • 00000000.log:这个文件中保存的是消息
  • kafka 内部自己创建量一个 __consumer_offsets 的主题,默认是 50 个分区。这个主题是存放消费者消费某个主题的偏移量。每个消费者会把消费的主题的偏移量主动上报给默认主题 __consumer_offsets,为了提升并发性,默认设置了 50 个分区。
    • 提交给哪个分区:通过 hash 函数:hash(consumerGroupId) % __consumer_offsets 主题分区数
    • 提交的内容:key 是 consumerGroupId + topic + 分区号,value 是 offset 偏移量
  • 文件中保存的消息,默认是保存 7 天,7 天后会被删除

6.2 分区的作用

主题 topic 在 kafka 中是逻辑的概念,kafka 通过 topic 将消息分类,不同的 topic 被订阅了该主题的消息者消费。

如果 topic 的消息特别多,因为消息是被保存在 log 日志文件中的,为了解决文件过大的问题,kafka 提出了 partition 分区的概念。

通过 partition 将一个 topic 的消息分区来存储,好处在于:

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

6.3 Kafka集群中的controller

每个 borker 启动时会向 zookeeper 创建一个临时序号节点,获得的序号最小的那个 broker 将会作为集群中的 controller。

controller的作用:

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

6.4 Kafka集群的rebalance

前提:消费者没有指定消费哪个分区

触发 rebalance 的条件:当消费组中的消费者和分区的关系发生变化的时候

rebalance 的策略:

  • range:通过公式计算每个消费者消费哪些分区,前面的消费者(分区总数 / 消费者数 + 1),后面的消费者(分区总数 / 消费者数)
  • 轮循:一个一个的分配,比如,第一个分区给消费者1,第二个分给消费者2…
  • sticky:粘合策略,如果需要rablance,会在之前已分配的基础上调整。如果这个策略没有开启,则全部重新分配,建议开启

6.5 如何防止消息丢失

  • 生产者:ack 为 1 或 -1,可以防止消息丢失,如果要做到99.999%,ack 设置成 -1,并且 min.insync.replicas 等于分区副本数
  • 消费者:关闭自动提交

6.6 如何防止重复消费

保证接口的幂等性。

实现接口的幂等性:

  1. 通过主键或唯一索引来实现幂等性
  2. 分布式锁实现幂等性,在每次执行方法之前先判断是否可以获取到分布式锁

6.7 如何做到顺序消费

  • 生产者:保证消息按顺序发送,且不丢失——设置 ack 为 1 或 -1,采用同步发送
  • 消费者:一个主题只有一个分区,一个消费组只有一个消费者

6.8 如何解决消息积压

消息的消费速度远跟不上生产的速度,导致 kafka 中有大量的消息没有被消费。随着堆积的消息越多,消费者寻址的性能会越差,导致 kafka 对外提供的服务性能越差。

解决消息积压的方法:

  • 消费者采用多线程,充分的利用机器的性能消费消息
  • 增加主题的分区数量,同时增加消费组的消费者数量
  • 优化消费消息的业务,避免消费时间过长

6.9 如何实现延迟队列效果

例如:实现 30 分钟未支付修改订单状态为超时未支付

  • 消费内容包含创建的时间
  • 消费者消费时,用当前时间与消息创建时间比较
    • 如果超时,则修改订单状态,并且继续消息后面的消息;
    • 如果未超时,那么后面的消息肯定也未超时,停止消费,并记住这个 offset
    • 等待 1 分钟后,从 offset 开始继续 poll 消息进行消费
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值