Spring整合Kafka(十八)----非阻塞重试

【Spring连载】使用Spring访问 Apache Kafka(十八)----非阻塞重试Non-Blocking Retries


版本2.9将机制更改为bootstrap基础设施bean;请参阅 配置,了解引导此功能所需的两种机制。
使用Kafka实现无阻塞重试/dlt功能通常需要设置额外的主题并创建和配置相应的监听器。由于2.7 Spring for Apache Kafka通过@RetryableTopic注解和RetryTopicConfiguration类提供了对它的支持,以简化bootstrap。
batch监听器不支持非阻塞重试。

一、这种模式是如何运作的How The Pattern Works

如果消息处理失败,则会将消息转发到具有back off时间戳的重试主题。重试主题consumer检查时间戳,如果不到期,则暂停该主题分区的消费。到期时,将恢复分区消费,消息再次被消费。如果消息处理再次失败,则消息将转发到下一个重试主题,并重复该模式,直到成功处理或尝试次数用完,并将消息发送到死信主题Dead Letter Topic(如果已配置)。
例如,如果你有一个"main-topic"主题,并且希望设置非阻塞重试,其指数退避(exponential backoff)为1000ms,乘数为2,最大尝试次数4次,则它将创建 main-topic-retry-1000, main-topic-retry-2000, main-topic-retry-4000 和 main-topic-dlt 主题,并配置相应的consumers。框架还负责创建主题以及设置和配置监听器。
通过使用这种策略,你将失去Kafka对该主题的排序保证。
你可以设置自己喜欢的AckMode模式,但建议使用RECORD。
目前,此功能不支持类级别的@KafkaListener注解。
当使用asyncAcks设置为true的手动AckMode时,DefaultErrorHandler必须配置为seekAfterError设置为false。从2.9.10和3.0.8版本开始,对于此类配置,这将无条件设置为true。对于早期版本,有必要重写RetryConfigurationSupport.configureCustomizers()方法以将属性设置为true。

@Override
protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
    customizersConfigurer.customizeErrorHandler(eh -> eh.setSeekAfterError(false));
}

此外,在这些版本之前,无论asyncAcks属性如何,使用默认(日志记录)DLT处理程序都与任何类型的手动AckMode不兼容。

二、重试等待策略延迟精度Back Off Delay Precision

概述和保证Overview and Guarantees

所有消息处理和BackOff都由consumer线程处理,因此,在尽最大努力的基础上保证了延迟精度。如果一条消息的处理时间比该消费者的下一条消息back off时间长,则下一条信息的延迟将高于预期。此外,对于短延迟(约1秒或更短),线程必须做的维护工作,如提交偏移量,可能会延迟消息处理的执行。如果重试主题的consumer正在处理多个分区,则精度也会受到影响,因为我们依赖于从轮询中唤醒consumer,并使用完整的pollTimeouts来进行时间调整。
也就是说,对于处理单个分区的consumers来说,在大多数情况下,消息的处理应该大致在其确切的到期时间进行。
消息在到期时间之前永远不会被处理是保证的。

三、配置Configuration

从2.9版本开始,对于默认配置,@EnableKafkaRetryTopic注释应该在@Configuration注解类中使用。这使该功能能够正确地引导,并允许在运行时注入要查找的一些功能组件。
如果添加此注解,则不必同时添加@EnableKafka,因为@EnableKafkaRetryTopic是用@EnableKafka进行元注释的。
此外,从该版本开始,对于功能组件和全局功能的更高级配置,RetryTopicConfigurationSupport类应在@Configuration类中进行继承,并覆盖适当的方法。有关更多详细信息,请参阅配置全局设置和特性
默认情况下,重试主题的容器将与主容器具有相同的并发性。从3.0版本开始,你可以为重试容器设置不同的并发性(可以在注解上,也可以在RetryConfigurationBuilder中)。
只能使用上述技术中的一种,并且只有一个@Configuration类可以扩展RetryTopicConfigurationSupport。

3.1 使用@RetryableTopic注解Using the @RetryableTopic annotation

要为@KafkaListener注解的方法配置重试主题和dlt,只需向其中添加@RetryableTopic注解,Spring for Apache Kafka将使用默认配置引导所有必要的topic和consumer。

@RetryableTopic(kafkaTemplate = "myRetryableTopicKafkaTemplate")
@KafkaListener(topics = "my-annotated-topic", groupId = "myGroupId")
public void processMessage(MyPojo message) {
        // ... message processing
}

你可以在同一个类中指定一个方法来处理dlt消息,方法是对其使用@DltHandler注解。如果没有提供DltHandler方法,则会创建一个只记录消费的默认consumer。

@DltHandler
public void processMessage(MyPojo message) {
// ... message processing, persistence, etc
}

