1. 介绍

Kafka作为一款分布式流处理平台,具有高吞吐量、持久性、容错性等特点,适用于构建大规模的实时数据管道。

Spring Boot作为快速开发框架,提供了简化开发和部署的能力,使得与Kafka的整合变得更加容易。

通过将Spring Boot与Kafka进行整合,具有以下优势

  1. 异步消息处理:Kafka可以作为消息队列,实现异步消息的生产和消费,提高系统的吞吐量和响应速度。
  2. 解耦和扩展性:消息队列的引入可以解耦不同系统或模块之间的依赖关系,提高系统的灵活性和可维护性。
  3. 数据流处理:Kafka支持流处理,可以用于实时数据分析、日志收集、事件驱动等场景,为业务提供更多的数据洞察。
  4. 可靠性和容错性:Kafka具有高度可靠性和容错性,能够保证消息不丢失,并且支持分布式部署,保证系统的稳定性和可靠性。

Spring Boot与Kafka整合为开发者提供了一种强大的消息传递解决方案,能够满足现代分布式系统对于消息传递的需求,提高系统的性能、可维护性和可扩展性。

2. Kafka基础

2.1. 安装KafKa

可参考本篇博客

kafka集群搭建_kafka交流群-CSDN博客

Kafka的一些特点

  1. 发布订阅模型:
    • 生产者将消息发布到主题,消费者订阅这些主题以接收消息。
    • 每个主题可以有多个订阅者,消息会广播给所有订阅者。
  2. 消息日志:
    • Kafka将消息存储在持久化的日志中,每个消息都有一个唯一的偏移量。
    • 日志被分割成多个分区,每个分区中的消息有顺序的索引。
    • 分区允许Kafka在集群中并行处理和存储消息,提高了吞吐量和扩展性。
  3. 分布式架构:
    • Kafka是一个分布式系统,集群由多个Broker组成。
    • 每个分区有副本分布在不同的Broker上,确保消息的可靠性和容错性。
    • 分布式架构支持水平扩展,能够处理大规模的数据和高并发的请求。
  4. 高吞吐量:
    • Kafka被设计为高吞吐量的消息系统,能够处理每秒数百万条消息。
    • 高效的批量处理和零拷贝机制使得Kafka能够提供低延迟的消息传递服务。
  5. 持久性:
    • Kafka的消息是持久化存储的,可以通过配置持久化策略来保留消息的时间和大小。
    • 消息一旦被写入到Kafka中就不会丢失,即使消费者尚未处理。
  6. 水平扩展:
    • Kafka集群可以水平扩展,通过增加Broker和分区来提高系统的容量和吞吐量。
    • 新的Broker和分区可以动态地加入到集群中,而不会中断服务。
  7. 可靠性和容错性:
    • Kafka通过副本机制和ISR(In-Sync Replicas)机制实现高可靠性和容错性。
    • ISR机制确保了即使部分Broker失效,也能继续保持数据的一致性和可用性。
  8. 流处理:
    • Kafka Streams API提供了流处理的能力,允许开发者在Kafka中进行实时数据处理和分析。
    • 流处理功能使得Kafka能够更灵活地处理实时数据流和生成实时结果。

3. Spring Boot整合Kafka

3.1. 引入Kafka依赖

image-20240229213324522

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



gradle:
implementation 'org.springframework.kafka:spring-kafka:2.9.13'

3.2.编写配置文件
# 配置环境的 指定topic 如果有多个 可以使用 ,进行连接
dc:
  topics:
    info: ENTRY_USER_INFO
  # 指定 组id
  group-id: hrfan-consumer-group
spring:
  kafka:
    consumer:
      bootstrap-servers: 192.168.112.128:9092
      group-id: hrfan-consumer-group
      auto-offset-reset: earliest
      # 错误处理
      key-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
      properties:
        spring.json.trusted.packages: '*'
        # 序列化、反序列化一致
        spring.deserializer.key.delegate.class: org.apache.kafka.common.serialization.StringDeserializer
        # 注意 这里需要使用 org.apache.kafka.common.serialization.StringDeserializer
        # 不能使用 org.springframework.kafka.support.serializer.JsonSerializer 不然会报错 需要保持一致
        spring.deserializer.value.delegate.class: org.apache.kafka.common.serialization.StringDeserializer

    producer:
      bootstrap-servers: 192.168.112.128:9092
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer

4. 生产者(produced)

4.1. 生产者基础案例(基础测试)
/**
 * 测试向Kafka发送消息
 *
 * @author 13723
 * @version 1.0
 * 2024/3/1 10:35
 */
