mqtt的messageId是怎麼回事

昨天去巡檢線上環境的時候,偶然發現了某個服務報了一個錯誤,而且是每隔90秒報一次,錯誤信息如下:

意思是內部錯誤,沒有新的messageid可以使用了。

消息隊列就不多說了。正常的情況就是一個消息會有一個消息id,如果不瞭解mqtt的消息id的話,我們正常人的思維就是這個消息id是個隨機數,因爲消息的id是int類型,所以最大值是2^31-1,大概是21億,對於一個龐大的系統而且是持續運行的系統,消息肯定會滿天飛,數量級可能是百億甚至千億,那這個id能裝得下麼。結合着這個疑問和這個報錯,捋了一下這塊的代碼。

 

對於消息隊列來講,就是生產者、消費者兩個角色。 生產者很簡單,生產消息,簡單的可以理解爲建立連接、發送消息、關閉連接。 消費者是監聽某個隊列,如果有消息就去接收,並做處理,那麼對於消費者來講就是一個持續連接的狀態。

下面先貼下僞代碼:

1、相關依賴:

 <!--lmq-mqtt-->
        <dependency>
            <groupId>org.eclipse.paho</groupId>
            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun.openservices</groupId>
            <artifactId>ons-client</artifactId>
            <version>1.3.2.Final</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.32</version>
        </dependency>

2、消費者,採用的是異步監聽的方式

@Component
public class MQTTAsyncClientFactory implements ApplicationRunner {
    private static MqttAsyncClient mqttAsyncClient;

    private MQTTAsyncClientFactory() {
    }

    public static MqttAsyncClient getMqttAsyncProducerClient() throws MqttException {
        return mqttAsyncClient;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        LMQProperties lmqProperties = LMQProperties.getInstance();
        String clientId = lmqProperties.getGroupId() + "@@@" + lmqProperties.getDeviceId() + InetAddress.getLocalHost().getHostAddress().toString().replaceAll("\\.", "");
        final MemoryPersistence memoryPersistence = new MemoryPersistence();
        mqttAsyncClient = new MqttAsyncClient(lmqProperties.getBrokerUrl(), clientId, memoryPersistence);
        MqttConnectOptions connOpts = new MqttConnectOptions();
        connOpts.setUserName(lmqProperties.getUserName());
        connOpts.setPassword(lmqProperties.getPassWord().toCharArray());
        connOpts.setCleanSession(lmqProperties.getCleanSession());
        connOpts.setKeepAliveInterval(10);
        connOpts.setAutomaticReconnect(true);
        connOpts.setMaxInflight(lmqProperties.getMaxInflight());
        mqttAsyncClient.setCallback(new MqttCallbackExtended() {
            @Override
            public void connectComplete(boolean reconnect, String serverURI) {
                try {
                    //訂閱Topic,可以訂閱多個
                    final String topicFilter[] = {lmqProperties.getTopic() + "/" + lmqProperties.getRecieveTopic2() + "/" + lmqProperties.getTopic3()};
                    //設置QoS級別
                    final int[] qos = {lmqProperties.getQos()};
                    Thread.sleep(2000);
                    System.out.println("連接成功,訂閱消息");
                    IMqttToken subscribe = mqttAsyncClient.subscribe(topicFilter, qos);
                    System.out.println("消息ID:"+subscribe.getMessageId()+" Thread="+Thread.currentThread().getName());
                } catch (MqttException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void connectionLost(Throwable throwable) {
                throwable.printStackTrace();
            }

            @Override
            public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
                //處理pos返回消息
                rececive(new String(mqttMessage.getPayload()));
            }

            @Override
            public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
                //this notice make sense when qos >0
                int serialid = iMqttDeliveryToken.getMessageId();
                System.out.println("消息id是"+serialid+" Thread="+Thread.currentThread().getName());
            }
        });
        mqttAsyncClient.connect(connOpts).waitForCompletion();
        System.out.println("mqtt連接成功");
    }


    public static boolean rececive(String msg){
        //TODO 處理pos返回消息
        return true;
    }
}

消費者其實實現起來也很簡單,就是實現接口

ApplicationRunner,並且把類註冊到spring容器中就行了。

ApplicationRunner接口很簡單:

public interface ApplicationRunner {

	/**
	 * Callback used to run the bean.
	 * @param args incoming application arguments
	 * @throws Exception on error
	 */
	void run(ApplicationArguments args) throws Exception;

}

