MQ: kafka的Java接入與入門示例(topic增刪改查,Producer多參發送,Consumer多分區接受)

本文主要通過實際編碼來對《MQ: 一張圖讀懂kafka工作原理》提到的部分原理進行驗證與實現。

相關文章參考:

1.版本說明

後續代碼依賴於以下版本,其他版本不保證代碼可用:

  • kafka 服務版本:2.11-1.0.1
  • kafka-clients.jar 版本:2.2.0
  • spring-kafka.jar 版本:1.3.5.RELEASE
  • spring-boot版本:1.5.10.RELEASE

2.kafka接入

pom.xml

先引入kafka的spring依賴包,這個包提供Producer和Consumer相關的操作。

<dependency>
  <groupId>org.springframework.kafka</groupId>
  <artifactId>spring-kafka</artifactId>
  <version>1.3.5.RELEASE</version>
</dependency>

如果想進行Topic、Partition相關的操作,則引入下面的包:

<dependency>
  <groupId>org.apache.kafka</groupId>
  <artifactId>kafka-clients</artifactId>
  <version>2.2.0</version>
</dependency>

application.properties

只給出最基本配置,如需調優,請自行增加其他配置。

spring.kafka.bootstrap-servers=127.0.0.1:9092

3.示例代碼:Topic的增刪改查

Toplic的增刪改查需要通過AdminClient操作,主要依賴kafka-clients.jar

3.1.獲取kafka管理客戶端

    /**
     * 獲取kafka管理客戶端
     */
    private static AdminClient getKafkaAdminClient() {
        Map<String, Object> props = new HashMap<>(1);
        props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "10.126.153.155:9092");
        return KafkaAdminClient.create(props);
    }

3.2.獲取全部topic名稱

    /**
     * 獲取全部topic名稱
     */
    private static Collection<String> getAllTopic(AdminClient client) throws InterruptedException, ExecutionException {
        return client.listTopics().listings().get().stream().map(TopicListing::name).collect(Collectors.toList());
    }

3.3.顯示指定Topic的詳細配置

    /**
     * 顯示指定topic的信息
     */
    private static void showTopicInfo(AdminClient client, Collection<String> topics, List<String> wantedTopicList) throws InterruptedException, ExecutionException {
        client.describeTopics(topics).all().get().forEach((topic, description) -> {
            if (wantedTopicList.contains(topic)) {
                log.info("==== Topic {} Begin ====", topic);
                for (TopicPartitionInfo partition : description.partitions()) {
                    log.info(partition.toString());
                }
                log.info("==== Topic {} End ====", topic);
            }
        });
    }

3.4.新建Topic

可以一次性創建多個Topic,每個topic需要指定名稱、Partition數量和Replicas數量。

Replicas數量不能超過broker數量。本文使用的kafka只有一個broker。

        //創建topic:副本數不能超過broker數量
        client.createTopics(Lists.newArrayList(
                //聊天室   3分區
                new NewTopic(TOPIC_CHAT_ROOM, 3, (short) 1),
                //郵件     3分區
                new NewTopic(TOPIC_MAIL, 3, (short) 1),
                //短信     1分區
                new NewTopic(TOPIC_SMS, 1, (short) 1)
        ));

3.5.刪除Topic

可以一次性刪除多個Topic。

 client.deleteTopics(Lists.newArrayList("topic-send-sms","topic-send-mail"));

3.6.修改Topic

無法已經存在的Topic的分區數量等配置,只能刪掉之後重建。

