“簡單”的訊息佇列與kafka

聆聽的車轍發表於2019-06-11

“簡單”的訊息佇列與kafka
小時候就特別喜歡龍系精靈,特別是乘龍,後來才知道只是冰水。。。尷尬。 在寵物小精靈中,乘龍一直是訓練家的載人夥伴,和我們下面的MQ好像有幾分相似呢~~

前言

MQ,全稱訊息佇列,現在市面上有很多種訊息佇列,像大家耳熟能詳的RabbitMQ,RocketMQ,Kafka等等,接下來為大家詳細的介紹訊息佇列。

使用場景

俗話說的好,多用多錯,不能為了技術而技術,要結合實際的業務場景使用合適的技術。
例如你用了Redis快取,這時候你也許得考慮主從架構。因為主從架構,你可能得考慮主從切換。同時也許你還得考慮叢集模式。這就大大的提高了開發與維護的成本。因此選擇一種合適的技術是十分重要的。

非同步解耦

“簡單”的訊息佇列與kafka

  1. 如果是單體應用,在使用者下單時,下訂單,減庫存,物流記錄這三個操作是同步阻塞的。如果將這三步的操作存放到各自的訊息佇列中,然後監聽這三個訊息佇列,那麼可以大大的減少時間。
  2. 但同時,由於是分散式應用,你可能得考慮分散式事務的必要性了。此外,為了保證MQ的高可用,你可能得去調研市場上叢集模式支援的最好的中介軟體了。

流量削峰

  1. 在一些秒殺場景,為了防止極高的併發,將資料庫沖垮,除了可以在負載均衡端進行限流的設定,同時也可以將使用者的請求資料存放到訊息佇列中,然後通過訊息佇列中控制處理速度。
  2. 同時這個處理的程式可以監聽zk的某個節點,在秒殺結束後,修改這個節點的值,程式監聽到這個節點值的變化,將不再處理請求(可以做成攔截器)。這個有時也被用來作為大資料中流處理的緩衝

“簡單”的訊息佇列與kafka

市面上的訊息佇列

RabbitMQ

介紹

RabbitMQ 試用於邏輯相對複雜,同時對於佇列的效能需求相對不高的場景。

模型

“簡單”的訊息佇列與kafka

  1. RabbitMQ傳送訊息時會先傳送到Exchange(交換機),指定routing key
  2. Exchange和佇列繫結時指定 binding key
  3. 只有當routing keybinding key 的匹配規則成立時,才會將訊息發往指定的佇列。
  4. 兩個消費者監聽同一個佇列,一條訊息不會被兩個佇列同時消費。這是和kafka有區別的。

kafka

kafka topic

RabbitMQ中 資料儲存的最小單位是queue,而在kafka中 的最小單位是partition,partition包含在topic中,見下圖。

直連模型

“簡單”的訊息佇列與kafka
一個消費組gourp1 監聽一個topic, c1和c2會分別消費一個分割槽。 如果是一個消費組的話,必須指定預設消費者組名稱

訂閱模型

“簡單”的訊息佇列與kafka
多個消費組(每個組中只有一個消費者), 監聽一個topic,每個消費者會消費topic中的所有分割槽。

kafka 偏移量(offset)

每條訊息在分割槽中都會有一個偏移量。在訊息進入分割槽前,會給新的訊息分配一個唯一的偏移量,保證了每個分割槽中訊息的順序性。
RabbitMQ中, 多個消費者監聽一個佇列,由於訊息是非同步傳送的,由於網路等原因,可能Consumer2先接受到Message2,這樣會導致訊息的處理順序不會按照訊息存入佇列的順序。當然,我們可以通過一個消費者監聽一個佇列來保證訊息的有序性。
而kafka呢,多個消費者在一個消費者組中,監聽一個topictopic中的不同的分割槽會被不同的消費者消費(一個消費者消費一個分割槽)。當然,我們只能保證每個分割槽的訊息時被順序消費的,不同的分割槽則不能保證了。

