快速上手Kafka,简易版

127 阅读4分钟

查看本机配置

b29c3a9521e1d797aa91f156783c0d9.png

检查JDK是否安装

java -version

aa7b7f3af7ecac05c4b4803073aa2b7.png

下载kafka、Zookeeper安装包

在~目录下创建一个kafka_test目录,进入该目录,下载kafka安装包跟Zookeeper安装包

wget https://archive.apache.org/dist/kafka/3.8.0/kafka_2.13-3.8.0.tgz
wget https://dlcdn.apache.org/zookeeper/zookeeper-3.8.4/apache-zookeeper-3.8.4.tar.gz

解压缩kafka安装包

tar -zxvf kafka_2.13-3.8.0.tgz

递归创建/app/kafka目录,将kafka安装包剪切到/app/kafka目录下

mkdir -p /app/kafka
mv kafka_2.13-3.8.0 /app/kafka/

后台启动kafka跟zookeeper

进入/app/kafka/kafka_2.13-3.8.0目录,启动zookeeper做配置中心,使用nohup命令使当前程序启动不占用控制台输出,即后台启动

nohup bin/zookeeper-server-start.sh config/zookeeper.properties &

b8dd7a553d2f34141f0fbe04876e59e.png 查看zookeeper进程是否启动成功:jps 55037f115369d114f5afa341dc6803c.png 后台启动kafka进程

nohup bin/kafka-server-start.sh config/server.properties &

查看kafka进程是否启动成功 d22daeb43e2a584ddd90cf40839cbdb.png kafka的工作机制:基于不同的topic来发送、接收业务消息,如订单,用户消息,所以我们先要创建topic,这里提供topic的查看文档帮助命令

bin/kafka-topics.sh --help

基于进程号查看Kafka占用的端口号(默认9092)

888df1356dfdea1bcb8487be028df34.png

创建topic

创建一个名为test的topic

bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic test

0429c9a49aa6f01d775eb431c084456.png 查看topic

bin/kafka-topics.sh --bootstrap-server localhost:9092 --list

创建生产者

创建一个基于控制台的生产者,控制台发送消息如需删除字符按住Ctrl+Backspace

发消息

bin/kafka-console-producer.sh  --bootstrap-server localhost:9092 --topic test

b5bc1dcb0e07d73e43b12155aca1046.png 复制该窗口,创建消费者,默认从启动消费者的那一刻开始从队列的最后位置开始消费,如果需要从头消费历史消息,可以加参数 --from-beginning

消费方式:

1.默认消费:

bin/kafka-console-consumer.sh  --bootstrap-server localhost:9092 --topic test

f8986661bf706485d96f487fac81956.png

2.从头消费:

bin/kafka-console-consumer.sh  --bootstrap-server localhost:9092 --topic test --from-beginning

d6583ca051d2f52481f690b9a06300d.png

3.指定位置开始消费

bin/kafka-console-consumer.sh  --bootstrap-server localhost:9092 --topic test --partition 0 --offset 2

e2898e6d312c60a85ccc6fb60f986ee.png 消息存储在partition中,这里指定从索引2的消息开始消费

6dc854f6fbc71b1a44dc4a9c5d2db0c.png 因为两个窗口分别属于不同的进程,所以也证明了进程通信可以通过消息队列来实现

消费者组

创建消费者组

同一个消费者组中的消息只会被消费一次,同一个消息可以被不同的消费组消费,创建三个消费组(其中两个消费者组名字一致)

bin/kafka-console-consumer.sh  --bootstrap-server localhost:9092 --topic test --group testGroup
bin/kafka-console-consumer.sh  --bootstrap-server localhost:9092 --topic test --group testGroup2

ce830d984cb13b616a40d2bab4483b1.png

72a900fbe9c1a3bb5787827b49dc27b.png

001793bec56d062f7921a3e4f14a757.png 窗口控制台2阻塞无输出,可以看到,同一个消费者组中的消息只会被消费一次(原因,同一个消费组共享同一个Offset),同一个消息可以被不同的消费组消费

查看具体消费者组的消费情况

bin/kafka-consumer-groups.sh  --bootstrap-server localhost:9092 --describe --group testGroup2