3.7.測試代碼與運行結果

    /**
     * 聊天室   3分區
     */
    public static final String TOPIC_CHAT_ROOM = "topic-hc-chat-room";
    public static final String PERSON_LORA = "Lora";
    public static final String PERSON_JACK = "Jack";
    public static final String PERSON_PAUL = "Paul";

    /**
     * 郵件     3分區
     */
    public static final String TOPIC_MAIL = "topic-hc-mail";

    public static final String CONSUMER_GROUP_MAIL_1 = "MailConsumer-Group-1";
    public static final String CONSUMER_MAIL = "MailConsumer-ALL";

    public static final String CONSUMER_GROUP_MAIL_2 = "MailConsumer-Group-2";
    public static final String CONSUMER_MAIL_PARTITION_0 = "MailConsumer-P0";
    public static final String CONSUMER_MAIL_PARTITION_12 = "MailConsumer-P1,P2";

    public static final String CONSUMER_GROUP_MAIL_3 = "MailConsumer-Group-3";
    public static final String CONSUMER_MAIL_MULTI_0 = "MailConsumer-M0";
    public static final String CONSUMER_MAIL_MULTI_1 = "MailConsumer-M1";
    public static final String CONSUMER_MAIL_MULTI_2 = "MailConsumer-M2";
    public static final String CONSUMER_MAIL_MULTI_3 = "MailConsumer-M3";

    /**
     * 短信     1分區
     */
    public static final String TOPIC_SMS = "topic-hc-sms";
    public static final String CONSUMER_SMS = "SmsConsumer";
		/**
     * 顯示、創建、刪除topic
     */
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //獲取kafka管理客戶端
        AdminClient client = getKafkaAdminClient();

        //查詢全部topic
        Collection<String> topics = getAllTopic(client);

        //創建topic:副本數不能超過broker數量
        client.createTopics(Lists.newArrayList(
                //聊天室   3分區
                new NewTopic(TOPIC_CHAT_ROOM, 3, (short) 1),
                //郵件     3分區
                new NewTopic(TOPIC_MAIL, 3, (short) 1),
                //短信     1分區
                new NewTopic(TOPIC_SMS, 1, (short) 1)
        ));

        //查詢topic詳情
        List<String> wantedTopicList = Lists.newArrayList(TOPIC_CHAT_ROOM, TOPIC_MAIL, TOPIC_SMS);
        showTopicInfo(client, topics, wantedTopicList);

        //刪除topic:想要修改topic的配置如分區等需要刪掉重建
        client.deleteTopics(Lists.newArrayList("topic-send-sms","topic-send-mail"));
    }

運行結果

運行結果顯示了Topic的Partition、Leader、Replicas和isr配置。

 INFO - Kafka version: 2.2.0 
 INFO - Kafka commitId: 05fcfde8f69b0349 
 INFO - ==== Topic topic-hc-chat-room Begin ==== 
 INFO - (partition=0, leader=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), replicas=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), isr=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null)) 
 INFO - (partition=1, leader=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), replicas=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), isr=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null)) 
 INFO - (partition=2, leader=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), replicas=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), isr=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null)) 
 INFO - ==== Topic topic-hc-chat-room End ==== 
 INFO - ==== Topic topic-hc-mail Begin ==== 
 INFO - (partition=0, leader=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), replicas=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), isr=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null)) 
 INFO - (partition=1, leader=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), replicas=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), isr=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null)) 
 INFO - (partition=2, leader=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), replicas=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), isr=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null)) 
 INFO - ==== Topic topic-hc-mail End ==== 
 INFO - ==== Topic topic-hc-sms Begin ==== 
 INFO - (partition=0, leader=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), replicas=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null), isr=c2-v03-0-0-1.yidian.com:9092 (id: 0 rack: null)) 
 INFO - ==== Topic topic-hc-sms End ==== 

4.示例代碼:Producer發送消息

發送端的邏輯比較清晰,只需要主要發送時傳遞的必填與選填參數即可。

參考上一篇文章,消息發送參數:topic必填、partition選填、key選填、message必填。

根據這些參數,將消息發送至哪個Topic-Partition的路由規則,還是去參考上一篇文章

下面通過一個API來展示:

/**
 * <p>生產者</P>
 *
 * @author hanchao
 */
@Slf4j
@RestController
public class ProducerController {

    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;