如果你没有指定kafkaTemplate名称,将查找名称为defaultRetryTopicKafkaTemplate的bean。如果没有找到bean,则抛出异常。
从3.0版本开始,@RetryableTopic注解可以用作自定义注解上的元注释;例如:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@RetryableTopic
static @interface MetaAnnotatedRetryableTopic {

    @AliasFor(attribute = "concurrency", annotation = RetryableTopic.class)
    String parallelism() default "3";

}

3.2 使用 RetryTopicConfiguration beans

你还可以通过在@Configuration注解类中创建RetryTopicConfiguration bean来配置非阻塞重试支持。

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, Object> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .create(template);
}

这将使用默认配置为带有“@KafkaListener”注解的方法中的所有主题创建重试主题和dlt,以及相应的consumer。消息转发需要KafkaTemplate实例。
为了对如何处理每个主题的非阻塞重试实现更细粒度的控制,可以提供多个RetryTopicConfiguration bean。

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .fixedBackOff(3000)
            .maxAttempts(5)
            .concurrency(1)
            .includeTopics("my-topic", "my-other-topic")
            .create(template);
}

@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<String, MyOtherPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .exponentialBackoff(1000, 2, 5000)
            .maxAttempts(4)
            .excludeTopics("my-topic", "my-other-topic")
            .retryOn(MyException.class)
            .create(template);
}

重试主题和dlt(dead-letter topic)的consumer将被分配到一个consumer组,该consumer组的组id是您在@KafkaListener注解的groupId参数中提供的带有主题后缀的组id的组合。如果你不提供任何内容,它们都将属于同一组,并且在重试主题上再平衡(rebalance)将导致在main topic上进行不必要的再平衡。
如果consumer使用者配置了ErrorHandlingDeserializer,以处理反序列化异常,那么为KafkaTemplate及其生产者配置一个serializer是很重要的,该serializer可以处理正常对象以及反序列化异常产生的原始byte[]值。template的泛型值类型应为Object。一种技术是使用DelegatingByTypeSerializer;示例如下:

@Bean
public ProducerFactory<String, Object> producerFactory() {
  return new DefaultKafkaProducerFactory<>(producerConfiguration(), new StringSerializer(),
    new DelegatingByTypeSerializer(Map.of(byte[].class, new ByteArraySerializer(),
          MyNormalObject.class, new JsonSerializer<Object>())));
}

@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
  return new KafkaTemplate<>(producerFactory());
}

多个@KafkaListener注解可以用于同一主题,无论是否手动分配分区以及非阻塞重试,但给定主题只能使用一个配置。最好使用单个RetryTopicConfiguration bean来配置此类主题;如果多个@RetryableTopic注解用于同一主题,则所有注解都应具有相同的值,否则其中一个注解将应用于该主题的所有监听器,而其他注解的值将被忽略。

3.3 配置全局设置和特性Configuring Global Settings and Features

自2.9版本以来,以前用于配置组件的bean覆盖方法已被删除(由于API的上述实验性质,没有弃用)。这不会更改RetryTopicConfiguration beans方法,只更改infrastructure组件的配置。现在,RetryTopicConfigurationSupport类应该在一个(单个)@Configuration类中进行继承,并覆盖正确的方法。示例如下:

@EnableKafka
@Configuration
public class MyRetryTopicConfiguration extends RetryTopicConfigurationSupport {

    @Override
    protected void configureBlockingRetries(BlockingRetriesConfigurer blockingRetries) {
        blockingRetries
                .retryOn(MyBlockingRetriesException.class, MyOtherBlockingRetriesException.class)
                .backOff(new FixedBackOff(3000, 3));
    }

    @Override
    protected void manageNonBlockingFatalExceptions(List<Class<? extends Throwable>> nonBlockingFatalExceptions) {
        nonBlockingFatalExceptions.add(MyNonBlockingException.class);
    }

    @Override
    protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
        // Use the new 2.9 mechanism to avoid re-fetching the same records after a pause
        customizersConfigurer.customizeErrorHandler(eh -> {
            eh.setSeekAfterError(false);
        });
    }

}

使用这种配置方法时,不应使用@EnableKafkaRetryTopic注解来防止由于重复的bean而导致上下文无法启动。请改用简单的@EnableKafka注解。
当autoCreateTopics为true时,将使用指定的分区数和replication因子创建main topic和retry topic。从3.0版本开始,默认的replication因子是-1,这意味着使用broker默认值。如果你的broker版本早于2.4,则需要设置一个显式值。要覆盖特定主题(例如main主题或DLT)的这些值,只需添加一个具有所需属性的NewTopic @Bean;将覆盖自动创建属性。
默认情况下,发布记录到重试主题将使用接收到的记录的原始分区。如果重试主题的分区少于main主题的分区,则应适当配置框架;下面是一个例子。

@EnableKafka
@Configuration
public class Config extends RetryTopicConfigurationSupport {

