進階高階IoT架構-教你如何簡單實現一個訊息佇列

三升水發表於2021-02-22

前言

訊息佇列是軟體系統領域用來實現系統間通訊最廣泛的中介軟體。基於訊息佇列的方式是指由應用中的某個系統負責傳送訊息,由關心這條訊息的相關係統負責接收訊息,並在收到訊息後進行各自系統內的業務處理。訊息可以非常簡單,比如只包含文字字串;也可以很複雜,比如包含位元組流、位元組陣列,還可以包含嵌入物件,甚至是Java物件(經過序列化的物件)。

訊息在被髮送後可以立即返回,由訊息佇列來負責訊息的傳遞,訊息釋出者只管將訊息釋出到訊息佇列而不用管誰來取,訊息使用者只管從訊息佇列中取訊息而不管是誰釋出的,這樣釋出者和使用者都不用知道對方的存在(見下圖)。

為何要用訊息佇列

從上面的描述可以看出,訊息佇列(MQ)是一種系統間相互協作的通訊機制。那麼什麼時候需要使用訊息佇列呢?

舉個例子。某天產品人員說“系統要增加一個鍋爐裝置報警功能,當鍋爐裝置溫度大於260度後,使用者能收到郵件”。在實際場景中這種需求很常見,開發人員覺得這個很簡單,就是提供一個判斷邏輯,當鍋爐裝置溫度大於260度進行判斷,然後傳送郵件,最好返回報警資訊以警示。

該功能上線執行了一段時間後,產品人員說“裝置高溫後收到郵件的響應有點慢,很多人都提出這個意見,能不能優化一下”。開發人員首先想到的優化方案是將鍋爐裝置溫度判斷邏輯與傳送郵件分開執行,怎麼分呢?可以單獨開啟執行緒來做傳送郵件的事情。

沒多久,產品人員又說“現在裝置高溫並收到郵件的響應是快了,但有使用者反映沒收到報警郵件,能不能在傳送郵件的時候先儲存所傳送郵件的內容,如果郵件傳送失敗了則進行補發”。

看著開發人員愁眉苦臉的樣子,產品人員說“在郵件傳送這塊平臺部門已經做好方案了,你直接用他們提供的服務就行”。開發人員一聽,趕緊和平臺部門溝通,對方的答覆是“我們提供一個類似於郵局信箱的東西,你直接往這個信箱裡寫上傳送郵件的地址、郵件標題和內容,之後就不用你操心了,我們會直接從信箱裡取訊息,向你所填寫的郵件地址傳送響應郵箱”。

這個故事講的就是使用訊息佇列的典型場景---非同步處理。訊息佇列還可用於解決解耦、流量削峰、日誌收集等問題。

簡單實現一個訊息佇列

回到訊息佇列這個術語本身,它包含了兩個關鍵詞: 訊息和佇列。訊息是指在應用間傳送的資料,訊息的表現形式是多樣的,可以簡單到只包含文字字串,也可以複雜到有一個結構化的物件定義格式。對於佇列,從抽象意義上來理解,就是指訊息的進和出。從時間順序上說,進和出並不一定是同步進行的,所以需要一個容器來暫存和處理訊息。因此,一個典型意義上的訊息佇列,至少需要包含訊息的傳送、接受和暫存功能。

  • Broker: 訊息處理中心,負責訊息的接受、儲存、轉發等。
  • Producer: 訊息生產者,負責產生和傳送訊息和訊息處理中心。
  • Consumer: 訊息消費者,負責從訊息處理中心獲取訊息,並進行相應的處理。

可以看到,訊息佇列服務的核心是訊息處理中心,它至少要具備訊息傳送、訊息接受和訊息暫存功能。所以,我們就從訊息處理中心開始逐步搭建一個訊息佇列。

訊息處理中心

先看一下訊息處理中心類(InMemoryStorage)的實現

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * @author james mu
 * @date 2020/7/27 21:47
 */
public final class InMemoryStorage {

    //儲存訊息資料的容器,<topic,訊息阻塞佇列> 鍵值對
    private final ConcurrentHashMap<String, BlockingQueque<QueueMsg>> storage;
    
    private static InMemoryStorage instance;
    
    private InMemoryStorage() {
        storage = new ConcurrentHashMap<>();
    }
    //利用雙重檢查加鎖(double-checked locking),首先檢查是否示例已經建立了,如果尚未建立,"才"進行同步。這樣以來,只有第一次會同步,這正是我們想要的。
    public static InMemoryStorage getInstance() {
        if (instance == null) {
            synchronized (InMemoryStorage.class) {
                if (instance == null) {
                    instance = new InMemoryStorage();
                }
            }
        }
        return instance;
    }

  	//儲存訊息到主題中,若topic對應的value為空,會將第二個引數的返回值存入並返回
    public boolean put(String topic, QueueMsg msg) {
        return storage.computeIfAbsent(topic, (t) -> new LinkedBlockingDeque<>()).add(msg);
    }

  	//獲得主題中的訊息
    public <T extends QueueMsg> List<T> get(String topic) {
      	//判斷map中是否包含此topic
        if (storage.containsKey(topic)) {
            List<T> entities;
          	//從此主題對應的阻塞佇列中出隊一個元素
            T first = (T) storage.get(topic).poll();
            if (first != null) {
                entities = new ArrayList<>();
                entities.add(first);
                List<QueueMsg> otherList = new ArrayList<>();
                //移動阻塞佇列中最大999個元素到arrayList中
                storage.get(topic).drainTo(otherList, 999);
                for (QueueMsg other : otherList) {
                    entities.add((T) other);
                }
            } else {
                entities = Collections.emptyList();
            }
        }
        return Collections.emptyList();
    }
  