    /**
     * 發送消息
     */
    @GetMapping("/kafka/batch-send")
    public boolean batchSendMessage(@RequestParam(required = false) String producer,
                                    @RequestParam String topic,
                                    @RequestParam(required = false) Integer partition,
                                    @RequestParam(required = false) String key,
                                    @RequestParam String value,
                                    @RequestParam(required = false) Integer batch) {
        producer = Objects.isNull(producer) ? "Message Producer" : producer;
        batch = Objects.isNull(batch) ? 1 : batch;

        for (int i = 0; i < batch; i++) {
            sendMessage(producer, topic, partition, key, value + i);
        }
        return true;
    }

    /**
     * 發送消息
     */
    private void sendMessage(String producer, String topic, Integer partition, String key, String value) {
        log.info("======>>> " + producer + ": topic={}, [partition={}], [key={}], value={}", topic, partition, key, value);
//        log.info("{}--發送消息:{}", producer, value);
        if (Objects.nonNull(partition)) {
            if (StringUtils.isNotEmpty(key)) {
                kafkaTemplate.send(topic, partition, key, value);
            } else {
                kafkaTemplate.send(topic, partition, null, value);
            }
        } else {
            if (StringUtils.isNotEmpty(key)) {
                kafkaTemplate.send(topic, key, value);
            } else {
                kafkaTemplate.send(topic, value);
            }
        }
    }
}

5.示例代碼:Consumer消費消息

相對於ProducerConsumer的邏輯相對複雜,因爲涉及Consumer Group的概念。

在本人的開發版本中,通過註解@KafkaListenergroupId參數設置Consumer Group,通過id參數標記Consumer

爲了方便打印日誌,簡單定義一個抽象類:

@Slf4j
public abstract class AbstractConsumer {
    /**
     * 打印消息
     */
    public void logRecord(String name, ConsumerRecord<?, ?> record) {
        log.info("<<<------ " + name + " : topic={}, [partition={}], [key={}], value={}, offset={}",record.topic(), record.partition(), record.key(), record.value(), record.offset());
        log.info("{}--收到消息:{}", name, record.value());
    }
}

5.1.Consumer Group只有1個Consumer

下面的Consumer Group只有1個Consumer。

/**
 * <p>郵件-3個分區-Consumer Group只有1個Consumer</P>
 *
 * @author hanchao
 */
@Component
public class MailConsumer extends AbstractConsumer {

    @KafkaListener(id = KafkaClientDemo.CONSUMER_MAIL, groupId = KafkaClientDemo.CONSUMER_GROUP_MAIL_1, topics = {KafkaClientDemo.TOPIC_MAIL})
    public void listenTopicForSms0(ConsumerRecord<?, ?> record) {
        logRecord(KafkaClientDemo.CONSUMER_MAIL, record);
    }
}

測試代碼

    /**
     * 保證消息被處理完
     */
    @After
    public void tearDown() throws Exception {
        Thread.sleep(5000);
    }

		/**
     * http get 請求 test 的簡單封裝
     */
    public void simpleGet(String url) throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get(url))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn().getResponse().getContentAsString();
    }
    
    /**
     * 短信,一個分區,一個Consumer
     * producer 無需傳參 partition和key
     * consumer 無需指定group
     */
    @Test
    public void testForSimple() throws Exception {
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&value=hello&batch=5");
    }

測試結果

  • 若Consumer-Group只有1個Consumer,則這個Partition中的所有消息都被這個Consumer消費。
  • Producer生產的 5條消息,被隨機分配給了3個partition。
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=null], value=hello0 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=null], value=hello1 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=null], value=hello2 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=null], value=hello3 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=null], value=hello4 
 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=1], [key=null], value=hello2, offset=11147 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=0], [key=null], value=hello0, offset=11157 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=0], [key=null], value=hello3, offset=11158 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=2], [key=null], value=hello1, offset=21147 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=2], [key=null], value=hello4, offset=21148 

5.2.Consumer Group只有1個Consumer - 指定partition發送

上面的例子中,partition和key未傳參,最終的消息隨機分佈在partition中。

下面,指定partition的值:

    /**
     * 指定分區進行分發
     */
    @Test
    public void testForPartition() throws Exception {
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&partition=0&value=hello&batch=1");
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&partition=1&value=hello&batch=1");
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&partition=2&value=hello&batch=1");
    }