    @Override
    protected Consumer<DeadLetterPublishingRecovererFactory> configureDeadLetterPublishingContainerFactory() {
        return dlprf -> dlprf.setPartitionResolver((cr, nextTopic) -> null);
    }

    ...

}

函数的参数是consumer record和下一个topic的名称。你可以返回一个特定的分区编号,也可以返回null以指示KafkaProducer应该确定分区。
默认情况下,当记录通过重试主题转换时,会保留重试header的所有值(尝试次数、时间戳)。从2.9.6版本开始,如果你只想保留这些标头的最后一个值,请使用上面显示的configureDeadLetterPublishingContainerFactory()方法将工厂的retainAllRetryHeaderValues属性设置为false。

四、程序化构建Programmatic Construction

该功能旨在与@KafkaListener一起使用;但是,一些用户请求提供有关如何以编程方式配置非阻塞重试的信息。下面的Spring Boot应用程序提供了一个如何执行此操作的示例。

@SpringBootApplication
public class Application extends RetryTopicConfigurationSupport {

    public static void main(String[] args) {
        SpringApplication.run(2Application.class, args);
    }

    @Bean
    RetryTopicConfiguration retryConfig(KafkaTemplate<String, String> template) {
        return RetryTopicConfigurationBuilder.newInstance()
                .maxAttempts(4)
                .autoCreateTopicsWith(2, (short) 1)
                .create(template);
    }

    @Bean
    TaskScheduler scheduler() {
        return new ThreadPoolTaskScheduler();
    }

    @Bean
    @Order(0)
    SmartInitializingSingleton dynamicRetry(RetryTopicConfigurer configurer, RetryTopicConfiguration config,
            KafkaListenerAnnotationBeanPostProcessor<?, ?> bpp, KafkaListenerContainerFactory<?> factory,
            Listener listener, KafkaListenerEndpointRegistry registry) {

        return () -> {
            KafkaListenerEndpointRegistrar registrar = bpp.getEndpointRegistrar();
            MethodKafkaListenerEndpoint<String, String> mainEndpoint = new MethodKafkaListenerEndpoint<>();
            EndpointProcessor endpointProcessor = endpoint -> {
                // customize as needed (e.g. apply attributes to retry endpoints).
                if (!endpoint.equals(mainEndpoint)) {
                    endpoint.setConcurrency(1);
                }
                // these are required
                endpoint.setMessageHandlerMethodFactory(bpp.getMessageHandlerMethodFactory());
                endpoint.setTopics("topic");
                endpoint.setId("id");
                endpoint.setGroupId("group");
            };
            mainEndpoint.setBean(listener);
            try {
                mainEndpoint.setMethod(Listener.class.getDeclaredMethod("onMessage", ConsumerRecord.class));
            }
            catch (NoSuchMethodException | SecurityException ex) {
                throw new IllegalStateException(ex);
            }
            mainEndpoint.setConcurrency(2);
            mainEndpoint.setTopics("topic");
            mainEndpoint.setId("id");
            mainEndpoint.setGroupId("group");
            configurer.processMainAndRetryListeners(endpointProcessor, mainEndpoint, config, registrar, factory,
                    "kafkaListenerContainerFactory");
        };
    }


    @Bean
    ApplicationRunner runner(KafkaTemplate<String, String> template) {
        return args -> {
            template.send("topic", "test");
        };
    }

}

@Component
class Listener implements MessageListener<String, String> {

    @Override
    public void onMessage(ConsumerRecord<String, String> record) {
        System.out.println(KafkaUtils.format(record));
        throw new RuntimeException("test");
    }

}

只有在刷新应用程序上下文之前处理了配置,才会自动创建主题,如上面的示例所示。要在运行时配置容器,需要使用其他技术创建主题。

五、特性Features

大多数特性对于@RetryableTopic注释和RetryTopicConfiguration bean都可用。

5.1 重试等待策略配置BackOff Configuration

BackOff配置依赖于Spring Retry项目中的BackOffPolicy接口。
它包括:

  • Fixed Back Off
  • Exponential Back Off
  • Random Exponential Back Off
  • Uniform Random Back Off
  • No Back Off
  • Custom Back Off
@RetryableTopic(attempts = 5,
    backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000))
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .fixedBackoff(3000)
            .maxAttempts(4)
            .create(template);
}

你也可以提供Spring Retry的SleepingBackOffPolicy接口的自定义实现:

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .customBackOff(new MyCustomBackOffPolicy())
            .maxAttempts(5)
            .create(template);
}

默认的backoff策略是FixedBackOffPolicy,最多3次尝试,间隔1000ms。
ExponentialBackOffPolicy的默认最大延迟为30秒。如果您的back off策略需要值大于该值的延迟,请相应地调整maxDelay属性。
第一次尝试会计入maxAttempts,因此如果你提供的maxAttempts值为4,则会有原始尝试加上3次重试。

5.2 全局超时Global timeout

