掘金 后端 ( ) • 2024-04-18 15:42

docker安装 || 官网下载安装包

安装
docker pull apache/kafka:3.7.0

启动:
docker run -p 9092:9092 apache/kafka:3.7.0

kafka 数据可视化工具

https://www.kafkatool.com/download.html

配置一下数据预览格式

image.png

基本概念

  • brokers 一个Kafka 服务器也称为Broker,它接受生产者发送的消息并存入磁盘。
    如果你搭建了集群,就会有多个Broker
  • topic 主题 相当于表名,你的生产者发消息,都要存到指定表里
  • partitions分区 一个topic可以有创建多个分区,就类似mysql的水平分表,把同类数据分散存在不同的表中

注意:多个分区,只能保证同一个分区的消费顺序,会影响消费顺序。如果没有key的情况下无法保证消费顺序,只能保证同一个分区的消费顺序

注意:如果应用程序对消息的顺序有严格的要求,需要在生产者端通过配置(key)来确保相关的消息发送到同一个分区。或者就建一个分区,不要多分区,也可保证顺序消费
什么是消息key? 就它, 自定义的 image.png

  • producer 生产者
    就是把你需要用到的数据,存到topic中, 类似 key-value数据结构的redis, 但kafka的key不是必传的,加key可以保证多分区下,相同的key写入到同一个分区(看上面的图片,就是生产者生产的数据)

  • consumer 消费者
    就是从指定topic中业务需要用到的数据,拿出来处理业务,处理完成后,可以提交标记这个数据的状态是已处理,对应主题的offset偏移量会增加,在一个分区内是按照创建顺序去消费的

  • partitions当前主题分区id, offset 偏移量(处理到哪行了), lag 还有多少没处理
    每个用户组都会记录自己每个分区下面的数据处理情况,下面是1个主题3个分区,消费者组的记录示例:

image.png

  • 消费者组
    消费者可以分组,多个消费者可以分到一个组中,消费者组不同,是可以重复消费的, kafka的发布订阅广播模式就是让不同用户组的用户都能重复消费实现的

消费者组示例:

const { Kafka } = require('kafkajs');

const kafka = new Kafka({
  clientId: 'my-app',
  brokers: ['localhost:9092']
});

// 注意这里的2个消费者是同一个组的
const consumerA = kafka.consumer({ groupId: 'my-group' });
const consumerB = kafka.consumer({ groupId: 'my-group' });
//这个是其它组的
const consumerC = kafka.consumer({ groupId: 'grop2' });

注意: 一个分区只能一个消费者处理,当你1个主题,配置了1个分区,配置2个消费者,另外一个消费者是不干活的。
多个分区,会自动分配不同的消费者去消费,假设3个分区, 2个消费者属于一个组, 那么可能0,1分区让a消费者处理,2分区让b消费者处理
consumerAconsumerC 是不同组的人, consumerA消费过的消息,consumerC可以再次消费,他们互不影响,各自的offset偏移量记录是分开记录的

数据安全

分区副本数量不能超过 broker数量,如果你没有集群,就是个单服务,副本数量就是1,无需配置

为什么要配置副本?在集群模式下,万一你某个broker节点挂了,另外节点会自动同步副本数据顶上来继续服务,提高服务稳定性,如果没有副本,顶上来的broker节点不知道你挂掉的那个broker对外提供什么数据,消费到哪了,也就无法提供服务。

kafkajs 配置副本示例:

const { Kafka } = require('kafkajs');

async function configureReplicationFactor() {
  // 创建Kafka实例
  const kafka = new Kafka({
    clientId: 'my-kafka-app',
    brokers: ['localhost:9092'] // 根据你的Kafka配置进行修改
  });

  // 创建Admin客户端
  const admin = kafka.admin();

  try {
    // 连接到Kafka集群
    await admin.connect();

    // 设置副本数量
    const topicConfig = {
      topic: 'my-topic', // 根据你的主题名称进行修改
      configEntries: [
        { name: 'replication.factor', value: '3' } // 设置副本数量为3
      ]
    };

    // 修改主题配置
    await admin.alterConfigs({
      validateOnly: false,
      resources: [topicConfig]
    });

    console.log('副本数量设置成功');
  } catch (error) {
    console.error('设置副本数量时出现错误:', error);
  } finally {
    // 断开连接
    await admin.disconnect();
  }
}

// 调用函数进行副本数量配置
configureReplicationFactor();

ack消息的确认机制