這個接口的註釋說的也很明確:

Interface used to indicate that a bean should <em>run</em> when it is contained within a {@link SpringApplication}. 
Multiple {@link ApplicationRunner} beans can be defined within the same application context and 
can be ordered using the {@link Ordered} interface or {@link Order @Order} annotation.

意思是隻要你這個bean實現了這個接口,並且包含在spring容器中,就會執行實現的run方法。多個bean可以在同一個spring上下文中,並且可以通過order接口或註解來進行指定執行順序。

run方法會在spring容器啓動之後進行執行,

這裏面有一個對象MqttAsyncClient,是在spring容器啓動之後進行實例化而且實例化了一次。

正常同步的話,我們可以在

mqttAsyncClient.connect(connOpts)之後,進行訂閱
mqttAsyncClient.subscribe(topicFilter, qos);

但是在這個demo裏面,我們是在連接成功之後,回調確認連接成功的時候進行的監聽,應該是一樣的。因爲確認連接的過程實際上也是發送了一條消息。

LMQProperties這個類是一個單例,封裝了mqtt連接的信息,具體配置的值我已經去掉了。
@Component
public class LMQProperties {
    //服務地址
    private String brokerUrl;
    //一級topic
    private String topic;
    //二級發送消息topic   商戶ID_標識符   標識符:用1/0標記,1:消息從雲平臺到POS,0:消息從POS到雲平臺
    private String sendTopic2;
    //二級接收消息topic
    private String recieveTopic2;
    //三級topic
    private String topic3;
    //服務系統用戶名
    private String userName;
    //
    private String passWord;
    //客戶端配置分組id 在控制檯創建後使用
    private String groupId;
    //Device ID: 每個設備獨一無二的標識,由業務方自己指定。需要保證全局唯一,例如每個傳感器設備的序列號。
    //與groupId 一起生成 clientID = groupId+@@@+deviceId
    private String deviceId;
    //QoS級別
    private Integer qos;
    //設置客戶端是否使用持久化模式
    private Boolean cleanSession;
    //允許未能及時收到broker回覆的ack時的MQ消息最大數量
    private int maxInflight;
    //老版本隊列的企業號白名單
    private String companyIdsWhite;

    private LMQProperties(){
        brokerUrl = "";
        topic = "";
        sendTopic2 = "test";
        recieveTopic2 = "";
        topic3 = "";
        userName="";
        passWord="";
        groupId="";
        deviceId="";
        qos=1;
        cleanSession= false;
        maxInflight=100000;
    }

    private static LMQProperties INSTANCE = new LMQProperties();
    public static LMQProperties getInstance(){
        return INSTANCE;
    }

    public String getBrokerUrl() {
        return brokerUrl;
    }

    public void setBrokerUrl(String brokerUrl) {
        this.brokerUrl = brokerUrl;
    }

    public String getTopic() {
        return topic;
    }

    public void setTopic(String topic) {
        this.topic = topic;
    }

    public String getSendTopic2() {
        return sendTopic2;
    }

    public void setSendTopic2(String sendTopic2) {
        this.sendTopic2 = sendTopic2;
    }

    public String getRecieveTopic2() {
        return recieveTopic2;
    }

    public void setRecieveTopic2(String recieveTopic2) {
        this.recieveTopic2 = recieveTopic2;
    }

    public String getTopic3() {
        return topic3;
    }

    public void setTopic3(String topic3) {
        this.topic3 = topic3;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassWord() {
        return passWord;
    }

    public void setPassWord(String passWord) {
        this.passWord = passWord;
    }

    public String getGroupId() {
        return groupId;
    }

    public void setGroupId(String groupId) {
        this.groupId = groupId;
    }

    public String getDeviceId() {
        return deviceId;
    }

    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }

    public Integer getQos() {
        return qos;
    }

    public void setQos(Integer qos) {
        this.qos = qos;
    }

    public Boolean getCleanSession() {
        return cleanSession;
    }

    public void setCleanSession(Boolean cleanSession) {
        this.cleanSession = cleanSession;
    }

    public int getMaxInflight() {
        return maxInflight;
    }

    public void setMaxInflight(int maxInflight) {
        this.maxInflight = maxInflight;
    }

    public String getCompanyIdsWhite() {
        return companyIdsWhite;
    }

    public void setCompanyIdsWhite(String companyIdsWhite) {
        this.companyIdsWhite = companyIdsWhite;
    }

3、生產者