@SpringBootTest
public class KafkaProducedTest {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

	@Value("${dc.topics.dutyform}")
	private String topics;
	/**
	 * kafka模板 String消息
	 */
	@Resource
	private KafkaTemplate<String, String> kafkaTemplate;

	/**
	 * kafka模板 Message消息
	 */
	@Resource
	private KafkaTemplate<String, Message<String>> kafkaTemplateMessage;

	@Test
	@DisplayName("测试向KfaKa发送消息")
	public void testSend() {

		// 发送普通消息
		kafkaTemplate.send(topics, "测试发送普通消息-无Key");

		// 发送Key-Value消息
		kafkaTemplate.send(topics, "hrfan-key-1", "测试发送key-Value消息");

		// 发送Partition(分区)-Offset(偏移量)消息
		// 在Apache Kafka中,消息被组织在称为"主题(Topics)"的逻辑类别中。
		// 每个主题可以被划分为一个或多个"分区(Partitions)"。分区是消息的物理存储单元,它们分布在不同的Kafka服务器上。
		// 每个分区中的消息都有一个唯一的编号,称为"偏移量(Offset)"。这个偏移量标识了消息在该分区中的位置。偏移量是一个递增的整数,新消息的偏移量比旧消息的偏移量大。
		kafkaTemplate.send(topics, 0, "hrfan-key-2", "测试发送Partition-Offset消息");

		// 可以理解为拼装JSON类型数据
		// 发送Message消息
		// 通过sendDefault()方法发送消息,消息将会被发送到默认的主题中。
		String event = "测试发送Message消息";
		Map<String, Object> map = new HashMap<>();
		map.put("token", UUID.randomUUID().toString());
		MessageHeaders headers = new MessageHeaders(map);
		Message<String> message = MessageBuilder.createMessage(event, headers);

		// 设置默认topic
		kafkaTemplate.setDefaultTopic(topics);
		// 将消息发送到默认的topic
		// 注意此时修改 泛型为
		// @Resource
		// private KafkaTemplate<String,Message<String>> kafkaTemplateMessage;
		kafkaTemplateMessage.sendDefault("hrfan-key-3", message);

		logger.info("消息发送成功");
	}

}

image-20240301133509648

image-20240301133913211

5. 消费者

5.1.消费者基本案例(基础测试)
/**
 * 模拟Kafka消费者
 * @author 13723
 * @version 1.0
 * 2024/2/29 17:01
 */
@Component
public class KafkaCustomerDemo {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

	@KafkaListener(topics = "#{'${dc.topics.dutyform}'.split(',')}", groupId = "${dc.group-id}", autoStartup = "true")
	public void onMessageSync(ConsumerRecord<String, String> record,  @Headers Map<String,Object> headers) {
		logger.error("获取到的信息为:{},",record);
	}


}

image-20240301133749367

6.Kafka常用配置

kafka配置文件解释
producer.properties解释
producer.properties:生产端的配置文件
#指定kafka节点列表,用于获取metadata,不必全部指定
#需要kafka的服务器地址,来获取每一个topic的分片数等元数据信息。
metadata.broker.list=kafka01:9092,kafka02:9092,kafka03:9092
 
#生产者生产的消息被发送到哪个block,需要一个分组策略。
#指定分区处理类。默认kafka.producer.DefaultPartitioner,表通过key哈希到对应分区
#partitioner.class=kafka.producer.DefaultPartitioner
 
#生产者生产的消息可以通过一定的压缩策略(或者说压缩算法)来压缩。消息被压缩后发送到broker集群,
#而broker集群是不会进行解压缩的,broker集群只会把消息发送到消费者集群,然后由消费者来解压缩。
#是否压缩,默认0表示不压缩,1表示用gzip压缩,2表示用snappy压缩。
#压缩后消息中会有头来指明消息压缩类型,故在消费者端消息解压是透明的无需指定。
#文本数据会以1比10或者更高的压缩比进行压缩。
compression.codec=none
 
#指定序列化处理类,消息在网络上传输就需要序列化,它有String、数组等许多种实现。
serializer.class=kafka.serializer.DefaultEncoder
 