你可以设置重试过程的全局超时时间。如果达到该时间,则下一次消费者抛出异常时,消息将直接发送到DLT(dead-letter topic),或者在没有DLT可用的情况下结束处理。

@RetryableTopic(backoff = @Backoff(2000), timeout = 5000)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .fixedBackoff(2000)
            .timeoutAfter(5000)
            .create(template);
}

默认值是没有超时设置,这也可以通过提供-1作为超时值来实现。

5.3 异常分类器Exception Classifier

你可以指定要对哪些异常重试,哪些不重试。还可以将其设置为遍历原因(traverse the causes)以查找嵌套异常。

@RetryableTopic(include = {MyRetryException.class, MyOtherRetryException.class}, traversingCauses = true)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        throw new RuntimeException(new MyRetryException()); // Will retry
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyOtherPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .notRetryOn(MyDontRetryException.class)
            .create(template);
}

默认行为是对所有异常重试,而不是遍历原因。

自2.8.3以来,有一个fatal异常的全局列表,这将导致记录在没有任何重试的情况下被发送到DLT。有关致命异常的默认列表,请参阅DefaultErrorHandler。通过继承RetryTopicConfigurationSupport重写@Configuration类中的configureNonBlockingRetries方法,可以向该列表添加异常或从中删除异常。有关详细信息,请参阅配置全局设置和特性

@Override
protected void manageNonBlockingFatalExceptions(List<Class<? extends Throwable>> nonBlockingFatalExceptions) {
    nonBlockingFatalExceptions.add(MyNonBlockingException.class);
}

要禁用fatal异常分类,只需清除提供的列表。

5.4 包括和排除主题Include and Exclude Topics

你可以通过. includeTopic(String topic)、. includeTopics(Collection topics)、 .excludeTopic(String topic)和 .excludeTopics(Collection topics) 方法来决定哪些主题将由RetryTopicConfiguration bean处理,哪些不处理。

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .includeTopics(List.of("my-included-topic", "my-other-included-topic"))
            .create(template);
}

@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .excludeTopic("my-excluded-topic")
            .create(template);
}

默认行为是包含所有主题。

5.5 主题自动创建Topics AutoCreation

除非另有说明,否则框架将使用KafkaAdmin bean的NewTopic bean自动创建所需的主题。你可以指定创建主题时使用的分区数量和复制因子,也可以关闭此功能。从3.0版本开始,默认复制因子为-1,这意味着使用broker默认值。如果您的broker版本早于2.4,则需要设置显式值。
注意,如果你不使用Spring Boot,则必须提供KafkaAdmin bean才能使用此功能。

@RetryableTopic(numPartitions = 2, replicationFactor = 3)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}

@RetryableTopic(autoCreateTopics = false)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .autoCreateTopicsWith(2, 3)
            .create(template);
}

@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .doNotAutoCreateRetryTopics()
            .create(template);
}

默认情况下,主题是自动创建的,只有一个分区,复制因子为-1(意味着使用代理默认值)。如果您的代理版本早于2.4,则需要显式设置值。

5.6 失败报头管理Failure Header Management

在考虑如何管理失败header(原始header和异常header)时,框架将委托给DeadLetterPublishingRecover来决定是否附加或替换标头。
默认情况下,它将appendOriginalHeaders显式设置为false,并将stripPreviousExceptionHeaders保留为DeadLetterPublishingRecover使用的默认值。
这意味着只有第一个“原始”的和最后一个异常header与默认配置一起保留。这是为了避免在涉及许多重试步骤时创建过大的消息(例如,由于堆栈跟踪header)。
有关详细信息,请参阅管理死信记录头
要将框架重新配置为对这些属性使用不同的设置,请通过在一个@Configuration类继承RetryTopicConfigurationSupport,覆盖configureCustomizers方法来配置DeadLetterPublishingRecoverer自定义程序。有关更多详细信息,请参阅配置全局设置和特性

@Override
protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
    customizersConfigurer.customizeDeadLetterPublishingRecoverer(dlpr -> {
        dlpr.setAppendOriginalHeaders(true);
        dlpr.setStripPreviousExceptionHeaders(false);
    });
}

