对于最新的稳定版本,请使用 spring-cloud-stream 4.3.0! |
死信主题处理
启用 DLQ
要启用 DLQ,基于 Kafka 绑定器的应用程序必须通过属性spring.cloud.stream.bindings.<binding-name>.group
. 匿名使用者组(即应用程序未显式提供组)无法启用 DLQ 功能。
当应用程序想要将错误的记录发送到 DLQ 主题时,该应用程序必须启用 DLQ 功能,因为缺省情况下未启用此功能。要启用 DLQ,请属性spring.cloud.stream.kafka.bindings.<binding-name>.consumer.enable-dlq
必须设置为 true。
启用 DLQ 后,在处理中发生错误后,所有重试都会根据spring.cloud.stream.bindings.<binding-name>.consumer.max-attempts
属性,则该记录将发送到 DLQ 主题。
默认情况下,max-attempts
属性设置为 3。 什么时候max-attempts
属性大于1
,并且启用了 dlq,那么您将看到重试遵循max-attempts
财产。 当未启用 dlq(这是默认值)时,则max-attempts
属性对重试的处理方式没有任何影响。在这种情况下,重试将回退到 Spring for Apache Kafka 中的容器默认值,即10
重试。 如果应用程序想要在禁用 DLQ 时完全禁用重试,则将max-attempts
属性设置为1
将不起作用。要在这种情况下完全禁用重试,您需要提供一个ListenerContainerCustomizer
然后使用适当的Backoff
设置。 这是一个例子。
@Bean
ListenerContainerCustomizer<AbstractMessageListenerContainer<?, ?>> customizer() {
return (container, destinationName, group) -> {
var commonErrorHandler = new DefaultErrorHandler(new FixedBackOff(0L, 0l));
container.setCommonErrorHandler(commonErrorHandler);
};
}
这样,默认容器行为将被禁用,并且不会尝试重试。如上所述,启用 DLQ 时,活页夹设置将具有优先权。
处理死信主题中的记录
由于该框架无法预测用户希望如何处理死信消息,因此它没有提供任何标准机制来处理它们。如果死信的原因是暂时的,您可能希望将消息路由回原始主题。但是,如果问题是永久性问题,则可能会导致无限循环。本主题中的示例 Spring Boot 应用程序是如何将这些消息路由回原始主题的示例,但它在三次尝试后将它们移动到“停车场”主题。该应用程序是另一个从死信主题中读取的 spring-cloud-stream 应用程序。当 5 秒内没有收到消息时,它就会退出。
这些示例假定原始目标为so8400out
消费群体是so8400
.
有几种策略需要考虑:
-
考虑仅在主应用程序未运行时运行重新路由。否则,暂时性错误的重试很快就会用完。
-
或者,使用两阶段方法:使用此应用程序路由到第三个主题,另一个应用程序从那里路由回主主题。
以下代码列表显示了示例应用程序:
spring.cloud.stream.bindings.input.group=so8400replay
spring.cloud.stream.bindings.input.destination=error.so8400out.so8400
spring.cloud.stream.bindings.output.destination=so8400out
spring.cloud.stream.bindings.parkingLot.destination=so8400in.parkingLot
spring.cloud.stream.kafka.binder.configuration.auto.offset.reset=earliest
spring.cloud.stream.kafka.binder.headers=x-retries
@SpringBootApplication
public class ReRouteDlqKApplication implements CommandLineRunner {
private static final String X_RETRIES_HEADER = "x-retries";
public static void main(String[] args) {
SpringApplication.run(ReRouteDlqKApplication.class, args).close();
}
private final AtomicInteger processed = new AtomicInteger();
@Autowired
private StreamBridge streamBridge;
@Bean
public Function<Message<?>, Message<?>> reRoute() {
return failed -> {
processed.incrementAndGet();
Integer retries = failed.getHeaders().get(X_RETRIES_HEADER, Integer.class);
if (retries == null) {
System.out.println("First retry for " + failed);
return MessageBuilder.fromMessage(failed)
.setHeader(X_RETRIES_HEADER, 1)
.setHeader(BinderHeaders.PARTITION_OVERRIDE,
failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID))
.build();
}
else if (retries < 3) {
System.out.println("Another retry for " + failed);
return MessageBuilder.fromMessage(failed)
.setHeader(X_RETRIES_HEADER, retries + 1)
.setHeader(BinderHeaders.PARTITION_OVERRIDE,
failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID))
.build();
}
else {
System.out.println("Retries exhausted for " + failed);
streamBridge.send("parkingLot", MessageBuilder.fromMessage(failed)
.setHeader(BinderHeaders.PARTITION_OVERRIDE,
failed.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID))
.build());
}
return null;
};
}
@Override
public void run(String... args) throws Exception {
while (true) {
int count = this.processed.get();
Thread.sleep(5000);
if (count == this.processed.get()) {
System.out.println("Idle, exiting");
return;
}
}
}
}