**測試結果:**消息按照設想,分別被存放到了分區0、1、2 。

 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=0], [key=null], value=hello0 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=1], [key=null], value=hello0 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=2], [key=null], value=hello0 
 
  INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=0], [key=null], value=hello0, offset=11159 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=1], [key=null], value=hello0, offset=11148 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=2], [key=null], value=hello0, offset=21149 

5.3.Consumer Group只有1個Consumer - 指定keyf發送

如果不指定partition,只是指定key呢?看下面的代碼:

    /**
     * 指定key進行分發
     * key的作用是爲消息選擇存儲分區,key可以爲空。
     * 當指定key且不爲空的時候,kafka是根據key的hash值與分區數取模來決定數據存儲到那個分區。
     * 當key=null時,kafka是先從緩存中取分區號,然後判斷緩存的值是否爲空,如果不爲空,就將消息存到這個分區,否則隨機選擇一個分區進行存儲,並將分區號緩存起來,供下次使用。
     */
    @Test
    public void testForKey() throws Exception {
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&key=9&value=hello&batch=1");
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&key=10&value=hello&batch=1");
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&key=11&value=hello&batch=1");
    }

測試結果

key的作用是爲消息選擇存儲分區,key可以爲空。

當指定key且不爲空的時候,kafka是根據key的hash值與分區數取模來決定數據存儲到那個分區。

當key=null時,kafka是先從緩存中取分區號,然後判斷緩存的值是否爲空,如果不爲空,就將消息存到這個分區,否則隨機選擇一個分區進行存儲,並將分區號緩存起來,供下次使用。

 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=9], value=hello0 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=10], value=hello0 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=11], value=hello0 

 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=2], [key=9], value=hello0, offset=21150 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=1], [key=10], value=hello0, offset=11149 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=0], [key=11], value=hello0, offset=11160 

5.4.Consumer Group有多個Consumer - 手動指定分區消費

下面的Consumer Group有2個Consumer,其中Consumer0指定接收Partition0的消息,Consumer1指定接收Partition1和2的消息。

/**
 * <p>郵件-3個分區-分區消費</P>
 *
 * @author hanchao
 */
@Component
public class MailPartitionConsumer extends AbstractConsumer {
    /**
     * 分區消費能夠加快消息消費速度
     * 此Consumer只消費分區0的數據
     */
    @KafkaListener(id = CONSUMER_MAIL_PARTITION_0, groupId = CONSUMER_GROUP_MAIL_2, topicPartitions = {@TopicPartition(topic = TOPIC_MAIL, partitions = "0")})
    public void listenTopicForSms0(ConsumerRecord<?, ?> record) {
        logRecord(CONSUMER_MAIL_PARTITION_0, record);
    }

    /**
     * 分區消費能夠加快消息消費速度
     * 此Consumer消費分區1,2的數據
     */
    @KafkaListener(id = CONSUMER_MAIL_PARTITION_12, groupId = CONSUMER_GROUP_MAIL_2, topicPartitions = {@TopicPartition(topic = TOPIC_MAIL, partitions = {"1", "2"})})
    public void listenTopicForSms1(ConsumerRecord<?, ?> record) {
        logRecord(CONSUMER_MAIL_PARTITION_12, record);
    }
}

測試代碼

    /**
     * 指定分區進行分發
     */
    @Test
    public void testForPartition() throws Exception {
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&partition=0&value=hello&batch=1");
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&partition=1&value=hello&batch=1");
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&partition=2&value=hello&batch=1");
    }

測試結果

  • MailConsumer-P0確實只消費了partition=0的消息, MailConsumer-P1,P2消費了partition=1,2的消息。
  • 分區消費的好處是:同一個Consumer接收消息時串行的,多個Consumer同時接收多個分區的消息,能夠加快消息接收速度。
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=0], [key=null], value=hello0 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=1], [key=null], value=hello0 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=2], [key=null], value=hello0 

 INFO - <<<------ MailConsumer-P0 : topic=topic-hc-mail, [partition=0], [key=null], value=hello0, offset=11152 
 INFO - <<<------ MailConsumer-P1,P2 : topic=topic-hc-mail, [partition=1], [key=null], value=hello0, offset=11142 
 INFO - <<<------ MailConsumer-P1,P2 : topic=topic-hc-mail, [partition=2], [key=null], value=hello0, offset=21142 