从2.8.4版本开始,如果你希望添加自定义标头(除了工厂添加的重试信息标头外,还可以向工厂添加headersFunction,factory.setHeadersFunction((rec, ex) → { …​ })。
默认情况下,添加的任何头都是累积的——Kafka标头可以包含多个值。从2.9.5版本开始,如果函数返回的Headers包含类型为“DeadLetterPublishingRecover.SingleRecordHeader”的标头,则该标头的任何现有值都将被删除,只保留新的单个值。

5.7 自定义死信发布恢复器Custom DeadLetterPublishingRecoverer

正如在Failure Header Management中看到的那样,你可以自定义框架创建的默认DeadLetterPublishingRecoverer实例。但是,对于某些用例,有创建DeadLetterPublishingRecoverer的子类,例如覆盖createProducerRecord()以修改发送到重试(或死信)主题的内容。从3.0.9版本开始,您可以覆盖RetryConfigurationSupport.configureDeadLetterPublishingContainerFactory()方法以提供DeadLetterPublisherCreator实例,例如:

@Override
protected Consumer<DeadLetterPublishingRecovererFactory>
        configureDeadLetterPublishingContainerFactory() {

    return (factory) -> factory.setDeadLetterPublisherCreator(
            (templateResolver, destinationResolver) ->
                    new CustomDLPR(templateResolver, destinationResolver));
}

建议您在构造自定义实例时使用提供的解析器。

六、合并阻塞和非阻塞重试Combining Blocking and Non-Blocking Retries

从2.8.4开始,您可以将框架配置为同时使用阻塞和非阻塞重试。例如,可以有一组异常,这些异常也可能会在下一条记录上继续触发错误,例如DatabaseAccessException,因此你可以在将同一条记录发送到重试主题或直接发送到DLT之前重试几次。
要配置阻塞重试,请在一个@Configuration类中继承RetryTopicConfigurationSupport,覆盖configureBlockingRetries方法,并添加要重试的异常以及要使用的BackOff。默认的BackOff是FixedBackOff,它没有延迟和有9次尝试。有关详细信息,请参阅配置全局设置和特性

@Override
protected void configureBlockingRetries(BlockingRetriesConfigurer blockingRetries) {
    blockingRetries
            .retryOn(MyBlockingRetryException.class, MyOtherBlockingRetryException.class)
            .backOff(new FixedBackOff(3000, 5));
}

结合全局可重试主题的fatal异常分类,你可以为您想要的任何行为配置框架,例如让一些异常同时触发阻塞和非阻塞重试,只触发一种或另一种,或者直接进入DLT而不进行任何类型的重试。
以下是两种配置协同工作的示例:

@Override
protected void configureBlockingRetries(BlockingRetriesConfigurer blockingRetries) {
    blockingRetries
            .retryOn(ShouldRetryOnlyBlockingException.class, ShouldRetryViaBothException.class)
            .backOff(new FixedBackOff(50, 3));
}

@Override
protected void manageNonBlockingFatalExceptions(List<Class<? extends Throwable>> nonBlockingFatalExceptions) {
    nonBlockingFatalExceptions.add(ShouldSkipBothRetriesException.class);
}

在本例中:

  • ShouldRetryOnlyBlockingException.class将仅通过阻塞重试,如果所有重试都失败,消息将直接进入DLT。
  • ShouldRetryViaBothException.class将通过阻塞重试,如果所有阻塞重试都失败,则将消息转发到下一个重试主题进行另一组尝试。
  • ShouldSkipBothRetriesException.class永远不会以任何方式重试,如果第一次处理尝试失败,消息将直接进入DLT。
    请注意,阻塞重试行为是allowlist–你可以添加你确实希望以这种方式重试的异常;而非阻塞重试分类是针对FATAL异常的,因此是denylist–你添加了不想进行非阻塞重试的异常,消息会直接发送到DLT。
    非阻塞异常分类行为还取决于特定主题的配置。

七、访问投递尝试次数Accessing Delivery Attempts

为了访问阻塞和非阻塞投递尝试次数,将这些头添加到你的@KafkaListener方法签名中:

@Header(KafkaHeaders.DELIVERY_ATTEMPT) int blockingAttempts,
@Header(name = RetryTopicHeaders.DEFAULT_HEADER_ATTEMPTS, required = false) Integer nonBlockingAttempts

只有当您将ContainerProperties.deliveryAttemptHeader设置为true时,才会提供阻塞投递尝试次数。
请注意,对于初始投递,非阻塞尝试将为空。
从3.0.10版本开始,提供了一个方便的KafkaMessageHeaderAccessor,可以更简单地访问这些头;访问器可以作为监听器方法的参数提供:

@RetryableTopic(backoff = @Backoff(...))
@KafkaListener(id = "dh1", topics = "dh1")
void listen(Thing thing, KafkaMessageHeaderAccessor accessor) {
    ...
}

使用accessor.getBlockingRetryDeliveryAttempt() 和 accessor.getNonBlockingRetryDeliveryAttempt()获取值。如果未启用阻塞重试,访问器将抛出IllegalStateException;对于非阻塞重试,访问器为初始投递返回1。

八、主题命名Topic Naming

重试主题和DLT(dead letter topic)的命名方法是在main topic后面加上一个提供的或默认的值,再加上该主题的延迟或索引。
示例:
“my-topic” → “my-topic-retry-0”, “my-topic-retry-1”, …​, “my-topic-dlt”

“my-other-topic” → “my-topic-myRetrySuffix-1000”, “my-topic-myRetrySuffix-2000”, …​, “my-topic-myDltSuffix”.
默认行为是为每次尝试创建单独的重试主题,并附加一个索引值:retry-0,retry-1…​, 重试。因此,默认情况下,重试主题的数量是配置的maxAttempts减去1。
你可以配置后缀(8.1),选择是追加尝试索引还是延迟(8.2),在使用fixed backoff时使用单个重试主题(8.3),在使用exponential backoffs时对具有maxInterval的尝试使用单个重试主题(8.4)。

8.1 重试主题和Dlt后缀Retry Topics and Dlt Suffixes

你可以指定重试和Dlt主题将使用的后缀。

@RetryableTopic(retryTopicSuffix = "-my-retry-suffix", dltTopicSuffix = "-my-dlt-suffix")
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyOtherPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .retryTopicSuffix("-my-retry-suffix")
            .dltTopicSuffix("-my-dlt-suffix")
            .create(template);
}

