查看本机配置
检查JDK是否安装
java -version
下载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 &
查看zookeeper进程是否启动成功:jps
后台启动kafka进程
nohup bin/kafka-server-start.sh config/server.properties &
查看kafka进程是否启动成功
kafka的工作机制:基于不同的topic来发送、接收业务消息,如订单,用户消息,所以我们先要创建topic,这里提供topic的查看文档帮助命令
bin/kafka-topics.sh --help
基于进程号查看Kafka占用的端口号(默认9092)
创建topic
创建一个名为test的topic
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic test
查看topic
bin/kafka-topics.sh --bootstrap-server localhost:9092 --list
创建生产者
创建一个基于控制台的生产者,控制台发送消息如需删除字符按住Ctrl+Backspace
发消息
bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test
复制该窗口,创建消费者,默认从启动消费者的那一刻开始从队列的最后位置开始消费,如果需要从头消费历史消息,可以加参数 --from-beginning
消费方式:
1.默认消费:
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test
2.从头消费:
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
3.指定位置开始消费
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --partition 0 --offset 2
消息存储在partition中,这里指定从索引2的消息开始消费
因为两个窗口分别属于不同的进程,所以也证明了进程通信可以通过消息队列来实现
消费者组
创建消费者组
同一个消费者组中的消息只会被消费一次,同一个消息可以被不同的消费组消费,创建三个消费组(其中两个消费者组名字一致)
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
窗口控制台2阻塞无输出,可以看到,同一个消费者组中的消息只会被消费一次(原因,同一个消费组共享同一个Offset),同一个消息可以被不同的消费组消费
查看具体消费者组的消费情况
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group testGroup2
其中的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();
}
}
}
}