#如果要压缩消息,这里指定哪些topic要压缩消息,默认empty,表示不压缩。
#如果上面启用了压缩,那么这里就需要设置
#compressed.topics= 
#这是消息的确认机制,默认值是0。在面试中常被问到。
#producer有个ack参数,有三个值,分别代表:
#(1)不在乎是否写入成功;
#(2)写入leader成功;
#(3)写入leader和所有副本都成功;
#要求非常可靠的话可以牺牲性能设置成最后一种。
#为了保证消息不丢失,至少要设置为1,也就
#是说至少保证leader将消息保存成功。
#设置发送数据是否需要服务端的反馈,有三个值0,1,-1,分别代表3种状态:
#0: producer不会等待broker发送ack。生产者只要把消息发送给broker之后,就认为发送成功了,这是第1种情况;
#1: 当leader接收到消息之后发送ack。生产者把消息发送到broker之后,并且消息被写入到本地文件,才认为发送成功,这是第二种情况;#-1: 当所有的follower都同步消息成功后发送ack。不仅是主的分区将消息保存成功了,
#而且其所有的分区的副本数也都同步好了,才会被认为发动成功,这是第3种情况。
request.required.acks=0
 
#broker必须在该时间范围之内给出反馈,否则失败。
#在向producer发送ack之前,broker允许等待的最大时间 ,如果超时,
#broker将会向producer发送一个error ACK.意味着上一次消息因为某种原因
#未能成功(比如follower未能同步成功)
request.timeout.ms=10000
 
#生产者将消息发送到broker,有两种方式,一种是同步,表示生产者发送一条,broker就接收一条;
#还有一种是异步,表示生产者积累到一批的消息,装到一个池子里面缓存起来,再发送给broker,
#这个池子不会无限缓存消息,在下面,它分别有一个时间限制(时间阈值)和一个数量限制(数量阈值)的参数供我们来设置。
#一般我们会选择异步。
#同步还是异步发送消息,默认“sync”表同步,"async"表异步。异步可以提高发送吞吐量,
#也意味着消息将会在本地buffer中,并适时批量发送,但是也可能导致丢失未发送过去的消息
producer.type=sync
 
#在async模式下,当message被缓存的时间超过此值后,将会批量发送给broker,
#默认为5000ms
#此值和batch.num.messages协同工作.
queue.buffering.max.ms = 5000
 
#异步情况下,缓存中允许存放消息数量的大小。
#在async模式下,producer端允许buffer的最大消息量
#无论如何,producer都无法尽快的将消息发送给broker,从而导致消息在producer端大量沉积
#此时,如果消息的条数达到阀值,将会导致producer端阻塞或者消息被抛弃,默认为10000条消息。
queue.buffering.max.messages=20000
 
#如果是异步,指定每次批量发送数据量,默认为200
batch.num.messages=500
 
#在生产端的缓冲池中,消息发送出去之后,在没有收到确认之前,该缓冲池中的消息是不能被删除的,
#但是生产者一直在生产消息,这个时候缓冲池可能会被撑爆,所以这就需要有一个处理的策略。
#有两种处理方式,一种是让生产者先别生产那么快,阻塞一下,等会再生产;另一种是将缓冲池中的消息清空。
#当消息在producer端沉积的条数达到"queue.buffering.max.meesages"后阻塞一定时间后,
#队列仍然没有enqueue(producer仍然没有发送出任何消息)
#此时producer可以继续阻塞或者将消息抛弃,此timeout值用于控制"阻塞"的时间
#-1: 不限制阻塞超时时间,让produce一直阻塞,这个时候消息就不会被抛弃
#0: 立即清空队列,消息被抛弃
queue.enqueue.timeout.ms=-1
 
 
#当producer接收到error ACK,或者没有接收到ACK时,允许消息重发的次数
#因为broker并没有完整的机制来避免消息重复,所以当网络异常时(比如ACK丢失)
#有可能导致broker接收到重复的消息,默认值为3.
message.send.max.retries=3
 
#producer刷新topic metada的时间间隔,producer需要知道partition leader
#的位置,以及当前topic的情况
#因此producer需要一个机制来获取最新的metadata,当producer遇到特定错误时,
#将会立即刷新
#(比如topic失效,partition丢失,leader失效等),此外也可以通过此参数来配置
#额外的刷新机制,默认值600000
topic.metadata.refresh.interval.ms=60000
consumer.properties解释
#消费者集群通过连接Zookeeper来找到broker。
#zookeeper连接服务器地址
zookeeper.connect=zk01:2181,zk02:2181,zk03:2181
 
#zookeeper的session过期时间,默认5000ms,用于检测消费者是否挂掉
zookeeper.session.timeout.ms=5000
 
#当消费者挂掉,其他消费者要等该指定时间才能检查到并且触发重新负载均衡
zookeeper.connection.timeout.ms=10000
 
