spring-kafka多线程顺序消费

本文介绍了一个共享充电宝业务中,利用Spring-Kafka实现kafka消息的多线程顺序消费的需求。通过订单号对分区数取余确保消息发送到相同分区,并保证每个分区由同一线程消费,以此实现顺序性和并发性能的平衡。
摘要由CSDN通过智能技术生成

业务场景

我们公司是做共享充电宝的业务的。有一些比较大的代理商或者ka商户,他们需要了解到他们自己下面的商户的订单数据,这些订单数据需要由我们推送给他们。

大致架构为数据部门通过canal订阅订单表的数据,然后推送到kafka ,我们订阅数据部门kafka获取到代理商下商户的实时订单数据再推送给代理商。比如,代理商下商户产生了一笔订单,整个过程会产生,订单生成,订单已支付,充电宝已被取走,充电宝已归还等多种状态的订单消息,我们需要实时把这些订单消息推送给代理商。我们的业务场景需要消息的顺序推送和多线程并发消费以提高性能

kafka多线程消费方案

  1. 消费者程序启动多个线程,每个线程维护专属的KafkaConsumer实例,负责完整的消息获取、消息处理
    流程。如下图所示:
    在这里插入图片描述
  2. 消费者程序使用单或多线程获取消息,同时创建多个消费线程执行消息处理逻辑。获取消息的线程可以 是一个,也可以是多个,每个线程维护专属的KafkaConsumer实例,处理消息则交由特定的线程池来 做,从而实现消息获取与消息处理的真正解耦。具体架构如下图所示:
    在这里插入图片描述
    这两种方案孰优孰劣呢?应该说是各有千秋。这两种方案的优缺点,我们先来看看下面这张表格。
    在这里插入图片描述

kafka怎么保证顺序消费

保证顺序消费,需要满足如下条件

  1. 保证相同订单编号的消息需要发送到同一个分区。
@Configuration
public class SenderConfig {
   

  @Value("${kafka.bootstrap-servers}")
  private String bootstrapServers;

  @Bean
  public Map<String, Object> producerConfigs() {
   
    Map<String, Object> props = new HashMap<>();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

    return props;
  }

  @Bean
  public ProducerFactory<String, String> producerFactory() {
   
    return new DefaultKafkaProducerFactory<>(producerConfigs());
  }

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

  @Bean
  public Sender sender() {
   
    return new Sender();
  }
}

public class Sender {
   

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    public void send(String topic, String data) {
   
        kafkaTemplate.send(topic, data);
    }

