[pulsar series] pulsar partition message to achieve sequential consumption

Introduction to pulsar

Apache Pulsar is a cloud-native, distributed messaging and streaming platform originally created at Yahoo! and now a top-level Apache Software Foundation project

This is an introduction from the official website that Apache Pulsar is a cloud-native distributed message and stream processing platform, originally created in Yahoo! Now a top-level project at the Apache Software Foundation

For the details of the specific pulsar, you can learn through the official documents

need

When updating the transportation status of the main order, there is a business requirement. For example, the detail line of the main order has four materials A, B, C, and D. The A and B material lines correspond to the transportation order number T0001, and the C and D materials correspond to the transportation order number T0002. There is a logic in the background business that only when the shipping order numbers corresponding to all materials are signed, the main order shipping status can be closed.

The original message consumer is consumed in Shared mode. The official website explains this mode in this way. Messages are distributed to different consumers through the round robin polling mechanism, and each message will only be distributed to one consumer. When a consumer disconnects, all unacknowledged messages sent to it will be rescheduled and distributed to other surviving consumers.

So what are the problems with this model in demand? First of all, the service is deployed on multiple nodes (if there are 2 consumers), now the upstream service pushes a message (A, B material transportation order number T0001), and then pushes a message (C, D material transportation order number T0002), these two consumers consume this piece of logic at the same time, then our main order status judgment logic will have problems or it will be very troublesome to handle.

How can the message middleware pulsar easily fulfill such a requirement? My first thought at the time was to use Exclusive mode, but due to the impact on the throughput of consumption, I just gave up. According to my previous impression of Kafka, I wondered if pulsar has such content as partitioning? Sure enough, I found a partition topic to implement this problem message Apache Pulsar

producer

Specify the topic partition to be created through the admin API and specify the number of partitions when creating it.

The class encapsulated by the producer sending the partition message

@Slf4j
@Component
public class ProducerPartionFactory {

    @Autowired
    private PulsarClient client;

    @Autowired
    private PulsarAdmin pulsarAdmin;

    // 消息生产者集合
    private ConcurrentHashMap<String, Producer<byte[]>> producerMap = new ConcurrentHashMap<>();