默认后缀是“-retry”和“-dlt”,分别用于重试主题和dlt主题。

8.2 追加主题的索引或延迟Appending the Topic’s Index or Delay

你可以将主题的索引值或延迟值追加在后缀之后。

@RetryableTopic(topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .suffixTopicsWithIndexValues()
            .create(template);
    }

默认行为是使用延迟值的后缀,但具有多个主题的固定延迟配置除外,在这种情况下,topic以自身的索引作为后缀。

8.3 固定延迟重试的单个主题Single Topic for Fixed Delay Retries

如果你使用固定延迟策略,如FixedBackOffPolicy或NoBackOffPolicy,则可以使用单个主题来完成非阻塞重试。该主题将以提供的或默认的后缀作为后缀,并且不会追加索引值或延迟值。
以前的FixedDelayStrategy现在已弃用,可以用SameIntervalTopicReuseStrategy代替。

@RetryableTopic(backoff = @Backoff(2000), fixedDelayTopicStrategy = FixedDelayStrategy.SINGLE_TOPIC)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .fixedBackoff(3000)
            .maxAttempts(5)
            .useSingleTopicForFixedDelays()
            .create(template);
}

默认行为是为每次尝试创建单独的重试主题,并附加其索引值:retry-0, retry-1,…

8.4 maxInterval指数延迟的单个主题Single Topic for maxInterval Exponential Delay

如果使用指数退避策略(ExponentialBackOffPolicy),则可以使用单个重试主题来完成非阻塞重试,其中尝试的延迟为maxInterval。
这个“final”重试主题将以提供的或默认的后缀作为后缀,并将追加索引或maxInterval值。
通过选择使用单个主题进行maxInterval延迟的重试,配置长时间保持重试的指数重试策略(exponential retry policy)可能会变得更可行,因为在这种方法中,你不需要大量的主题。
默认行为是使重试主题的数量等于配置的maxAttempts减1,并且当使用exponential backoff时,重试主题以延迟值为后缀,最后一个重试主题(对应于maxInterval延迟)以追加索引为后缀。
例如,当配置initialInterval=1000、multiplier=2和maxInterval=16000的exponential backoff时,为了持续尝试一个小时,需要将maxAttempts配置为229,默认情况下,所需的重试主题为:

  • retry-1000
  • retry-2000
  • retry-4000
  • retry-8000
  • retry-16000-0
  • retry-16000-1
  • retry-16000-2
    …​
  • retry-16000-224

当使用在相同间隔重复使用重试主题的策略时,在上述相同配置中,所需的重试主题为:

  • retry-1000
  • retry-2000
  • retry-4000
  • retry-8000
  • retry-16000
    这将是未来版本中的默认设置。
@RetryableTopic(attempts = 230,
    backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 16000),
    sameIntervalTopicReuseStrategy = SameIntervalTopicReuseStrategy.SINGLE_TOPIC)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .exponentialBackoff(1000, 2, 16000)
            .maxAttempts(230)
            .useSingleTopicForSameIntervals()
            .create(template);
}

8.5 自定义命名策略Custom naming strategies

更复杂的命名策略可以通过注册一个实现RetryTopicNamesProviderFactory的bean来完成。默认实现是SuffixingRetryTopicNamesProviderFactory,可以通过以下方式注册不同的实现:

@Override
protected RetryTopicComponentFactory createComponentFactory() {
    return new RetryTopicComponentFactory() {
        @Override
        public RetryTopicNamesProviderFactory retryTopicNamesProviderFactory() {
            return new CustomRetryTopicNamesProviderFactory();
        }
    };
}

作为一个例子,下面的实现,除了标准后缀,添加一个前缀到重试主题和dlt的名称:

public class CustomRetryTopicNamesProviderFactory implements RetryTopicNamesProviderFactory {

