5
5
- 消息队列
6
6
---
7
7
8
+ ## Kafka 基础
9
+
8
10
### Kafka 是什么?主要应用场景有哪些?
9
11
10
12
Kafka 是一个分布式流式处理平台。这到底是什么意思呢?
@@ -61,6 +63,8 @@ Kafka 主要有两大应用场景:
61
63
62
64
> ** RocketMQ 的消息模型和 Kafka 基本是完全一样的。唯一的区别是 Kafka 中没有队列这个概念,与之对应的是 Partition(分区)。**
63
65
66
+ ## Kafka 核心概念
67
+
64
68
### 什么是 Producer、Consumer、Broker、Topic、Partition?
65
69
66
70
Kafka 将生产者发布的消息发送到 ** Topic(主题)** 中,需要这些消息的消费者可以订阅这些 ** Topic(主题)** ,如下图所示:
@@ -91,13 +95,15 @@ Kafka 将生产者发布的消息发送到 **Topic(主题)** 中,需要这
91
95
1 . Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力(负载均衡)。
92
96
2 . Partition 可以指定对应的 Replica 数, 这也极大地提高了消息存储的安全性, 提高了容灾能力,不过也相应的增加了所需要的存储空间。
93
97
94
- ### Zookeeper 在 Kafka 中的作用知道吗?
98
+ ## Zookeeper 和 Kafka
95
99
96
- > ** 要想搞懂 zookeeper 在 Kafka 中的作用 一定要自己搭建一个 Kafka 环境然后自己进 zookeeper 去看一下有哪些文件夹和 Kafka 有关,每个节点又保存了什么信息。** 一定不要光看不实践,这样学来的也终会忘记!这部分内容参考和借鉴了这篇文章:https://www.jianshu.com/p/a036405f989c 。
100
+ ### Zookeeper 在 Kafka 中的作用是什么?
101
+
102
+ > 要想搞懂 zookeeper 在 Kafka 中的作用 一定要自己搭建一个 Kafka 环境然后自己进 zookeeper 去看一下有哪些文件夹和 Kafka 有关,每个节点又保存了什么信息。 一定不要光看不实践,这样学来的也终会忘记!这部分内容参考和借鉴了这篇文章:https://www.jianshu.com/p/a036405f989c 。
97
103
98
104
下图就是我的本地 Zookeeper ,它成功和我本地的 Kafka 关联上(以下文件夹结构借助 idea 插件 Zookeeper tool 实现)。
99
105
100
- <img src =" https://my-blog-to-use. oss-cn-beijing.aliyuncs.com/2019-11 /zookeeper-kafka.jpg " style =" zoom :50% ;" />
106
+ <img src =" https://oss.javaguide.cn/github/javaguide/high-performance/message-queue /zookeeper-kafka.jpg " style =" zoom :50% ;" />
101
107
102
108
ZooKeeper 主要为 Kafka 提供元数据的管理的功能。
103
109
@@ -108,6 +114,16 @@ ZooKeeper 主要为 Kafka 提供元数据的管理的功能。
108
114
3 . ** 负载均衡** :上面也说过了 Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力。 对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。
109
115
4 . ......
110
116
117
+ ### 使用 Kafka 能否不引入 Zookeeper?
118
+
119
+ 在 Kafka 2.8 之前,Kafka 最被大家诟病的就是其重度依赖于 Zookeeper。在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的方式来使用 Kafka。
120
+
121
+ 不过,要提示一下:** 如果要使用 KRaft 模式的话,建议选择较高版本的 Kafka,因为这个功能还在持续完善优化中。Kafka 3.3.1 版本是第一个将 KRaft(Kafka Raft)共识协议标记为生产就绪的版本。**
122
+
123
+ ![ ] (https://oss.javaguide.cn/github/javaguide/high-performance/message-queue/kafka3.3.1-kraft- production-ready.png)
124
+
125
+ ## Kafka 消费顺序、消息丢失和重复消费
126
+
111
127
### Kafka 如何保证消息的消费顺序?
112
128
113
129
我们在使用消息队列的过程中经常有业务场景需要严格保证消息的消费顺序,比如我们同时发了 2 个消息,这 2 个消息对应的操作分别对应的数据库操作是:
@@ -119,7 +135,7 @@ ZooKeeper 主要为 Kafka 提供元数据的管理的功能。
119
135
120
136
我们知道 Kafka 中 Partition(分区)是真正保存消息的地方,我们发送的消息都被放在了这里。而我们的 Partition(分区) 又存在于 Topic(主题) 这个概念中,并且我们可以给特定 Topic 指定多个 Partition。
121
137
122
- ![ ] ( https://my-blog-to-use. oss-cn-beijing.aliyuncs.com/2019-11 /KafkaTopicPartionsLayout.png )
138
+ ![ ] ( https://oss.javaguide.cn/github/javaguide/high-performance/message-queue /KafkaTopicPartionsLayout.png )
123
139
124
140
每次添加消息到 Partition(分区) 的时候都会采用尾加法,如上图所示。 ** Kafka 只能为我们保证 Partition(分区) 中的消息有序。**
125
141
@@ -136,7 +152,7 @@ Kafka 中发送 1 条消息的时候,可以指定 topic, partition, key,data
136
152
137
153
当然不仅仅只有上面两种方法,上面两种方法是我觉得比较好理解的,
138
154
139
- ### Kafka 如何保证消息不丢失
155
+ ### Kafka 如何保证消息不丢失?
140
156
141
157
#### 生产者丢失消息的情况
142
158
@@ -164,7 +180,7 @@ if (sendResult.getRecordMetadata() != null) {
164
180
165
181
如果消息发送失败的话,我们检查失败的原因之后重新发送即可!
166
182
167
- ** 另外这里推荐为 Producer 的` retries ` (重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你 3 次一下子就重试完了**
183
+ 另外,这里推荐为 Producer 的` retries ` (重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你 3 次一下子就重试完了。
168
184
169
185
#### 消费者丢失消息的情况
170
186
@@ -204,7 +220,7 @@ acks 的默认值即为 1,代表我们的消息被 leader 副本接收之后
204
220
205
221
我们最开始也说了我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。多个 follower 副本之间的消息同步情况不一样,当我们配置了 ** unclean.leader.election.enable = false** 的话,当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。
206
222
207
- ### Kafka 如何保证消息不重复消费
223
+ ### Kafka 如何保证消息不重复消费?
208
224
209
225
** kafka 出现消息重复消费的原因:**
210
226
@@ -218,35 +234,38 @@ acks 的默认值即为 1,代表我们的消息被 leader 副本接收之后
218
234
- 处理完消息再提交:依旧有消息重复消费的风险,和自动提交一样
219
235
- 拉取到消息即提交:会有消息丢失的风险。允许消息延时的场景,一般会采用这种方式。然后,通过定时任务在业务不繁忙(比如凌晨)的时候做数据兜底。
220
236
221
- # kafka的重试机制
222
- 网上关于 Spring kafka 的默认重试机制文章很多,但大多都是过时的,和实际运行结果完全不一样。以下是根据 Spring-kafka-2.9.3 源码重新梳理一下。
237
+ ## Kafka 重试机制
223
238
224
- ## 消费失败会怎么样
239
+ 在 Kafka 如何保证消息不丢失这里,我们提到了 Kafka 的重试机制。由于这部分内容较为重要,我们这里再来详细介绍一下。
240
+
241
+ 网上关于 Spring Kafka 的默认重试机制文章很多,但大多都是过时的,和实际运行结果完全不一样。以下是根据 [ spring-kafka-2.9.3] ( https://mvnrepository.com/artifact/org.springframework.kafka/spring-kafka/2.9.3 ) 源码重新梳理一下。
242
+
243
+ ### 消费失败会怎么样?
225
244
226
245
在消费过程中,当其中一个消息消费异常时,会不会卡住后续队列消息的消费?这样业务岂不是卡住了?
227
246
228
247
生产者代码:
229
248
230
- ``` Java
231
- for (int i = 0 ; i < 10 ; i++ ) {
232
- kafkaTemplate. send(KafkaConst . TEST_TOPIC , String . valueOf(i))
233
- }
234
- ```
249
+ ``` Java
250
+ for (int i = 0 ; i < 10 ; i++ ) {
251
+ kafkaTemplate. send(KafkaConst . TEST_TOPIC , String . valueOf(i))
252
+ }
253
+ ```
235
254
236
255
消费者消代码:
237
256
238
- ``` Java
239
- @KafkaListener (topics = {KafkaConst . TEST_TOPIC },groupId = " apple" )
240
- private void customer(String message) throws InterruptedException {
241
- log. info(" kafka customer:{}" ,message);
242
- Integer n = Integer . parseInt(message);
243
- if (n% 5 == 0 ){
244
- throw new RuntimeException ();
245
- }
246
- }
247
- ```
257
+ ``` Java
258
+ @KafkaListener (topics = {KafkaConst . TEST_TOPIC },groupId = " apple" )
259
+ private void customer(String message) throws InterruptedException {
260
+ log. info(" kafka customer:{}" ,message);
261
+ Integer n = Integer . parseInt(message);
262
+ if (n% 5 == 0 ){
263
+ throw new RuntimeException ();
264
+ }
265
+ }
266
+ ```
248
267
249
- 在默认配置下,当消费异常会进行重试,重试多次后会跳过当前消息,继续进行后续消息的消费,不会一直卡在当前消息。下面是一段消费的日志,可以看出当 test-0@95 重试多次后会被跳过。
268
+ 在默认配置下,当消费异常会进行重试,重试多次后会跳过当前消息,继续进行后续消息的消费,不会一直卡在当前消息。下面是一段消费的日志,可以看出当 ` test-0@95 ` 重试多次后会被跳过。
250
269
251
270
``` Java
252
271
2023 - 08- 10 12 : 03 : 32.918 DEBUG 9700 -- - [ntainer#0 - 0 - C - 1 ] o.s.kafka.listener. DefaultErrorHandler : Skipping seek of: test- 0 @95
@@ -255,23 +274,66 @@ acks 的默认值即为 1,代表我们的消息被 leader 副本接收之后
255
274
256
275
```
257
276
258
- ## 默认会重试多少次?
277
+ 因此,即使某个消息消费异常,Kafka 消费者仍然能够继续消费后续的消息,不会一直卡在当前消息,保证了业务的正常进行。
278
+
279
+ ### 默认会重试多少次?
259
280
260
281
默认配置下,消费异常会进行重试,重试次数是多少, 重试是否有时间间隔?
261
- 10 次。看源码 FailedRecordTracker 类有个 recovered 函数,返回 Boolean 值判断是否要进行重试,下面是这个函数中判断是否重试的逻辑:
262
282
263
- ``` Java
264
- FailedRecord failedRecord = getFailedRecordInstance(record, exception, map, topicPartition);
265
- this . retryListeners. forEach(rl - >
266
- rl. failedDelivery(record, exception, failedRecord. getDeliveryAttempts(). get()));
267
- long nextBackOff = failedRecord. getBackOffExecution(). nextBackOff();
268
- if (nextBackOff != BackOffExecution . STOP ) {
269
- this . backOffHandler. onNextBackOff(container, exception, nextBackOff);
270
- return false ;
271
- }
283
+ 看源码 ` FailedRecordTracker ` 类有个 ` recovered ` 函数,返回 Boolean 值判断是否要进行重试,下面是这个函数中判断是否重试的逻辑:
284
+
285
+ ``` java
286
+ @Override
287
+ public boolean recovered(ConsumerRecord << ? , ? > record, Exception exception,
288
+ @Nullable MessageListenerContainer container,
289
+ @Nullable Consumer << ? , ? > consumer) throws InterruptedException {
290
+
291
+ if (this . noRetries) {
292
+ // 不支持重试
293
+ attemptRecovery(record, exception, null , consumer);
294
+ return true ;
295
+ }
296
+ // 取已经失败的消费记录集合
297
+ Map < TopicPartition , FailedRecord > map = this . failures. get();
298
+ if (map == null ) {
299
+ this . failures. set(new HashMap < > ());
300
+ map = this . failures. get();
301
+ }
302
+ // 获取消费记录所在的Topic和Partition
303
+ TopicPartition topicPartition = new TopicPartition (record. topic(), record. partition());
304
+ FailedRecord failedRecord = getFailedRecordInstance(record, exception, map, topicPartition);
305
+ // 通知注册的重试监听器,消息投递失败
306
+ this . retryListeners. forEach(rl - >
307
+ rl. failedDelivery(record, exception, failedRecord. getDeliveryAttempts(). get()));
308
+ // 获取下一次重试的时间间隔
309
+ long nextBackOff = failedRecord. getBackOffExecution(). nextBackOff();
310
+ if (nextBackOff != BackOffExecution . STOP ) {
311
+ this . backOffHandler. onNextBackOff(container, exception, nextBackOff);
312
+ return false ;
313
+ } else {
314
+ attemptRecovery(record, exception, topicPartition, consumer);
315
+ map. remove(topicPartition);
316
+ if (map. isEmpty()) {
317
+ this . failures. remove();
318
+ }
319
+ return true ;
320
+ }
321
+ }
322
+ ```
323
+
324
+ 其中, ` BackOffExecution.STOP ` 的值为 -1。
325
+
326
+ ``` java
327
+ @FunctionalInterface
328
+ public interface BackOffExecution {
329
+
330
+ long STOP = - 1 ;
331
+ long nextBackOff ();
332
+
333
+ }
272
334
```
273
335
274
- 其中 BackOffExecution.STOP 的值为 -1, nextBackOff 的值调用 BackOff 类的 nextBackOff() 函数。如果当前执行次数大于最大执行次数则返回 STOP,既超过这个最大执行次数后才会停止重试。
336
+ ` nextBackOff ` 的值调用 ` BackOff ` 类的 ` nextBackOff() ` 函数。如果当前执行次数大于最大执行次数则返回 ` STOP ` ,既超过这个最大执行次数后才会停止重试。
275
337
276
338
``` Java
277
339
public long nextBackOff() {
@@ -285,27 +347,29 @@ public long nextBackOff() {
285
347
}
286
348
```
287
349
288
- 那么这个 getMaxAttempts 的值又是多少呢?回到最开始,当执行出错会进入 DefaultErrorHandler 。 DefaultErrorHandler 默认的构造函数是:
350
+ 那么这个 ` getMaxAttempts ` 的值又是多少呢?回到最开始,当执行出错会进入 ` DefaultErrorHandler ` 。 ` DefaultErrorHandler ` 默认的构造函数是:
289
351
290
352
``` Java
291
353
public DefaultErrorHandler() {
292
354
this (null , SeekUtils . DEFAULT_BACK_OFF );
293
355
}
294
356
```
295
357
296
- SeekUtils.DEFAULT_BACK_OFF 定义的是:
358
+ ` SeekUtils.DEFAULT_BACK_OFF ` 定义的是:
297
359
298
360
``` Java
299
361
public static final int DEFAULT_MAX_FAILURES = 10 ;
300
362
301
363
public static final FixedBackOff DEFAULT_BACK_OFF = new FixedBackOff (0 , DEFAULT_MAX_FAILURES - 1 );
302
364
```
303
365
304
- DEFAULT_MAX_FAILURES 的值是10,currentAttempts从0到9,所以总共会执行10次,每次重试的时间间隔为0 。
366
+ ` DEFAULT_MAX_FAILURES ` 的值是 10, ` currentAttempts ` 从 0 到 9,所以总共会执行 10 次,每次重试的时间间隔为 0 。
305
367
306
- ## 如何自定义重试次数,以及时间间隔
368
+ 最后,简单总结一下:Kafka 消费者在默认配置下会进行最多 10 次 的重试,每次重试的时间间隔为 0,即立即进行重试。如果在 10 次重试后仍然无法成功消费消息,则不再进行重试,消息将被视为消费失败。
307
369
308
- 从上面的代码可以知道,默认错误处理器的重试次数以及时间间隔是由 FixedBackOff 控制的,FixedBackOff 是 DefaultErrorHandler 初始化时默认的。所以自定义重试次数以及时间间隔,只需要在 DefaultErrorHandler 初始化的时候传入自定义的 FixedBackOff 即可。重新实现一个 KafkaListenerContainerFactory ,调用 setCommonErrorHandler 设置新的自定义的错误处理器就可以实现。
370
+ ### 如何自定义重试次数以及时间间隔?
371
+
372
+ 从上面的代码可以知道,默认错误处理器的重试次数以及时间间隔是由 ` FixedBackOff ` 控制的,` FixedBackOff ` 是 ` DefaultErrorHandler ` 初始化时默认的。所以自定义重试次数以及时间间隔,只需要在 ` DefaultErrorHandler ` 初始化的时候传入自定义的 ` FixedBackOff ` 即可。重新实现一个 ` KafkaListenerContainerFactory ` ,调用 ` setCommonErrorHandler ` 设置新的自定义的错误处理器就可以实现。
309
373
310
374
``` Java
311
375
@Bean
@@ -319,9 +383,9 @@ public KafkaListenerContainerFactory kafkaListenerContainerFactory(ConsumerFacto
319
383
}
320
384
```
321
385
322
- ## 如何在重试失败后进行告警
386
+ ### 如何在重试失败后进行告警?
323
387
324
- 自定义重试失败后逻辑,需要手动实现,以下是一个简单的例子,重写 DefaultErrorHandler 的 handleRemaining 函数,加上自定义的告警等操作。
388
+ 自定义重试失败后逻辑,需要手动实现,以下是一个简单的例子,重写 ` DefaultErrorHandler ` 的 ` handleRemaining ` 函数,加上自定义的告警等操作。
325
389
326
390
``` Java
327
391
@Slf4j
@@ -339,17 +403,19 @@ public class DelErrorHandler extends DefaultErrorHandler {
339
403
}
340
404
}
341
405
```
342
- DefaultErrorHandler 只是默认的一个错误处理器,Spring kafka 还提供了 CommonErrorHandler 接口。手动实现 CommonErrorHandler 就可以实现更多的自定义操作,有很高的灵活性。例如根据不同的错误类型,实现不同的重试逻辑以及业务逻辑等。
343
406
344
- ## 重试失败后的数据如何再次处理
407
+ ` DefaultErrorHandler ` 只是默认的一个错误处理器,Spring Kafka 还提供了 ` CommonErrorHandler ` 接口。手动实现 ` CommonErrorHandler ` 就可以实现更多的自定义操作,有很高的灵活性。例如根据不同的错误类型,实现不同的重试逻辑以及业务逻辑等。
408
+
409
+ ### 重试失败后的数据如何再次处理?
345
410
346
411
当达到最大重试次数后,数据会直接被跳过,继续向后进行。当代码修复后,如何重新消费这些重试失败的数据呢?
347
412
348
- 死信队列(Dead Letter Queue,简称DLQ) 是消息中间件中的一种特殊队列。它主要用于处理无法被消费者正确处理的消息,通常是因为消息格式错误、处理失败、消费超时等情况导致的消息被"丢弃"或"死亡"的情况。当消息进入队列后,消费者会尝试处理它。如果处理失败,或者超过一定的重试次数仍无法被成功处理,消息可以发送到死信队列中,而不是被永久性地丢弃。在死信队列中,可以进一步分析、处理这些无法正常消费的消息,以便定位问题、修复错误,并采取适当的措施。
413
+ ** 死信队列(Dead Letter Queue,简称 DLQ) ** 是消息中间件中的一种特殊队列。它主要用于处理无法被消费者正确处理的消息,通常是因为消息格式错误、处理失败、消费超时等情况导致的消息被"丢弃"或"死亡"的情况。当消息进入队列后,消费者会尝试处理它。如果处理失败,或者超过一定的重试次数仍无法被成功处理,消息可以发送到死信队列中,而不是被永久性地丢弃。在死信队列中,可以进一步分析、处理这些无法正常消费的消息,以便定位问题、修复错误,并采取适当的措施。
349
414
350
415
` @RetryableTopic ` 是 Spring Kafka 中的一个注解,它用于配置某个 Topic 支持消息重试,更推荐使用这个注解来完成重试。
351
416
352
417
``` Java
418
+ // 重试 5 次,重试间隔 100 毫秒,最大间隔 1 秒
353
419
@RetryableTopic (
354
420
attempts = " 5" ,
355
421
backoff = @Backoff (delay = 100 , maxDelay = 1000 )
@@ -365,17 +431,9 @@ private void customer(String message) {
365
431
}
366
432
```
367
433
368
- 这个例子在listen方法上使用@RetryableTopic 注解,配置了:
369
- - 重试5次
370
- - 重试间隔100毫秒,最大间隔1秒
371
-
372
- 重试完毕后,如果仍然失败,则会进入消息队列,此时就存在两个队列:
373
- - test 原队列
374
- - test-dlt 原队列对应的死信队列
375
-
376
- 对于死信队列的处理,既可以用 ` @DltHandler ` 处理,也可以使用 ` @KafkaListener ` 重新消费。
434
+ 当达到最大重试次数后,如果仍然无法成功处理消息,消息会被发送到对应的死信队列中。对于死信队列的处理,既可以用 ` @DltHandler ` 处理,也可以使用 ` @KafkaListener ` 重新消费。
377
435
378
- ### Reference
436
+ ## 参考
379
437
380
438
- Kafka 官方文档:https://kafka.apache.org/documentation/
381
439
- 极客时间—《Kafka 核心技术与实战》第 11 节:无消息丢失配置怎么实现?
0 commit comments