#这是一个时间阈值。
#指定多久消费者更新offset到zookeeper中。
#注意offset更新时基于time而不是每次获得的消息。
#一旦在更新zookeeper发生异常并重启,将可能拿到已拿到过的消息
zookeeper.sync.time.ms=2000
 
#指定消费
group.id=xxxxx
 
#这是一个数量阈值,经测试是500条。
#当consumer消费一定量的消息之后,将会自动向zookeeper提交offset信息#注意offset信息并不是每消费一次消息就向zk提交
#一次,而是现在本地保存(内存),并定期提交,默认为true
auto.commit.enable=true
 
# 自动更新时间。默认60 * 1000
auto.commit.interval.ms=1000
 
# 当前consumer的标识,可以设定,也可以有系统生成,
#主要用来跟踪消息消费情况,便于观察
conusmer.id=xxx
 
# 消费者客户端编号,用于区分不同客户端,默认客户端程序自动产生
client.id=xxxx
 
# 最大取多少块缓存到消费者(默认10)
queued.max.message.chunks=50
 
# 当有新的consumer加入到group时,将会reblance,此后将会
#有partitions的消费端迁移到新  的consumer上,如果一个
#consumer获得了某个partition的消费权限,那么它将会向zk
#注册 "Partition Owner registry"节点信息,但是有可能
#此时旧的consumer尚没有释放此节点, 此值用于控制,
#注册节点的重试次数.
rebalance.max.retries=5
 
#每拉取一批消息的最大字节数
#获取消息的最大尺寸,broker不会像consumer输出大于
#此值的消息chunk 每次feth将得到多条消息,此值为总大小,
#提升此值,将会消耗更多的consumer端内存
fetch.min.bytes=6553600
 
#当消息的尺寸不足时,server阻塞的时间,如果超时,
#消息将立即发送给consumer
#数据一批一批到达,如果每一批是10条消息,如果某一批还
#不到10条,但是超时了,也会立即发送给consumer。
fetch.wait.max.ms=5000
socket.receive.buffer.bytes=655360
 
# 如果zookeeper没有offset值或offset值超出范围。
#那么就给个初始的offset。有smallest、largest、
#anything可选,分别表示给当前最小的offset、
#当前最大的offset、抛异常。默认largest
auto.offset.reset=smallest
 
# 指定序列化处理类
derializer.class=kafka.serializer.DefaultDecoder
server.properties解释
server.properties:服务端的配置文件
#broker的全局唯一编号,不能重复
broker.id=0
 
#用来监听链接的端口,producer或consumer将在此端口建立连接
port=9092
 
#处理网络请求的线程数量,也就是接收消息的线程数。
#接收线程会将接收到的消息放到内存中,然后再从内存中写入磁盘。
num.network.threads=3
 
#消息从内存中写入磁盘是时候使用的线程数量。
#用来处理磁盘IO的线程数量
num.io.threads=8
 
#发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
 
#接受套接字的缓冲区大小
socket.receive.buffer.bytes=102400
 
#请求套接字的缓冲区大小
socket.request.max.bytes=104857600
 
#kafka运行日志存放的路径
log.dirs=/export/servers/logs/kafka
 
#topic在当前broker上的分片个数
num.partitions=2
 
#我们知道segment文件默认会被保留7天的时间,超时的话就
#会被清理,那么清理这件事情就需要有一些线程来做。这里就是
#用来设置恢复和清理data下数据的线程数量
num.recovery.threads.per.data.dir=1
 
#segment文件保留的最长时间,默认保留7天(168小时),
#超时将被删除,也就是说7天之前的数据将被清理掉。
log.retention.hours=168
 
#滚动生成新的segment文件的最大时间
log.roll.hours=168
 
#日志文件中每个segment的大小,默认为1G
log.segment.bytes=1073741824
 
#上面的参数设置了每一个segment文件的大小是1G,那么
#就需要有一个东西去定期检查segment文件有没有达到1G,
#多长时间去检查一次,就需要设置一个周期性检查文件大小
#的时间(单位是毫秒)。
log.retention.check.interval.ms=300000
 
#日志清理是否打开
log.cleaner.enable=true
 
#broker需要使用zookeeper保存meta数据
zookeeper.connect=zk01:2181,zk02:2181,zk03:2181
 
#zookeeper链接超时时间
zookeeper.connection.timeout.ms=6000
 
#上面我们说过接收线程会将接收到的消息放到内存中,然后再从内存
#写到磁盘上,那么什么时候将消息从内存中写入磁盘,就有一个
#时间限制(时间阈值)和一个数量限制(数量阈值),这里设置的是
#数量阈值,下一个参数设置的则是时间阈值。
#partion buffer中,消息的条数达到阈值,将触发flush到磁盘。
log.flush.interval.messages=10000
 