    @Override
    public RetryTopicNamesProvider createRetryTopicNamesProvider(
                DestinationTopic.Properties properties) {

        if(properties.isMainEndpoint()) {
            return new SuffixingRetryTopicNamesProvider(properties);
        }
        else {
            return new SuffixingRetryTopicNamesProvider(properties) {

                @Override
                public String getTopicName(String topic) {
                    return "my-prefix-" + super.getTopicName(topic);
                }

            };
        }
    }
}

九、多监听器,相同主题Multiple Listeners, Same Topic(s)

从3.0版本开始,现在可以在同一主题上配置多个侦听器。为此,必须使用自定义主题命名将重试主题彼此隔离开来。用一个例子最好地说明了这一点:

@RetryableTopic(...
        retryTopicSuffix = "-listener1", dltTopicSuffix = "-listener1-dlt",
        topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE)
@KafkaListener(id = "listener1", groupId = "group1", topics = TWO_LISTENERS_TOPIC, ...)
void listen1(String message, @Header(KafkaHeaders.RECEIVED_TOPIC) String receivedTopic) {
    ...
}

@RetryableTopic(...
        retryTopicSuffix = "-listener2", dltTopicSuffix = "-listener2-dlt",
        topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE)
@KafkaListener(id = "listener2", groupId = "group2", topics = TWO_LISTENERS_TOPIC, ...)
void listen2(String message, @Header(KafkaHeaders.RECEIVED_TOPIC) String receivedTopic) {
    ...
}

topicSuffixingStrategy是可选的。框架将为每个监听器配置和使用一组单独的重试主题。

十、死信主题策略Dlt Strategies

框架为使用DLT提供了一些策略。你可以为DLT处理提供一种方法,使用默认的记录日志方法,或者根本不使用DLT。你还可以选择如果DLT处理失败会发生什么。

10.1 死信主题处理方法Dlt Processing Method

你可以指定用于处理DLT的方法,以及处理失败时的行为。为此,可以在带有@RetryableTopic注释的类的方法中使用@DltHandler注解。请注意,相同的方法将用于该类中的所有带@RetryableTopic注解的方法。

@RetryableTopic
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}

@DltHandler
public void processMessage(MyPojo message) {
// ... message processing, persistence, etc
}

DLT handler方法也可以通过RetryTopicConfigurationBuilder.dltHandlerMethod(String, String)方法提供,将应该处理DLT消息的bean的名称和方法名称作为参数传递。

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .dltHandlerMethod("myCustomDltProcessor", "processDltMessage")
            .create(template);
}

@Component
public class MyCustomDltProcessor {

    private final MyDependency myDependency;

    public MyCustomDltProcessor(MyDependency myDependency) {
        this.myDependency = myDependency;
    }

    public void processDltMessage(MyPojo message) {
       // ... message processing, persistence, etc
    }
}

如果未提供DLT处理程序,则使用默认的RetryTopicConfigurer.LoggingDltListenerHandlerMethod 被使用。
从2.8版本开始,如果你根本不想从该应用程序中的DLT消费,包括通过默认处理程序消费(或者您希望推迟消费),你可以控制DLT容器是否启动,而不依赖于容器工厂的autoStartup属性。
使用@RetryableTopic注释时,请将autoStartDltHandler属性设置为false;使用配置生成器时,请使用autoStartDltHandler(false)。
你稍后可以通过KafkaListenerEndpointRegistry启动DLT处理程序。

10.2 死信主题失败时的行为DLT Failure Behavior

如果DLT处理失败,有两种可能的行为:ALWAYS_RETRY_ON_ERROR 和 FAIL_ON_ERROR。在前者中,记录被转发回DLT主题,因此它不会阻塞其他DLT记录的处理。在后一种情况下,consumer在不转发消息的情况下结束执行。

@RetryableTopic(dltProcessingFailureStrategy =
            DltStrategy.FAIL_ON_ERROR)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .dltHandlerMethod("myCustomDltProcessor", "processDltMessage")
            .doNotRetryOnDltFailure()
            .create(template);
}

默认行为为ALWAYS_RETRY_ON_ERROR。
从2.8.3版本开始,如果记录导致引发fatal异常(如反序列化异常),ALWAYS_RETRY_ON_ERROR将不会将该记录路由回DLT,因为通常情况下,总是会引发此类异常。
被视为fatal的异常有:

  • DeserializationException
  • MessageConversionException
  • ConversionException
  • MethodArgumentResolutionException
  • NoSuchMethodException
  • ClassCastException
    你可以使用DestinationTopicResolver bean上的方法向该列表添加异常和从中删除异常。
    有关详细信息,请参阅异常分类器

10.3 配置不使用死信主题Configuring No DLT

该框架还提供了不为主题配置DLT的可能性。在这种情况下,重试结束后,处理就结束了。

@RetryableTopic(dltProcessingFailureStrategy =
            DltStrategy.NO_DLT)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .doNotConfigureDlt()
            .create(template);
}