ack, 默认值:1, 使用包不同,这个默认值可能有不同,例如 kafkajs包默认是 -1 也就是all
在Apache Kafka中,"ack"是指消息的确认机制。当生产者发送消息到Kafka集群时,它可以选择性地设置确认机制,以确保消息已成功发送到代理(broker)。Kafka提供了三种确认机制:

  1. acks=0:生产者不会等待任何确认,只是将消息发送到代理,并且不会知道消息是否已经成功写入。这种设置具有最低的延迟,但是可能会导致数据丢失,因为代理在接收到消息之前可能会崩溃或者出现故障。

  2. acks=1:生产者在消息被成功写入到主题的分区中后会收到来自分区领导者的确认。这个设置提供了更高的可靠性,因为生产者至少知道消息已经成功写入到分区中,但仍然可能会丢失数据,如果分区领导者在发送确认之后但在复制数据到其他副本之前崩溃,那么数据可能会丢失。

  3. acks=all:生产者在消息被成功写入到主题的所有副本后会收到来自分区领导者的确认。这个设置提供了最高的可靠性,因为只有在所有副本都写入成功后才会发送确认,但是会增加延迟,因为需要等待所有副本写入完成。

    通过选择合适的确认机制,生产者可以根据应用程序的需求在延迟和可靠性之间进行权衡

const { Kafka } = require('kafkajs');

// 创建 Kafka 实例
const kafka = new Kafka({
  clientId: 'my-kafka-app',
  brokers: ['localhost:9092'] // Kafka 代理的地址
});

// 创建生产者实例
const producer = kafka.producer();

// 发送消息
async function sendMessage() {
  await producer.connect();
  await producer.send({
    topic: 'my-topic',
    acks: 1, // 设置确认机制,这里设置为1表示等待分区领导者确认消息写入,默认是 -1 也就是 all
    messages: [
      { value: 'Hello Kafka!' }
    ]
  });
}

// 启动发送消息
sendMessage()
  .then(() => console.log('Message sent successfully'))
  .catch(error => console.error('Error sending message:', error))
  .finally(() => producer.disconnect());

kafkajs

fromBeginning: true 配置这个,项目启动的时候会从topic没有消费的地方开始消费。不配置的话只能是项目启动后,生产者新创建的消息,会实时消费,生产者创建之前的消息没有消费掉也不会主动去消费。

最后来一个完整kafkajs案例:

kafka_producer.js 生产者

const { Kafka } = require('kafkajs');

const kafka = new Kafka({
  clientId: 'my-app',
  brokers: ['localhost:9092']
});

const producer = kafka.producer();
const topic = "my-topic"
const admin = kafka.admin();
const produceMessages = async () => {
  await producer.connect();
  // 创建一个具有3个分区的主题
  await admin.createTopics({
    topics: [
      {
        topic: topic,
        numPartitions: 3
      }
    ]
  });
  // 待发送的消息
  const messages = Array.from({ length: 100 }, (_, index) => ({
    key: 'my-key', // 设置消息的键,多分区下可保证同一个key在同一个分区中
    value:`数据--${index}`
  }));

  // 发送消息
  await producer.send({
    topic,
    messages,
  });

  console.log('Messages have been sent successfully');

  await producer.disconnect();
};

produceMessages().catch(console.error);


kafka_consumer.js 消费者

const { Kafka } = require('kafkajs');

const kafka = new Kafka({
  clientId: 'my-app',
  brokers: ['localhost:9092']
});

// 注意这里的2个消费者是同一个组的
const consumerA = kafka.consumer({ groupId: 'my-group' });
const consumerB = kafka.consumer({ groupId: 'my-group' });

const topic = "my-topic"

const consumeMessages = async () => {
  await consumerA.connect();
  await consumerB.connect();

  await consumerA.subscribe({ topic, fromBeginning: true });
  await consumerB.subscribe({ topic, fromBeginning: true });

  consumerA.run({
    eachMessage: async ({ topic, partition, message }) => {
      try {
        console.log(`Consumer A: ${message.value.toString()}, topic:${topic},partition:${partition}`);
      } catch (error) {
        console.error('Consumer A Error:', error.message);
      }
    },
  });

  consumerB.run({
    eachMessage: async ({ topic, partition, message }) => {
      try {
        console.log(`Consumer B: ${message.value.toString()}, topic:${topic},partition:${partition}`);
      } catch (error) {
        console.error('Consumer B Error:', error.message);
      }
    },
  });
};

consumeMessages().catch(console.error);