特別注意

如果Topic共3個分區,卻在編碼時,只指定了2個分區會怎樣?

將 id = CONSUMER_MAIL_PARTITION_12 的partition參數由{“1”, “2”}修改爲{“1”},修改之後代碼如下:

    /**
     * 分區消費能夠加快消息消費速度
     * 此Consumer消費分區1的數據
     */
    @KafkaListener(id = CONSUMER_MAIL_PARTITION_12, groupId = CONSUMER_GROUP_MAIL_2, topicPartitions = {@TopicPartition(topic = TOPIC_MAIL, partitions = {"1"})})
    public void listenTopicForSms1(ConsumerRecord<?, ?> record) {
        logRecord(CONSUMER_MAIL_PARTITION_12, record);
    }

再次運行,測試結果如下:partition=2的消息沒有被消費!!!

 INFO - <<<------ MailConsumer-P0 : topic=topic-hc-mail, [partition=0], [key=null], value=hello0, offset=11154 
 INFO - <<<------ MailConsumer-P1,P2 : topic=topic-hc-mail, [partition=1], [key=null], value=hello0, offset=11144 

所以:

  • 手動設置partition可能會導致部分分區的數據被遺忘,不建議手動設置,推薦下一節的自動配置方式。

5.5.Consumer Group有多個Consumer + 自動分區消費

自動分區消費配置方式也很簡單,只需要去掉partition屬性,只保留topic配置即可:

/**
 * <p>郵件-3個分區-分區消費</P>
 *
 * @author hanchao
 */
@Component
public class MailPartitionConsumer extends AbstractConsumer {
    /**
     * 分區消費能夠加快消息消費速度
     * 此Consumer只消費分區0的數據
     */
    @KafkaListener(id = CONSUMER_MAIL_PARTITION_0, groupId = CONSUMER_GROUP_MAIL_2, topicPattern = TOPIC_MAIL)
    public void listenTopicForSms0(ConsumerRecord<?, ?> record) {
        logRecord(CONSUMER_MAIL_PARTITION_0, record);
    }

    /**
     * 分區消費能夠加快消息消費速度
     * 此Consumer消費分區1的數據
     */
    @KafkaListener(id = CONSUMER_MAIL_PARTITION_12, groupId = CONSUMER_GROUP_MAIL_2, topicPattern = TOPIC_MAIL)
    public void listenTopicForSms1(ConsumerRecord<?, ?> record) {
        logRecord(CONSUMER_MAIL_PARTITION_12, record);
    }
}

再次運行結果:

  • kafka自動將Consumer Group接受的消息分配給其內的Consumer們。
  • 多個Partition的消息可以被一個Consumer消費。
  • 單個Partition的消息只能被其中一個Consumer消費,不能被Consumer-Group內的多個Consumer消費。
 INFO - <<<------ MailConsumer-P1,P2 : topic=topic-hc-mail, [partition=0], [key=null], value=hello0, offset=11156 
 INFO - <<<------ MailConsumer-P1,P2 : topic=topic-hc-mail, [partition=1], [key=null], value=hello0, offset=11146 
 INFO - <<<------ MailConsumer-P0 : topic=topic-hc-mail, [partition=2], [key=null], value=hello0, offset=21146 

5.6.Consumer Group有多個Consumer + Consumer過多

上面場景中,消息partition=3,而consumer=2。如果Consumer數量大於partition數量呢?

下面的Consumer Group中,存在4個Consumer:

/**
 * <p>郵件-3個分區-Consumer數量大於分區數量</P>
 *
 * @author hanchao
 */