十一、指定监听器容器工厂Specifying a ListenerContainerFactory

默认情况下,RetryTopic配置将使用@KafkaListener注解提供的工厂,但是你可以指定一个不同的工厂来创建重试主题和dlt监听器容器。对于@RetryableTopic注解,你可以提供工厂的bean名称,对于RetryTopicConfiguration bean,你可以提供bean名称或实例本身。

@RetryableTopic(listenerContainerFactory = "my-retry-topic-factory")
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
        // ... message processing
}

@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template,
        ConcurrentKafkaListenerContainerFactory<Integer, MyPojo> factory) {

    return RetryTopicConfigurationBuilder
            .newInstance()
            .listenerFactory(factory)
            .create(template);
}

@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
    return RetryTopicConfigurationBuilder
            .newInstance()
            .listenerFactory("my-retry-topic-factory")
            .create(template);
}

自2.8.3版本以来,你可以将同一工厂用于可重试和不可重试的主题。

如果你需要将工厂配置行为恢复到2.8.3之前的版本,你可以在一个@Configuration类继承RetryTopicConfigurationSupport,覆盖其中的configureRetryTopicConfigurer方法,如配置全局设置和特性中所述,并将useLegacyFactoryConfigurer设置为true,例如:

@Override
protected Consumer<RetryTopicConfigurer> configureRetryTopicConfigurer() {
    return rtc -> rtc.useLegacyFactoryConfigurer(true);
}

十二、在运行时访问主题信息Accessing Topics’ Information at Runtime

自2.9版本以来,你可以通过注入提供的DestinationTopicContainer bean在运行时访问有关主题链(topic chain)的信息。此接口提供了为主题查找链中下一个主题的方法,或者在DLT中查找(如果已配置),以及有用的属性,如主题的名称、延迟和类型。
作为真实世界的用例示例,你可以使用这些信息,以便console应用程序可以在处理失败(例如bug / inconsistent state)得到解决后,将记录从DLT重新发送到链中的第一个重试主题。
DestinationTopicContainer#getNextDestinationTopicFor()方法提供的DestinationTopic对应于输入主题链中注册的下一个主题。由于不同的因素,如异常分类、尝试次数或single-topic 固定延迟策略,消息将转发到的实际主题可能会有所不同。如果需要权衡这些因素,请使用DestinationTopicResolver接口。

十三、修改KafkaBackOffException日志级别Changing KafkaBackOffException Logging Level

当重试主题中的消息未到期消费时,将引发KafkaBackOffException。默认情况下,此类异常记录在DEBUG级别,但你可以通过在@Configuration类的ListenerContainerFactoryConfigurer中设置错误处理自定义程序来更改此行为。
例如,要将日志记录级别更改为WARN,你可以添加:

@Override
protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
    customizersConfigurer.customizeErrorHandler(defaultErrorHandler ->
            defaultErrorHandler.setLogLevel(KafkaException.Level.WARN))
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring-Kafka整合是将Spring框架与Kafka消息系统进行整合,使得开发者能够方便地使用Spring框架进行Kafka消息的生产和消费。 Spring-Kafka整合提供了以下功能: 1. 自动配置Kafka生产者和消费者。 2. 提供KafkaTemplate用于发送消息。 3. 提供@KafkaListener注解用于监听Kafka主题。 4. 提供KafkaListenerContainerFactory用于创建Kafka监听器容器。 5. 提供KafkaAdmin用于管理Kafka集群。 Spring-Kafka整合的使用步骤如下: 1. 添加Spring-Kafka依赖 在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>${spring-kafka.version}</version> </dependency> ``` 2. 配置Kafka连接 在application.properties文件中添加Kafka连接相关配置: ``` spring.kafka.bootstrap-servers=localhost:9092 ``` 3. 编写Kafka生产者 使用KafkaTemplate发送消息: ``` @Autowired private KafkaTemplate<String, String> kafkaTemplate; public void sendMessage(String topic, String message) { kafkaTemplate.send(topic, message); } ``` 4. 编写Kafka消费者 使用@KafkaListener注解监听Kafka主题: ``` @KafkaListener(topics = "test-topic") public void receiveMessage(String message) { //消费消息 } ``` 5. 配置Kafka监听器容器 使用KafkaListenerContainerFactory创建Kafka监听器容器: ``` @Bean public KafkaListenerContainerFactory<?> kafkaListenerContainerFactory() { ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory()); factory.setConcurrency(1); factory.getContainerProperties().setPollTimeout(3000); return factory; } ``` 6. 配置Kafka管理器 使用KafkaAdmin创建Kafka管理器: ``` @Bean public KafkaAdmin kafkaAdmin() { Map<String, Object> configs = new HashMap<>(); configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); return new KafkaAdmin(configs); } ``` Spring-Kafka整合的使用可以使得开发者更加方便地使用Kafka消息系统,提高消息的生产和消费效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值