kafka實戰

一個分割槽,一個消費者組,一個消費者

/** 一個消費者 一個消費者組 一個分割槽 **/
    public void send1() {
        kafkaTemplate.send("test1",0,"test1","data1");
        System.out.println("生產者傳送訊息");
    }
複製程式碼
@KafkaListener(topics = {"test1"}, groupId = "test1")
    public void test1(ConsumerRecord<?, ?> record) {
        Optional<?> kafkaMessage = Optional.ofNullable(record.value());
        if (kafkaMessage.isPresent()) {
            Object message = kafkaMessage.get();
            System.out.println("消費者接受訊息" + record);
        }
    }
複製程式碼
生產者傳送訊息
消費者接受訊息ConsumerRecord(topic = test1, partition = 0, offset = 0, CreateTime = 1560260495702, serialized key size = 5, serialized value size = 5, headers = RecordHeaders(headers = [], isReadOnly = false), key = test1, value = data1)
複製程式碼

2個分割槽,一個消費者組,一個消費者,消費所有分割槽

## 增加分割槽數
kafka-topics --zookeeper localhost:2181 --alter --topic test1 --partitions 2
## 結果
WARNING: If partitions are increased for a topic that has a key, the partition logic or ordering of the messages will be affected
Adding partitions succeeded!
複製程式碼
## 生產者
kafkaTemplate.send("test1",0 #分割槽數,"test1","data1");
kafkaTemplate.send("test1",1,"test11","data11");
System.out.println("生產者傳送訊息");
複製程式碼
## 消費者
@KafkaListener(topics = {"test1"}, groupId = "test1")
    public void test1(ConsumerRecord<?, ?> record) {
        Optional<?> kafkaMessage = Optional.ofNullable(record.value());
        if (kafkaMessage.isPresent()) {
            Object message = kafkaMessage.get();
            System.out.println("消費者1接受訊息" + record);
        }
    }
複製程式碼
消費者1接受訊息ConsumerRecord(topic = test1, partition = 0, offset = 4, CreateTime = 1560261029521, serialized key size = 5, serialized value size = 5, headers = RecordHeaders(headers = [], isReadOnly = false), key = test1, value = data1)
消費者1接受訊息ConsumerRecord(topic = test1, partition = 1, offset = 1, CreateTime = 1560261029521, serialized key size = 6, serialized value size = 6, headers = RecordHeaders(headers = [], isReadOnly = false), key = test11, value = data11)
複製程式碼

一個分割槽,2個消費者組,2個消費者 ,每個消費者都消費一個分割槽

/** 2個消費者 2個消費者組 1個分割槽 **/
    public void send3() {
        kafkaTemplate.send("test1",0,"test1","data1");
        System.out.println("生產者傳送訊息");
    }
複製程式碼
@KafkaListener(topics = {"test1"}, groupId = "test1")
    public void test1(ConsumerRecord<?, ?> record) {

        Optional<?> kafkaMessage = Optional.ofNullable(record.value());

        if (kafkaMessage.isPresent()) {

            Object message = kafkaMessage.get();
            System.out.println("消費者1接受訊息" + record);
        }
    }

    @KafkaListener(topics = {"test1"}, groupId = "test2")
    public void test2(ConsumerRecord<?, ?> record) {

        Optional<?> kafkaMessage = Optional.ofNullable(record.value());

        if (kafkaMessage.isPresent()) {

            Object message = kafkaMessage.get();
            System.out.println("消費者2接受訊息" + record);
        }
    }
複製程式碼
消費者1接受訊息ConsumerRecord(topic = test1, partition = 0, offset = 7, CreateTime = 1560261183386, serialized key size = 5, serialized value size = 5, headers = RecordHeaders(headers = [], isReadOnly = false), key = test1, value = data1)
消費者2接受訊息ConsumerRecord(topic = test1, partition = 0, offset = 7, CreateTime = 1560261183386, serialized key size = 5, serialized value size = 5, headers = RecordHeaders(headers = [], isReadOnly = false), key = test1, value = data1)
複製程式碼

2個分割槽,2個消費者,1個消費者組(負載均衡)

/** 2個消費者 1個消費者組 2個分割槽 **/
    public void send4() {
        kafkaTemplate.send("test1",0,"test1","data1");
        kafkaTemplate.send("test1",1,"test11","data11");
        System.out.println("生產者傳送訊息");

    }
複製程式碼
    @KafkaListener(topics = {"test1"}, groupId = "test1")
    public void test1(ConsumerRecord<?, ?> record) {

        Optional<?> kafkaMessage = Optional.ofNullable(record.value());

        if (kafkaMessage.isPresent()) {

            Object message = kafkaMessage.get();
            System.out.println("消費者1接受訊息" + record);
        }
    }

    @KafkaListener(topics = {"test1"}, groupId = "test1")
    public void test2(ConsumerRecord<?, ?> record) {

        Optional<?> kafkaMessage = Optional.ofNullable(record.value());

        if (kafkaMessage.isPresent()) {

            Object message = kafkaMessage.get();
            System.out.println("消費者2接受訊息" + record);
        }
    }
複製程式碼
消費者1接受訊息ConsumerRecord(topic = test1, partition = 1, offset = 3, CreateTime = 1560261444482, serialized key size = 6, serialized value size = 6, headers = RecordHeaders(headers = [], isReadOnly = false), key = test11, value = data11)
消費者2接受訊息ConsumerRecord(topic = test1, partition = 0, offset = 9, CreateTime = 1560261444482, serialized key size = 5, serialized value size = 5, headers = RecordHeaders(headers = [], isReadOnly = false), key = test1, value = data1)
複製程式碼

2個分割槽,3個消費者,1個消費者組(有個消費者未消費分割槽)

/** 2個消費者 1個消費者組 2個分割槽 **/
    public void send4() {
        kafkaTemplate.send("test1",0,"test1","data1");
        kafkaTemplate.send("test1",1,"test11","data11");
        System.out.println("生產者傳送訊息");

    }
複製程式碼
    @KafkaListener(topics = {"test1"}, groupId = "test1")
    public void test1(ConsumerRecord<?, ?> record) {

        Optional<?> kafkaMessage = Optional.ofNullable(record.value());

        if (kafkaMessage.isPresent()) {

            Object message = kafkaMessage.get();
            System.out.println("消費者1接受訊息" + record);
        }
    }

    @KafkaListener(topics = {"test1"}, groupId = "test1")
    public void test2(ConsumerRecord<?, ?> record) {

        Optional<?> kafkaMessage = Optional.ofNullable(record.value());

        if (kafkaMessage.isPresent()) {

            Object message = kafkaMessage.get();
            System.out.println("消費者2接受訊息" + record);
        }
    }
    
    @KafkaListener(topics = {"test1"}, groupId = "test1")
    public void test3(ConsumerRecord<?, ?> record) {

        Optional<?> kafkaMessage = Optional.ofNullable(record.value());

        if (kafkaMessage.isPresent()) {

            Object message = kafkaMessage.get();
            System.out.println("消費者3接受訊息" + record);
        }
    }
複製程式碼
消費者1接受訊息ConsumerRecord(topic = test1, partition = 1, offset = 3, CreateTime = 1560261444482, serialized key size = 6, serialized value size = 6, headers = RecordHeaders(headers = [], isReadOnly = false), key = test11, value = data11)
消費者2接受訊息ConsumerRecord(topic = test1, partition = 0, offset = 9, CreateTime = 1560261444482, serialized key size = 5, serialized value size = 5, headers = RecordHeaders(headers = [], isReadOnly = false), key = test1, value = data1)
複製程式碼

kafka訊息傳送機制

xxxx

未完待續

相關文章