Kafka原理与开发实战
想要了解kafka集群的搭建请移步:Linux中Kafka集群搭建实战与常用命令
原理
什么是kafka
kafka是一个消息中间件,它具有分布式高吞吐量的发布订阅消息系统;
基于zookeeper集群,具有高可用特性;
kafka组成部分
Broker:一台kafka服务器就是一个broker。一个集群由多个broker组成(只有一个是leader其余全部是follower)。一个broker可以容纳多个topic。
Topic:每条发布到Kafka集群的消息都有一个分类被称为Topic,可以理解为一个队列;
Message:消息,有两部分组成:定长消息头与变长消息体
Partition:Topic的物理分区,一个Topic有一个或者多个Partition组成,每个Partition都有一个独立的id编号,是一个有序队列;
Producer:消息生产者,就是向kafka broker发消息的客户端
Consumer:消息消费者,向kafka broker取消息的客户端。
Consumer Group:消费组,每一个Consumer都有独立的Cosumer Group,如果没有指定属于默认的Group
Consumer Coordinator:每一个Comsumer Group 都会有一个Broker作为协调者
消息存储机制
1,topic:每个发布到kafka集群的消息都有一个类别,这个类别就是通过topic做区分的;
2,partition:每一个topic是有一个或者多个partition组成
其实,partition是一个磁盘空间,简单点就是在kafka的logDir的文件夹;
他的命名规则是有:${topicname}_${partitionId}组成;eg:test1_0
每个一个partition都是该分区的日志段数据:该文件夹下面有三个文件组成:一个数据文件和两个索引文件;
eg: 00000000000000000000.log 00000000000000000000.index 00000000000000000000.timeindex
因此,每个消息都会被顺序加载到指定分区的磁盘中,保证高吞吐量;
但是kafka可以保证在同一个partitioni中消息有序,但是整体而言不能保证所有分区有序;
3,logSegment:
消息会根据时间或者数据大小被切分成一个或者多个日志段:
数据文件:
00000000000000000000.log
索引文件:
00000000000000000000.index
00000000000000000000.timeindex
消息是根据时间追到到日志文件中,日志文件就是用来存数据的;
索引文件与日志文件名称相同,是为了定位数据位置的;
查询机制
logSegment文件中:
有三个文件组成:
数据文件:
00000000000000000000.log
索引文件:
00000000000000000000.index
00000000000000000000.timeindex
在指定分区内的文件是有序的,所有根据偏移量,通过二分查找算法可以快速定位数据的索引位置,通过索引就可以快速定位文件数据;
生产者发送消息的策略与发送方式
1,同步阻塞:逐条发送,并通过get方法获取future对象,当执行get方法时会同步阻塞,处理结束后,在进行下条消息的发送;场景:保证顺序.不关系吞吐量;
2,异步忘记:不关心消息是否会出错,不关心顺序,只关心吞吐量;
3,异步回调:对发送的消息需要调用回调函数判断是否发送成功,因此在执行回调时也会同步阻塞;场景:不关心顺序与吞吐量,只保证数据是否出错;
消费者负载均衡策略
首先要明白kafka消费方组成部分:消费者、消费者组、消费组协调者
一,什么场景会出发消费失衡状态:
1,消费组有新消费者加入
2,有消费者宕机或者退出消费组
3,topic增加了partition
4,kafka集群中有broker宕机;
二,consumer与partition关系:
一个partition分区,能且只能被消费组中的一个消费者消费消息
springboot整合kafka实战
依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
yml配置文件
server:
port: 20000
spring:
kafka:
# broker集群
bootstrap-servers: 192.168.0.105:9092,192.168.0.106:9092,192.168.0.107:9092
producer:
# 发送消息失败,重试次数
retries: 1
#当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。
batch-size: 16384
# 设置生产者内存缓冲区的大小
buffer-memory: 33554432
# 键的序列化方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
# 值的序列化方式
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# acks控制多少个分区副本必须写入消息后生产者才能认为写入成功,这个参数对消息丢失可能性有很大影响
# acks=0 :生产者把消息发送到broker即认为成功,不等待broker的处理结果. 这种方式的吞吐最高,但也是最容易丢失消息的.
# acks=1 :生产者在该分区的主的broker(leader)写入消息并返回成功后,认为消息发送成功. 这种方式能够一定程度避免消息丢失, 但如果在写入leader broker后,还没有把消息同步到其他(follower broker时), 这时follower broker宕机了, 那么该消息还是会丢失.
# acks=all 或者 -1:生产者会等待所有broker成功写入该消息, 这种方式是最安全的, 能够保证消息不丢失, 但是延迟也是最大的.
acks: -1
consumer:
# 自动提交的时间间隔 在spring boot 2.X 版本中这里采用的是值的类型为Duration 需要符合特定的格式,如1S,1M,2H,5D
auto-commit-interval: 1S
# 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
# latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
# earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
auto-offset-reset: earliest
# 是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false,然后手动提交偏移量
enable-auto-commit: false
# 键的反序列化方式
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
# 值的反序列化方式
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
# 在侦听器容器中运行的线程数。
concurrency: 5
#listner负责ack,每调用一次,就立即commit
ack-mode: manual_immediate
# 一次拉取的最大条数
max-poll-records: 10
session:
timeout: 20000
生产者代码
package com.example.power.kafka;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ExecutionException;
/**
* @author LiuXinming
* @since 2020/11/14 20:39
*/
@Slf4j
@RestController
@RequestMapping("/kafka")
public class KafkaProducer {
@Autowired
KafkaTemplate<String, String> kafkaTemplate;
/**
* producer发送消息
*
* @param name 姓名
* @param age 年龄
* @param desc 描述
* @return 结果
*/
@GetMapping("/send/{name}/{age}/{desc}")
public Object send(@PathVariable("name") String name, @PathVariable("age") Integer age, @PathVariable("desc") String desc) {
KafkaDO kafkaDO = new KafkaDO();
kafkaDO.setAge(age);
kafkaDO.setName(name);
kafkaDO.setDesc(desc);
// 3,异步忘记发送
// kafkaTemplate.send("test1", 0, "test", JSON.toJSONString(kafkaDO));
try {
ListenableFuture<SendResult<String, String>> send = kafkaTemplate.send("test1", 0, "test", JSON.toJSONString(kafkaDO));
// 1,同步阻塞发送
send.get();
log.info("producer.发送成功");
// 2,异步回调发送
// send.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
// @Override
// public void onFailure(Throwable throwable) {
// log.info("producer.onFailure: 异步回调失败");
// }
//
// @Override
// public void onSuccess(SendResult<String, String> stringStringSendResult) {
// log.info("producer.onSuccess 异步回调成功");
// }
// });
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return "kafka发送成功";
}
}
消费者代码
package com.example.power.kafka;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
/**
* @author LiuXinming
* @since 2020/11/14 20:51
*/
@Slf4j
@Component
public class KafkaConsumer {
@KafkaListener(topics = "test1",groupId = "cloud-test")
public Object getMsg(ConsumerRecord<String,String> record){
String value = record.value();
log.info("consumer.value:{}", JSON.toJSONString(value));
return JSON.toJSONString(value);
}
}