@Component
public class MailMultiConsumer extends AbstractConsumer {
    /**
     * 如果Consumer-Group組中的Consumer數量多於分區數量,則在服務穩定運行期間,會有Consumer永遠無法消費消息
     */
    @KafkaListener(id = CONSUMER_MAIL_MULTI_0, groupId = CONSUMER_GROUP_MAIL_3, topics = {TOPIC_MAIL})
    public void listenTopic0(ConsumerRecord<?, ?> record) {
        logRecord(CONSUMER_MAIL_MULTI_0, record);
    }

    @KafkaListener(id = CONSUMER_MAIL_MULTI_1, groupId = CONSUMER_GROUP_MAIL_3, topics = {TOPIC_MAIL})
    public void listenTopic1(ConsumerRecord<?, ?> record) {
        logRecord(CONSUMER_MAIL_MULTI_1, record);
    }

    @KafkaListener(id = CONSUMER_MAIL_MULTI_2, groupId = CONSUMER_GROUP_MAIL_3, topics = {TOPIC_MAIL})
    public void listenTopic2(ConsumerRecord<?, ?> record) {
        logRecord(CONSUMER_MAIL_MULTI_2, record);
    }

    @KafkaListener(id = CONSUMER_MAIL_MULTI_3, groupId = CONSUMER_GROUP_MAIL_3, topics = {TOPIC_MAIL})
    public void listenTopic3(ConsumerRecord<?, ?> record) {
        logRecord(CONSUMER_MAIL_MULTI_3, record);
    }
}

測試結果

  • 每次運行,總有1個分區不會獲取消息。
  • 也就是說:若單個Topic的分區數量小於Consumer-Group內的Consumer個數,則會存在Consumer接受不到這個Topic的消息。
 INFO - <<<------ MailConsumer-M3 : topic=topic-hc-mail, [partition=0], [key=null], value=hello0, offset=11156 
 INFO - <<<------ MailConsumer-M1 : topic=topic-hc-mail, [partition=1], [key=null], value=hello0, offset=11146 
 INFO - <<<------ MailConsumer-M2 : topic=topic-hc-mail, [partition=2], [key=null], value=hello0, offset=21146 

5.7.多Consumer Group

上面的場景都是針對單個Consumer Group,現在對上述3個Consumer Group進行同時測試:

    @Test
    public void testForSimple() throws Exception {
        simpleGet("/kafka/batch-send?topic=" + TOPIC_MAIL + "&value=hello&batch=5");
    }

測試結果(爲了方便查看,測試結果的日誌順序進行了調整):

  • Producer共計生產了5條消息,每個Consumer-Group分別收到了5條消息。
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=null], value=hello0 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=null], value=hello1 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=null], value=hello2 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=null], value=hello3 
 INFO - ======>>> Message Producer: topic=topic-hc-mail, [partition=null], [key=null], value=hello4 

 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=1], [key=null], value=hello2, offset=11147 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=0], [key=null], value=hello0, offset=11157 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=0], [key=null], value=hello3, offset=11158 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=2], [key=null], value=hello1, offset=21147 
 INFO - <<<------ MailConsumer-ALL : topic=topic-hc-mail, [partition=2], [key=null], value=hello4, offset=21148 

 INFO - <<<------ MailConsumer-P0 : topic=topic-hc-mail, [partition=2], [key=null], value=hello1, offset=21147 
 INFO - <<<------ MailConsumer-P0 : topic=topic-hc-mail, [partition=2], [key=null], value=hello4, offset=21148 
 INFO - <<<------ MailConsumer-P1,P2 : topic=topic-hc-mail, [partition=1], [key=null], value=hello2, offset=11147 
 INFO - <<<------ MailConsumer-P1,P2 : topic=topic-hc-mail, [partition=0], [key=null], value=hello0, offset=11157 
 INFO - <<<------ MailConsumer-P1,P2 : topic=topic-hc-mail, [partition=0], [key=null], value=hello3, offset=11158 

 INFO - <<<------ MailConsumer-M1 : topic=topic-hc-mail, [partition=1], [key=null], value=hello2, offset=11147 
 INFO - <<<------ MailConsumer-M2 : topic=topic-hc-mail, [partition=2], [key=null], value=hello1, offset=21147 
 INFO - <<<------ MailConsumer-M2 : topic=topic-hc-mail, [partition=2], [key=null], value=hello4, offset=21148 
 INFO - <<<------ MailConsumer-M3 : topic=topic-hc-mail, [partition=0], [key=null], value=hello0, offset=11157 
 INFO - <<<------ MailConsumer-M3 : topic=topic-hc-mail, [partition=0], [key=null], value=hello3, offset=11158 