 public static void publishMsg(int cnt) throws Exception {
        MqttAsyncClient mqttClient = MQTTAsyncClientFactory.getMqttAsyncProducerClient();
        LMQProperties lmqProperties = LMQProperties.getInstance();
        //拼接topic
        final String mqttSendTopic = lmqProperties.getTopic() + "/" + lmqProperties.getRecieveTopic2() + "/" + lmqProperties.getTopic3();
        MqttMessage message = new MqttMessage("我是消息內容".getBytes());
        //設置QoS級別
        message.setQos(lmqProperties.getQos());
        mqttClient.publish(mqttSendTopic, message);
        if(cnt%1000==0)
        System.out.println("這是第"+cnt+"次發送消息");

    }

生產者很簡單,就是執行一下publish就可以了。

 

消息想產生一定是會在發消息的時候生成,所以接下來就可以跟下publish方法是怎麼生成消息id的。

publish調用的是

ClientComms的sendNoWait,

 

 

最終調用的是ClientState的send方法

getNextMessageId()方法就是生成新消息id的方法:
private synchronized int getNextMessageId() throws MqttException {
		int startingMessageId = nextMsgId;
		// Allow two complete passes of the message ID range. This gives
		// any asynchronous releases a chance to occur
		int loopCount = 0;
	    do {
	        nextMsgId++;
	        if ( nextMsgId > MAX_MSG_ID ) {
	            nextMsgId = MIN_MSG_ID;
	        }
	        if (nextMsgId == startingMessageId) {
	        	loopCount++;
	        	if (loopCount == 2) {
	        		throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_NO_MESSAGE_IDS_AVAILABLE);
	        	}
	        }
	    } while( inUseMsgIds.containsKey( new Integer(nextMsgId) ) );
	    Integer id = new Integer(nextMsgId);
	    inUseMsgIds.put(id, id);
	    return nextMsgId;
	}

這個方法是個同步方法,說的就是給消息獲取新的消息id,並且把它存到一個已經在使用的集合裏面作爲標記。

 

而這個方法裏面的這個異常便是我們的那個異常了

在這個方法裏面,涉及到這幾個變量:

nextMsgId默認是0, 最小消息id是1,最大消息id是65535。inUseMsgIds這個標記已經使用的消息id的集合是個hashtable。

這個方法大致解釋一下,就是進入這個方法之後,會將當前nextMsgId的值存到startingMessageId這個變量上。

定義一個循環數量的變量loopCount,如果循環了達到兩次,就拋出那個異常。

進入循環的判斷條件就是nextMsgId在inUseMsgIds的集合裏面。

循環裏面,如果nextMsgId大於最大消息id的時候,會被設置爲最小消息id。 所以從這裏可以看出,這裏的消息id是循環重複使用的。那麼什麼情況下會出現那個異常呢,就是當從1~65535的消息id都放到了inUseMsgIds裏面,並且nextMsgId達到了最大值65535的時候,nextMsgId會被重置爲1,會重新再跑一遍1~65535的while循環,就會報那個錯了。

inUseMsgIds是記錄已經使用的消息id。那麼他裏面的內容什麼時候被清理的呢,ClientState裏面提供了

clearState(),close()方法,進行inUseMsgIds.clear()。 而clearState()是在
shutdownConnection的時候執行的。 也就是說在mqtt的client失去連接或者關閉的時候,就會清理inUseMsgIds。

如果只有這個機制才能清理,那麼如果一直不失去連接或者不關閉連接,豈不是就會報那個錯誤,設計一定不會這麼傻。所以ClientState還提供了以下方法:

private synchronized void releaseMessageId(int msgId) {
   inUseMsgIds.remove(new Integer(msgId));
}

這個方法總共有三個調用者:

CommsSender,CommsReceiver,CommsCallback

 

CommsSender在發送成功之後,會調用ClientState.notifySend()方法,進而調用releaseMssageId()。

CommsReceiver在收到ack的消息的時候,會調用ClientState.notifyReceivedAck()方法,進而調用releaseMssageId()。
CommsCallback當等待者和回調處理完消息後調用,會調用ClientState.notifyComplete()方法,進而調用releaseMssageId()。

所以正常情況下,只要消息發送成功,消息成功消費,基本就不存在報那個錯。因爲消息的id產生了,只要不用了就會被釋放。

所以非常遺憾,線上服務重啓後就沒有再出現這個問題,所以也沒有排查到當時爲啥會出現那個錯誤。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章