#消息buffer的时间,达到阈值,将触发将消息从内存flush到磁盘,
#单位是毫秒。
log.flush.interval.ms=3000
 
#删除topic需要server.properties中设置delete.topic.enable=true否则只是标记删除
delete.topic.enable=true
 
#此处的host.name为本机IP(重要),如果不改,则客户端会抛出:
#Producer connection to localhost:9092 unsuccessful 错误!
host.name=kafka01
 
advertised.host.name=192.168.239.128

7.ACK

什么是ACK?

​ 在Apache Kafka中,“Ack” 是 “Acknowledgement” 的缩写,用于表示生产者(producer)发送消息给 Kafka 服务器后,服务器返回的确认信息。

Kafka提供了三种ACK级别

  • acks=0:生产者发送消息后,不等待任何确认,直接发送下一条消息。
  • acks=1:生产者发送消息后,等待leader节点成功写入消息后返回确认,然后发送下一条消息。
  • acks=all:生产者发送消息后,等待所有的follower节点和leader节点都成功写入消息后返回确认,然后发送下一条消息。

acks=all 是最安全的设置,但是也会导致最慢的性能,因为要等待多个副本的确认。

image-20240301183001141

  • 生产者(produced)发送消息到 leaderleader收到消息会发送ACK
    • leader负责处理读写操作
      • 如果leader出现故障,会从follwer中重新选取leader
    • follower负责副本数据之间的同步
      • follower可以理解为自动备份,会不断从对应分区拉取leader的数据,对数据进行存储
  • leaderfollower 之间同步数据也会发送ACK

在Spring-Kafka中,提供了集中AckMode模式

org.springframework.kafka.listener.ContainerProperties.AckMode
public static enum AckMode {
        RECORD,
        BATCH,
        TIME,
        COUNT,
        COUNT_TIME,
        MANUAL,
        MANUAL_IMMEDIATE;

        private AckMode() {
        }
}
  1. RECORD:每处理一条消息后立即发送确认。这意味着每处理一条消息,消费者都会向 Kafka 代理发送一个确认消息。
  2. BATCH:批量确认模式。消费者将处理一批消息后才发送一次确认。这种模式可以减少确认消息的数量,提高性能。
  3. TIME:定时确认模式。消费者将在一定时间间隔内处理的所有消息后发送一次确认。这可以控制确认消息的发送频率。
  4. COUNT:计数确认模式。消费者将处理一定数量的消息后发送一次确认。这种模式也有助于控制确认消息的发送频率。
  5. COUNT_TIME:结合了计数和定时的确认模式。消费者将在达到一定数量的消息或一定时间间隔内发送一次确认,以提高灵活性和性能。
  6. MANUAL:手动确认模式。消费者需要在处理消息后显式地调用确认操作,以告知 Kafka 代理消息已被处理。
  7. MANUAL_IMMEDIATE:立即手动确认模式。与上述手动确认模式类似,但在调用确认操作后立即发送确认,而不是等待一定的时间或数量。

7.1手动提交ACK

Kafka中ACK默认是自动提交的,在开发中,有时候我们需要进行手动提交ACK,那么在配置中我们可以做如下修改

  • 禁止自动提交enable-auto-commit=false
  • 设置ack-modemanual_immediat(立即手动确认模式)
	@KafkaListener(topics = "#{'${dc.topics.user}'.split(',')}", groupId = "${dc.group-id}", autoStartup = "true")
	public void onMessageSync(ConsumerRecord<String, String> record,  @Headers Map<String,Object> headers,Acknowledgment ack) {
		logger.error("获取到的信息为:{},",record);

		//手动提交offset
		// ack.acknowledge();
	}

image-20240301201320109

image-20240301201438112

image-20240301201524566

8.查看指定分区的数据

有时候需要在服务器上查询一些分区的数据,可以使用 kafka提供的工具 kafka-console-consumer

kafka-console-consumer --bootstrap-server 192.168.112.129:9092 --topic ENTRY_LIST_SEND_INFO --from-beginning

image-20240301171337018

image-20240301171647186

Logo

Kafka开源项目指南提供详尽教程,助开发者掌握其架构、配置和使用,实现高效数据流管理和实时处理。它高性能、可扩展,适合日志收集和实时数据处理,通过持久化保障数据安全,是企业大数据生态系统的核心。

更多推荐