5.8.聊天室示例

上面的例子挺枯燥的,最後來個更形象的例子。

聊天室場景:

  • 聊天室有3個人:Jack、Paul和Lora。
  • Jack發送消息:新人報到,多多指教!
  • Lora發送消息:歡迎歡迎!!!
  • Paul發送消息:歡迎!另:入羣請改名,謝謝!

這個場景其實上面已經基本實現了,這裏就不細說了,直接給代碼和結果 。

消費者代碼

/**
 * <p>聊天室消費者</P>
 *
 * @author hanchao
 */
@Component
public class ChatConsumerPaul extends AbstractConsumer {

    /**
     * 聊天者:Paul
     */
    @KafkaListener(id = PERSON_PAUL, groupId = PERSON_PAUL, topics = {TOPIC_CHAT_ROOM})
    public void listenTopic2(ConsumerRecord<?, ?> record) {
        logRecord(PERSON_PAUL, record);
    }
}
/**
 * <p>聊天室消費者</P>
 *
 * @author hanchao
 */
@Component
public class ChatConsumerLora extends AbstractConsumer {

    /**
     * 聊天者:Lora
     */
    @KafkaListener(id = PERSON_LORA, groupId = PERSON_LORA, topics = {TOPIC_CHAT_ROOM})
    public void listenTopic1(ConsumerRecord<?, ?> record) {
        logRecord(PERSON_LORA, record);
    }
}
/**
 * <p>聊天室消費者</P>
 *
 * @author hanchao
 */
@Component
public class ChatConsumerPaul extends AbstractConsumer {

    /**
     * 聊天者:Paul
     */
    @KafkaListener(id = PERSON_PAUL, groupId = PERSON_PAUL, topics = {TOPIC_CHAT_ROOM})
    public void listenTopic2(ConsumerRecord<?, ?> record) {
        logRecord(PERSON_PAUL, record);
    }
}

測試代碼

    /**
     * 聊天室
     */
    @Test
    public void testForChatRoom() throws Exception {
        simpleGet("/kafka/batch-send?topic=" + TOPIC_CHAT_ROOM + "&producer=" + PERSON_JACK + "&value=" + "新人報到,多多指教!");

        Thread.sleep(1000);
        simpleGet("/kafka/batch-send?topic=" + TOPIC_CHAT_ROOM + "&producer=" + PERSON_LORA + "&value=" + "歡迎歡迎!!!");

        Thread.sleep(1000);
        simpleGet("/kafka/batch-send?topic=" + TOPIC_CHAT_ROOM + "&producer=" + PERSON_PAUL + "&value=" + "歡迎!另:入羣請改名,謝謝!");
    }

測試結果

 INFO - Jack--發送消息:新人報到,多多指教!0 

 INFO - Jack--收到消息:新人報到,多多指教!0 
 INFO - Paul--收到消息:新人報到,多多指教!0 
 INFO - Lora--收到消息:新人報到,多多指教!0 


 INFO - Lora--發送消息:歡迎歡迎!!!0 

 INFO - Jack--收到消息:歡迎歡迎!!!0 
 INFO - Lora--收到消息:歡迎歡迎!!!0 
 INFO - Paul--收到消息:歡迎歡迎!!!0 
 

 INFO - Paul--發送消息:歡迎!另:入羣請改名,謝謝!0 

 INFO - Lora--收到消息:歡迎!另:入羣請改名,謝謝!0 
 INFO - Jack--收到消息:歡迎!另:入羣請改名,謝謝!0 
 INFO - Paul--收到消息:歡迎!另:入羣請改名,謝謝!0 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章