    //刪除此map中所有的鍵值對
    public void cleanup() {
        storage.clear();
    }
}

作為一個訊息處理中心中,至少要有一個資料容器用來儲存接受到的訊息。

Java中的佇列(Queue)是提供該功能的一種簡單的資料結構,同時為簡化佇列操作的併發訪問處理,我們選擇了它的一個子類LinkedBlockingDeque。該類提供了對資料的插入、獲取、查詢等操作,其底層將資料以連結串列的形式儲存。如果用 offer方法插入資料時佇列沒滿,則資料插入成功,並立 即返回:如果佇列滿了,則直接返回 false。 如果用 poll方法刪除資料時佇列不為空, 則返回隊 列頭部的資料;如果佇列為空,則立刻返回 null。

訊息格式定義

佇列訊息介面定義(QueueMsg)

/**
 * @author james mu
 * @date 2020/7/27 22:00
 */
public interface QueueMsg {
		//訊息鍵
    String getKey();
  	//訊息頭
    QueueMsgHeaders getHeaders();
		//訊息負載byte陣列
    byte[] getData();
}

佇列訊息頭介面定義(QueueMsgHeaders)

import java.util.Map;

/**
 * @author james mu
 * @date 2020/7/27 21:55
 */
public interface QueueMsgHeaders {
		//訊息頭放入
    byte[] put(String key, byte[] value);
		//訊息頭通過key獲取byte陣列
    byte[] get(String key);
		//訊息頭資料全部讀取方法
    Map<String, byte[]> getData();
}

佇列訊息格式(ProtoQueueMsg)

/**
 * @author jamesmsw
 * @date 2021/2/19 2:23 下午
 */
public class ProtoQueueMsg implements QueueMsg {
    private final String key;
    private final String value;
    private final QueueMsgHeaders headers;

    public ProtoQueueMsg(String key, String value) {
        this(key, value, new DefaultQueueMsgHeaders());
    }

    public ProtoQueueMsg(String key, String value, QueueMsgHeaders headers) {
        this.key = key;
        this.value = value;
        this.headers = headers;
    }

    @Override
    public String getKey() {
        return key;
    }

    @Override
    public QueueMsgHeaders getHeaders() {
        return headers;
    }

    @Override
    public byte[] getData() {
        return value.getBytes();
    }
}

預設佇列訊息頭(DefaultQueueMsgHeaders)

import java.util.HashMap;
import java.util.Map;

/**
 * @author james mu
 * @date 2020/7/27 21:57
 */
public class DefaultQueueMsgHeaders implements QueueMsgHeaders {

    protected final Map<String, byte[]> data = new HashMap<>();

    @Override
    public byte[] put(String key, byte[] value) {
        return data.put(key, value);
    }

    @Override
    public byte[] get(String key) {
        return data.get(key);
    }

    @Override
    public Map<String, byte[]> getData() {
        return data;
    }
}

訊息生產者

import iot.technology.mqtt.storage.msg.QueueMsg;
import iot.technology.mqtt.storage.queue.QueueCallback;

/**
 * @author james mu
 * @date 2020/8/31 11:05
 */
public class Producer<T extends QueueMsg> {

    private final InMemoryStorage storage = InMemoryStorage.getInstance();

    private final String defaultTopic;

    public Producer(String defaultTopic) {
        this.defaultTopic = defaultTopic;
    }

    public void send(String topicName, T msg) {
        boolean result = storage.put(topicName, msg);
    }
}

訊息消費者

import lombok.extern.slf4j.Slf4j;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author james mu
 * @date 2020/8/31 11:23
 */
@Slf4j
public class Consumer<T extends QueueMsg> {
    private final InMemoryStorage storage = InMemoryStorage.getInstance();
    private volatile Set<String> topics;
    private volatile boolean stopped;
    private volatile boolean subscribed;
    private final String topic;

  	//虛構函式
    public Consumer(String topic) {
        this.topic = topic;
        stopped = false;
    }

    public String getTopic() {
        return topic;
    }

    public void subscribe() {
        topics = Collections.singleton(topic);
        subscribed = true;
    }

  	//批量訂閱主題
    public void subscribe(Set<String> topics) {
        this.topics = topics;
        subscribed = true;
    }

    public void unsubscribe() {
        stopped = true;
    }

  	//不斷讀取topic集合下阻塞佇列中的資料集合
    public List<T> poll(long durationInMillis) {
        if (subscribed) {
            List<T> messages = topics
                    .stream()
                    .map(storage::get)
                    .flatMap(List::stream)
                    .map(msg -> (T) msg).collect(Collectors.toList());

            if (messages.size() > 0) {
                return messages;
            }
            try {
                Thread.sleep(durationInMillis);
            } catch (InterruptedException e) {
                if (!stopped) {
                    log.error("Failed to sleep.", e);
                }
            }
        }

        return Collections.emptyList();
    }
}

至此,一個簡單的訊息佇列中就實現完畢了。

有的同學可能會質疑我上面設計的實戰性,不用擔心,在下一節中,我將帶大家通過閱讀高達8k+?的Thingsboard的記憶體型訊息佇列原始碼,看下是否和我上面的設計一致。

相關文章