Apache Pulsar的consume的各种subscription mode订阅模式、consume消费者的消费规则

1. consume的各种subscription mode订阅模式

1.1 介绍

  • 一个subscription name有时可以有多个consume客户端
  • 一个consume只能有一个subscription name

1.2 topic数据准备

向my-topic以单条方式发送如下10条数据

producer.newMessage().key("key-1").value("message-1-1").send();
producer.newMessage().key("key-1").value("message-1-2").send();
producer.newMessage().key("key-1").value("message-1-3").send();
producer.newMessage().key("key-2").value("message-2-1").send();
producer.newMessage().key("key-2").value("message-2-2").send();
producer.newMessage().key("key-2").value("message-2-3").send();
producer.newMessage().key("key-3").value("message-3-1").send();
producer.newMessage().key("key-3").value("message-3-2").send();
producer.newMessage().key("key-4").value("message-4-1").send();
producer.newMessage().key("key-4").value("message-4-2").send();

1.3 subscription mode

1.3.1 Exclusive(默认)

subscription name只能被第一个consume订阅。其它consume订阅会报错

consume的示例代码

    import org.apache.pulsar.client.api.PulsarClient
    import org.apache.pulsar.client.api.Consumer
    import org.apache.pulsar.client.api.Schema
    import org.apache.pulsar.client.api.SubscriptionType
    
    val pulsarClient:PulsarClient = _
    val consumer:Consumer[String]  = pulsarClient.newConsumer(Schema.STRING)
      .topic("my-topic")
      .subscriptionName("subscription-exclusive")
      .subscriptionType(SubscriptionType.Exclusive)
      .subscribe()

1.3.2 Failover

consume1和consume2的示例代码一样,如下所示:

    val pulsarClient:PulsarClient = _
    val consumer:Consumer[String]  = pulsarClient.newConsumer(Schema.STRING)
      .topic("my-topic")
      .subscriptionName("subscription-failover")
      .subscriptionType(SubscriptionType.Failover)
      .subscribe()

先启动consume1,consume1是active consume。再启动consume2,consume2是standby consume。如果consume1消费了5条数据之后挂掉了,consume2变成active consume消费剩余的5条数据

注意:如果topic是分区的,假如my-topic有2个partition,可能consume1对于partition1是active consume,consume2对于partition1是standby consume;consume2对于partition2是active consume,consume1对于partition2是standby consume

1.3.3 Shared

consume1和consume2的示例代码一样,如下所示:

    val pulsarClient:PulsarClient = _
    val consumer:Consumer[String]  = pulsarClient.newConsumer(Schema.STRING)
      .topic("my-topic")
      .subscriptionName("subscription-shared")
      .subscriptionType(SubscriptionType.Shared)
      .subscribe()

先启动consume1,再启动consume2。consume1和consume2都是active consume,消息以round-robin的方式被consume1和consume2消费。此消费方式不能保证消息的顺序性

1.3.4 Key_Shared

consume1和consume2的示例代码一样,如下所示:

    val pulsarClient:PulsarClient = _
    val consumer:Consumer[String]  = pulsarClient.newConsumer(Schema.STRING)
      .topic("my-topic")
      .subscriptionName("subscription-key-shared")
      .subscriptionType(SubscriptionType.Key_Shared)
      .subscribe()

先启动consume1,再启动consume2。consume1和consume2都是active consume。相同key的消息只能被一个consume消费,但一个key被哪个consume消费是随机的。例如key-1和key-3被consume1消费;key-2和key-4被consume2消费

注意:默认produce以batch的方式发送消息,一个batch可能包含不同的key。consume也是以batch的方式进行消费。这和Key_Shared的订阅模式是相冲突的。可以通过下面两种方式进行解决:

方式一:produce使用KeyBasedBatcher

    import org.apache.pulsar.client.api.Producer
    import org.apache.pulsar.client.api.BatcherBuilder
    
    val produce:Producer[String] = pulsarClient.newProducer(Schema.STRING)
      .topic("my-topic")
      .batcherBuilder(BatcherBuilder.KEY_BASED)
      .create()

方式二:produce关闭batch发送消息

    val produce:Producer[String] = pulsarClient.newProducer(Schema.STRING)
      .topic("my-topic")
      .enableBatching(false)
      .create()

注意:如果消息没有指定key,则消息只能被一个consume消费

2. consume消费者的消费规则

  1. 默认是从Latest位置消费,也可以从Earliest位置消费
  2. 一个subscription消费过了一条消息,就算开启从Earliest位置消费,也不会重复消费该消息,而是从subscription保持的位置消费
  3. 被一个subscription消费的一条消息,不会立刻删除。其它subscription开启从Earliest位置消费,也能消费该消息
