赞
踩
Kafka作为一款分布式流处理平台,具有高吞吐量、持久性、容错性等特点,适用于构建大规模的实时数据管道。
Spring Boot作为快速开发框架,提供了简化开发和部署的能力,使得与Kafka的整合变得更加容易。
通过将Spring Boot与Kafka进行整合,具有以下优势
异步消息处理
:Kafka可以作为消息队列,实现异步消息的生产和消费,提高系统的吞吐量和响应速度。解耦和扩展性
:消息队列的引入可以解耦不同系统或模块之间的依赖关系,提高系统的灵活性和可维护性。数据流处理
:Kafka支持流处理,可以用于实时数据分析、日志收集、事件驱动等场景,为业务提供更多的数据洞察。可靠性和容错性
:Kafka具有高度可靠性和容错性,能够保证消息不丢失,并且支持分布式部署,保证系统的稳定性和可靠性。Spring Boot与Kafka整合为开发者提供了一种强大的消息传递解决方案,能够满足现代分布式系统对于消息传递的需求,提高系统的性能、可维护性和可扩展性。
可参考本篇博客
Kafka的一些特点
<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'
# 配置环境的 指定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
/** * 测试向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("消息发送成功"); } }
/** * 模拟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); } }
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
#消费者集群通过连接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:服务端的配置文件 #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
什么是ACK?
在Apache Kafka中,“Ack” 是 “Acknowledgement” 的缩写,用于表示生产者(producer)发送消息给 Kafka 服务器后,服务器返回的确认信息。
Kafka
提供了三种ACK级别
:
acks=0
:生产者发送消息后,不等待任何确认,直接发送下一条消息。acks=1
:生产者发送消息后,等待leader
节点成功写入消息后返回确认,然后发送下一条消息。acks=all
:生产者发送消息后,等待所有的follower
节点和leader
节点都成功写入消息后返回确认,然后发送下一条消息。acks=all 是最安全的设置,但是也会导致最慢的性能,因为要等待多个副本的确认。
生产者(produced)
发送消息到 leader
,leader
收到消息会发送ACK
leader
负责处理读写操作
leader
出现故障,会从follwer
中重新选取leader
follower
负责副本数据之间的同步
follower
可以理解为自动备份,会不断从对应分区拉取leader的数据,对数据进行存储leader
和 follower
之间同步数据
也会发送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() {
}
}
7.1手动提交ACK
Kafka中ACK默认是自动提交的,在开发中,有时候我们需要进行手动提交ACK,那么在配置中我们可以做如下修改
- 禁止自动提交
enable-auto-commit=false
,- 设置
ack-mode
为manual_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();
}
有时候需要在服务器上查询一些分区的数据,可以使用 kafka提供的工具
kafka-console-consumer
kafka-console-consumer --bootstrap-server 192.168.112.129:9092 --topic ENTRY_LIST_SEND_INFO --from-beginning
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。