本文主要通過實際編碼來對《MQ: 一張圖讀懂kafka工作原理》提到的部分原理進行驗證與實現。
相關文章參考:
1.版本說明
後續代碼依賴於以下版本,其他版本不保證代碼可用:
kafka
服務版本:2.11-1.0.1kafka-clients.jar
版本:2.2.0spring-kafka.jar
版本:1.3.5.RELEASEspring-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消費消息
相對於Producer
,Consumer
的邏輯相對複雜,因爲涉及Consumer Group
的概念。
在本人的開發版本中,通過註解@KafkaListener
的groupId
參數設置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