    public void send(String topic, int partition, String data) {
   
        kafkaTemplate.send(topic, partition, data);
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringKafkaApplicationTest {
   

    private static String BATCH_TOPIC = "batch.t";

    private static Integer PARTITIONS = 6;
    /**
     * 已支付
     */
    private static Integer PAYED_STATUS = 2;
    /**
     * 已取走
     */
    private static Integer SEND_BACK_STATUS = 3;

    @Autowired
    private Sender sender;

    private static DelayQueue delayQueue = new DelayQueue();

    @Test
    public void testReceive() throws Exception {
   
        for (int i = 1; i < 50; i++) {
   
            Integer orderNum = 800010 + i;
            Integer orderPrice = RandomUtil.randomInt(1, 20);
            // 用户支付成功,订单状态为支付成功
            OrderDTO order = new OrderDTO(orderNum, orderPrice, PAYED_STATUS);
            // 发送支付成功订单消息到对应的kafka分区
            Integer destinationPartition = orderNum % PARTITIONS;
            sender.send(BATCH_TOPIC, destinationPartition, JSONUtil.toJsonStr(order));
            // 创建任务放入延迟队列(模拟用户支付成功到取走充电宝花费的时间)
            long delayTime = 200;
            OrderTask orderTask = new OrderTask(delayTime, order);
            delayQueue.offer(orderTask);
        }

        while (true) {
   
            // 用户取走充电宝,订单状态更改为 已取走
            OrderTask orderTask = (OrderTask) delayQueue.take();
            OrderDTO orderDTO = orderTask.getOrderDTO();
            Integer destinationPartition = orderDTO.getOrderNum() % PARTITIONS;
            orderDTO.setOrderStatus(SEND_BACK_STATUS);
            // 发送已取走订单消息到对应的kafka 分区
            sender.send(BATCH_TOPIC, destinationPartition, JSONUtil.toJsonStr(orderDTO));
        }

    }
}

可以看出我们通过订单号对分区数进行取余,来确定该消息发送到哪一个分区,保证相同订单号的消息被发送到相同的分区。当然也可以对字符串这些进行hash ,获得hash值来对分区数取余

Integer destinationPartition=orderDTO.getOrderNum()%PARTITIONS;
  1. 保证同一个分区的消息由同一个线程来消费。

我们的业务场景需要采用多线程方案一来处理我们的业务

普通方式实现方案一

public class KafkaConsumerRunner implements Runnable {
   
    private final AtomicBoolean closed = new AtomicBoolean(false);
    private final KafkaConsumer consumer;

    public KafkaConsumerRunner(KafkaConsumer consumer) {
   
        this.consumer = consumer;
    }

    @Override
    public void run() {
   
        try {
   
            consumer.subscribe(Arrays.asList("topic"));
            while (!closed.get()) {
   
                // 执行消息处理逻辑
                ConsumerRecords records = consumer.poll(10000);
            }
        } catch (Exception e) {
   
            // Ignore exception if closing
            if (!closed.get()) {
   
                throw e;
            }
        } finally {
   
            consumer.close();
        }
    }

    /**
     * Shutdown hook which can be called from a separate thread
     */
    public void shutdown() {
   
        closed.set(true);
        consumer.wakeup();
    }
}

spring-kafka为我们做的封装

消费者相关配置
这里我们需要注意的是factory.setConcurrency(4)。
这个是配置主要是设置KafkaConsumer的数量,最大为topic 的分区数。当然你如果设置的值超过topic 分区数,spring-kafka 还是只会为我们创建最大分区数的KafkaConsumer数量,也就是创建KafkaConsumer数量能少于分区数,但不会超过分区数。少于分区数的话,一个KafkaConsumer会消费多个分区的数据,保证所有的分区数据都有对应的KafkaConsumer来进行消费;但不会出现多个KafkaConsumer消费同一个分区的情况,因为如果是这样也就无法保证消息的顺序消费机制。
一般情况下如果数据量较大,我们需要把此值设置为topic分区数,这样一个KafkaConsumer消费一个分区的数据,提高数据的并发消费能力。

@Configuration
@EnableKafka
public class ReceiverConfig {
   

    @Value("${kafka.bootstrap-servers}")
    private String bootstrapServers;

    @Bean
    public Map<String, Object> consumerConfigs() {
   
        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "batch");
        // maximum records per poll
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "100");
        return props;
    }

    @Bean
    public ConsumerFactory<String, String> consumerFactory() {
   
        return new DefaultKafkaConsumerFactory<>(consumerConfigs());
    }

    @Bean(name = "kafkaListenerContainerFactory")
    public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
   
        ConcurrentKafkaListenerContainerFactory<String, String> factory =
                new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        // enable batch listening
        factory.setBatchListener(true);
        factory.setConcurrency(4);
        return factory;
    }

    @Bean
    public Receiver receiver() {
   
        return new Receiver();
    }
}

Receiver 代码

public class Receiver {
   

    @Autowired
    private PushOrderService pushOrderService;

    private static final Logger LOGGER = LoggerFactory.getLogger(Receiver.class);

    private static final String BATCH_TOPIC = "batch.t";

    @KafkaListener(topics = BATCH_TOPIC, containerFactory = "kafkaListenerContainerFactory")
    public void receivePartitions(List<String> data,
                                  @Header(KafkaHeaders.RECEIVED_PARTITION_ID) List<Integer> partitions,
                                  @Header(KafkaHeaders.OFFSET) List<Long> offsets
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值