### 头歌平台生产者消费者问题的同步控制实现 生产者-消费者问题是多线程编程中的经典问题之一,其核心在于如何通过某种机制来协调多个生产者线程和消费者线程之间的数据共享与访问[^1]。为了确保程序能够正常运行并避免死锁或资源竞争等问题,通常会采用同步控制技术。 #### 同步控制的核心概念 同步控制的主要目的是防止多个线程同时修改同一份数据而导致的数据不一致问题。在生产者-消费者模型中,常见的同步方法包括使用 `wait()` 和 `notify()` 方法以及基于阻塞队列的方式。 --- #### 基于 `wait/notify` 的同步控制实现 以下是利用 Java 中的对象监视器(Object Monitor)机制实现的一个简单示例: ```java class SharedBuffer { private final int[] buffer = new int[5]; // 缓冲区大小为5 private int count = 0; // 当前缓冲区内元素数量 public synchronized void produce(int value) throws InterruptedException { while (count >= buffer.length) { // 如果缓冲区已满,则等待 wait(); } buffer[count++] = value; System.out.println("Produced: " + value); notify(); // 唤醒可能正在等待的消费者线程 } public synchronized int consume() throws InterruptedException { while (count <= 0) { // 如果缓冲区为空,则等待 wait(); } int consumedValue = buffer[--count]; System.out.println("Consumed: " + consumedValue); notify(); // 唤醒可能正在等待的生产者线程 return consumedValue; } } public class ProducerConsumerExample { public static void main(String[] args) { SharedBuffer sharedBuffer = new SharedBuffer(); Thread producerThread = new Thread(() -> { try { for (int i = 0; i < 10; i++) { sharedBuffer.produce(i); // 生产者向缓冲区写入数据 Thread.sleep(100); // 模拟延迟 } } catch (InterruptedException e) { e.printStackTrace(); } }); Thread consumerThread = new Thread(() -> { try { for (int i = 0; i < 10; i++) { sharedBuffer.consume(); // 消费者从缓冲区读取数据 Thread.sleep(150); // 模拟延迟 } } catch (InterruptedException e) { e.printStackTrace(); } }); producerThread.start(); consumerThread.start(); } } ``` 上述代码展示了如何通过 `synchronized` 关键字锁定对象实例,并结合 `wait()` 和 `notify()` 来实现线程间的通信。 --- #### 基于阻塞队列的同步控制实现 另一种更现代的方法是使用 Java 提供的阻塞队列(Blocking Queue),它内置了必要的同步逻辑,从而简化了开发过程。下面是一个简单的例子: ```java import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; public class BlockingQueueExample { public static void main(String[] args) { BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5); Thread producerThread = new Thread(() -> { try { for (int i = 0; i < 10; i++) { queue.put(i); // 将数据放入队列,如果队列满了则自动阻塞 System.out.println("Produced: " + i); Thread.sleep(100); } } catch (InterruptedException e) { e.printStackTrace(); } }); Thread consumerThread = new Thread(() -> { try { for (int i = 0; i < 10; i++) { Integer value = queue.take(); // 从队列取出数据,如果队列为空则自动阻塞 System.out.println("Consumed: " + value); Thread.sleep(150); } } catch (InterruptedException e) { e.printStackTrace(); } }); producerThread.start(); consumerThread.start(); } } ``` 这种方式无需手动管理线程间的通知关系,因为阻塞队列已经封装好了这些细节。 --- #### Spring Boot 集成 Pulsar 的生产者与消费者示例 对于实际应用环境下的消息传递需求,可以考虑使用 Apache Pulsar 这样的分布式消息中间件。以下是在 Spring Boot 中集成 Pulsar 的一个基础示例[^2]: ##### 生产者代码 ```java @SpringBootApplication public class PulsarProducerApplication implements CommandLineRunner { @Autowired private PulsarTemplate<String> pulsarTemplate; public static void main(String[] args) { SpringApplication.run(PulsarProducerApplication.class, args); } @Override public void run(String... args) throws Exception { String message = "Hello from Pulsar!"; pulsarTemplate.send("persistent://public/default/test-topic", message); System.out.println("Message sent: " + message); } } ``` ##### 消费者代码 ```java @Component public class PulsarConsumer { @PulsarListener(subscriptionName = "test-subscription", topicNames = {"persistent://public/default/test-topic"}) public void receive(String message) { System.out.println("Received Message: " + message); } } ``` 此部分实现了基于 Pulsar 的异步消息处理能力,适合大规模分布式系统的应用场景。 --- ### 总结 无论是传统的 `wait/notify` 方式还是现代化的阻塞队列工具,都可以有效解决生产者-消费者的同步问题。而在企业级环境中引入像 Pulsar 这类高级框架,则能进一步提升性能与可扩展性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值