c192d4b6b26210cc215057e196d64d2.png 其中的LAG表示还未消费的消息数,LOG-END-OFFSET表示总的消息数,CURRENT-OFFSET表示当前消费的最后一条消息的编号,即索引

至此,一个简易版的消息队列就搭建完成,第一次写,更多的是记录学习历程,后续将继续更新,希望对您有帮助哈哈

附上Java版本的代码实现:

/**
 * 简单的消息队列模拟实现
 */
public class SimpleKafkaSimulation {
    // 消息队列
    private static final BlockingQueue<String> messageQueue = new LinkedBlockingQueue<>();
    // 消费者组与消费者的映射
    private static final Map<String, List<Consumer>> consumerGroups = new ConcurrentHashMap<>();
    // 记录每个消费者组消费到的偏移量
    private static final Map<String, AtomicInteger> groupOffsets = new ConcurrentHashMap<>();
    // 消费者类
    static class Consumer {
        private final String consumerId;

        public Consumer(String consumerId) {
            this.consumerId = consumerId;
        }

        public void consume(String groupId) {
            try {
                System.out.printf("消费者组 %s 的消费者 %s 开始消费消息...%n", groupId, consumerId);
                while (true) {
                    // 模拟消费者组内的负载均衡
                    List<Consumer> consumers = consumerGroups.get(groupId);
                    int consumerIndex = consumers.indexOf(this);
                    int totalConsumers = consumers.size();

                    // 从队列中获取消息
                    String message = null;
                    synchronized (messageQueue) {
                        int offset = groupOffsets.get(groupId).get();
                        if (offset < messageQueue.size()) {
                            // 模拟分区分配,每个消费者消费特定位置的消息
                            if (offset % totalConsumers == consumerIndex) {
                                // 获取对应位置的消息
                                message = getMessageByOffset(offset);
                                groupOffsets.get(groupId).incrementAndGet();
                            }
                        }
                    }

                    if (message != null) {
                        System.out.printf("消费者组 %s 的消费者 %s 消费消息: %s%n", groupId, consumerId, message);
                    } else {
                        // 没有消息可消费,休眠一段时间
                        Thread.sleep(1000);
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        // 根据偏移量获取消息
        private String getMessageByOffset(int offset) {
            List<String> messages = new ArrayList<>(messageQueue);
            return messages.get(offset);
        }
    }


    public static void main(String[] args) {
        // 初始化消费者组
        initConsumerGroups();

        // 启动生产者
        Thread producerThread = new Thread(new Producer());
        producerThread.start();

        // 启动消费者
        for (Map.Entry<String, List<Consumer>> entry : consumerGroups.entrySet()) {
            String groupId = entry.getKey();
            List<Consumer> consumers = entry.getValue();
            for (Consumer consumer : consumers) {
                Thread consumerThread = new Thread(() -> consumer.consume(groupId));
                consumerThread.start();
            }
        }
    }

    // 初始化消费者组
    private static void initConsumerGroups() {
        // 初始化testGroup,包含两个消费者
        List<Consumer> testGroupConsumers = new ArrayList<>();
        testGroupConsumers.add(new Consumer("consumer-1"));
        testGroupConsumers.add(new Consumer("consumer-2"));
        consumerGroups.put("testGroup", testGroupConsumers);
        groupOffsets.put("testGroup", new AtomicInteger(0));

        // 初始化testGroup2,包含一个消费者
        List<Consumer> testGroup2Consumers = new ArrayList<>();
        testGroup2Consumers.add(new Consumer("consumer-3"));
        consumerGroups.put("testGroup2", testGroup2Consumers);
        groupOffsets.put("testGroup2", new AtomicInteger(0));
    }

    // 生产者类
    static class Producer implements Runnable {
        private final String[] messages = {"hello kafka", "hello zkh", "1", "2", "3", "4", "5"};

        @Override
        public void run() {
            try {
                System.out.println("生产者开始发送消息...");
                for (String message : messages) {
                    messageQueue.put(message);
                    System.out.println("生产者发送消息: " + message);
                    Thread.sleep(500); // 控制发送速度
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}