    private Producer<byte[]> getTheProducer(PulsarProducePartionRoter pulsarProducePartionRoter) throws Exception {
        String topic = pulsarProducePartionRoter.getTopic();
        Producer<byte[]> producer = producerMap.get(topic);
        if (producer == null) {
            synchronized (topic.intern()) {
                producer = producerMap.get(topic);
                if (producer == null) {

                    PartitionedTopicMetadata metadata = null;
                    String localIpAddress = null;
                    try {
                        metadata = pulsarAdmin.topics().getPartitionedTopicMetadata("persistent://public/test/" + topic);
                        if (metadata.partitions == 0) {
						   //使用pulsarAdmin来初始化创建分区消息,这里的分区建议按照消费者数量或者消费者数量*n来定义
                            pulsarAdmin.topics().createPartitionedTopic("persistent://public/test/" + topic, 4);
                        }

                        //因为我们这里是多个节点,这里使用ip地址来区别我们不同的生产者名称
                        localIpAddress = IPUtils.getLocalIpAddress();
                        if(StringUtils.isNotBlank(localIpAddress)){
                            localIpAddress.replaceAll("\\.","_");
                        }

                    } catch (Exception ex) {
                        log.error("创建分区主题异常:{}", ex);
                        throw ex;
                    }
                    try {
                        // topic 生产者消息写到哪个主题中
                        // producerName 生产者的名字,不唯一即可,如果不设置,会有默认值
                        // sendTimeout 超时时间
                        // 默认情况下,当队列已满时,所有对Send和SendAsync方法的调用都将失败,除非您将BlockIfQueueFull设置为true
                        producer = client.newProducer(Schema.BYTES)
                                .topic("persistent://public/test/" + topic)
                                .producerName(topic + localIpAddress)
                                .sendTimeout(10, TimeUnit.SECONDS)
//                                .blockIfQueueFull(true)
                                .create();
                        producerMap.putIfAbsent(topic, producer);
                        return producer;
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        return producer;
    }


    public boolean send(PulsarProducePartionRoter pulsarProducePartionRoter, String message) {
        boolean isSuccess = false;
        try {
            Producer<byte[]> producer = this.getTheProducer(pulsarProducePartionRoter);
            MessageId messageId = producer.newMessage().key(pulsarProducePartionRoter.getKey()).value(message.getBytes()).send();
            log.info("获取分区发送信息:{}", JSON.toJSONString(messageId));
            if (messageId != null) {
                isSuccess = true;
            }
        } catch (Exception e) {
            log.error("pulsar发送异常:", e);
        }
        return isSuccess;
    }
}


@Data
public class PulsarProducePartionRoter {

    private String topic;

    private String key;

}
IPUtils tool class
public class IPUtils {
    /**
     * 获取本地IP地址
     *
     * @return 本地IP地址
     */
    public static String getLocalIpAddress() {
        try {
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            while (interfaces.hasMoreElements()) {
                NetworkInterface iface = interfaces.nextElement();
                // 排除回环和虚拟接口
                if (iface.isLoopback() || iface.isVirtual() || !iface.isUp()) {
                    continue;
                }
                Enumeration<InetAddress> addresses = iface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    InetAddress addr = addresses.nextElement();
                    if (!addr.isLinkLocalAddress() && !addr.isLoopbackAddress() && addr.isSiteLocalAddress()) {
                        return addr.getHostAddress();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

A piece of logic for producers to send messages

       String topic ="KEY_NEW_ORDER_SIGN";
        PulsarProducePartionRoter pulsarProducePartionRoter = new PulsarProducePartionRoter();
        pulsarProducePartionRoter.setTopic(topic);
        pulsarProducePartionRoter.setKey(requestBean.getStoreNo());
        boolean isSuccess = producerPartionFactory.send(pulsarProducePartionRoter, JSON.toJSONString(requestBean));

We can see that the same stroeNo is assigned the same partition number 

consumer

After the project is started, we start the consumer thread pool to consume partition message business logic

@Slf4j
@Component
public class SignKeyNewShardConsumerLister implements CommandLineRunner {

    private volatile boolean isRunning = true;

    private ThreadPoolExecutor threadPoolExecutor = null;

    @Autowired
    private PulsarClient pulsarClient;

    private final EmitterProcessor<FailedMessage> exceptionEmitter = EmitterProcessor.create();

    private Consumer<byte[]> consumer;

    @Autowired
    private PulsarAdmin pulsarAdmin;

    @Override
    public void run(String... args) throws Exception {

        threadPoolExecutor = new ThreadPoolExecutor(1, 1,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(100),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());


        try {

            PartitionedTopicMetadata metadata = null;
            String topic = null;
            try {
                topic =  "persistent://public/test/KEY_NEW_ORDER_SIGN" ;
                metadata = pulsarAdmin.topics().getPartitionedTopicMetadata(topic);
                if (metadata.partitions == 0) {
                    pulsarAdmin.topics().createPartitionedTopic(topic, 4);
                }
            } catch (Exception ex) {
                if (ex.getMessage().contains("Topic not exist")) {
                    pulsarAdmin.topics().createPartitionedTopic(topic, 4);
                } else {
                    log.error("创建分区主题异常:{}", ex);
                    throw ex;
                }

            }
            consumer = pulsarClient.newConsumer(Schema.BYTES)
                    .topic(topic)
                    .subscriptionName(Topic.KEY_NEW_ORDER_SIGN.getCode())
                    .receiverQueueSize(1)
                    .subscriptionType(SubscriptionType.Key_Shared)
                    .keySharedPolicy(KeySharedPolicy.KeySharedPolicySticky.autoSplitHashRange())
//                    .autoUpdatePartitions(true)
                    .subscribe();
        } catch (Exception e) {
            log.error("初始化客户端SignConsumerLister 异常", e);
        }
        threadPoolExecutor.submit(() -> {
            Message<byte[]> receiveMsg = null;
            while (isRunning) {
                try {
                    receiveMsg = consumer.receive();
                    if (receiveMsg != null) {
                    
                        try {
                               // todo 执行业务逻辑
                        } catch (Exception ex) {
                            log.error("处理签收业务异常:{}", ex);
                        }
                        consumer.acknowledge(receiveMsg);
                    }
                } catch (Exception e) {
                    log.error("SignConsumerLister  exec processMessage Exception: {}", e);
                    consumer.negativeAcknowledge(receiveMsg);
                    exceptionEmitter.onNext(new FailedMessage(e, consumer, receiveMsg));
                }
            }
        });
    }



    @PreDestroy
    public void doDestroyListen() {
        isRunning = false;
        if (threadPoolExecutor != null) {
            threadPoolExecutor.shutdown();
        }
        try {
            consumer.close();
        } catch (Exception e) {
            log.error("SignConsumerLister  exec destroy Exception: {}", e);
        }
    }
}

We also saw that the consumer service processed this message on the same instance node and consumed it sequentially

Guess you like

Origin blog.csdn.net/run_boy_2022/article/details/131769383