RabbitMQ如何保證訊息的可達性

weixin_34247155發表於2018-11-25

一、RabbitMQ簡介

AMQP,即Advanced Message Queuing Protocol,一個提供統一訊息服務的應用層標準高階訊息佇列協議,是應用層協議的一個開放標準,為面向訊息的中介軟體設計。

RabbitMQ,又稱為高效能分散式訊息佇列,它實現了AMQP標準協議。

分散式訊息佇列有很多應用場景,比如非同步處理、應用解耦、流量削峰等。

1、非同步處理

使用者註冊後需要傳送簡訊和郵件,傳統做法是先將使用者資訊寫入資料庫,然後傳送簡訊、傳送郵件,都完成後返回。

如果用到訊息佇列,可以先將使用者資訊寫入資料庫,然後將註冊資訊寫入訊息佇列,傳送簡訊、傳送郵件或者還有其他的業務邏輯都訂閱此訊息,完成傳送。

2、應用解耦

還是上面的例子,如果在一個大型分散式網站中,使用者系統、簡訊系統、郵件系統可能都是獨立的系統服務。

這時候,在使用者註冊成功後,你可以通過RPC遠端呼叫不同的服務介面,但更好的做法還是通過訊息佇列,訂閱自己感興趣的資料,日後就算增加或者刪減功能,主業務都不用變動。

3、流量削峰

一般在秒殺或者團購活動中,流量激增,應用面臨壓力過大。可以在應用前端加入訊息佇列,通過設定佇列最大長度來限制活動人數。這時候,後端伺服器就可以遊刃有餘的處理資料了。

二、訊息通訊

在AMQP協議中,有幾個基本概念,我們必須先搞明白。

1、Virtual host

虛擬主機,每一個虛擬主機中包含所有的AMQP基本元件,使用者、隊裡、交換器等都是在虛擬主機裡面建立。典型的用法是,如果公司的多個產品只想用一個伺服器,就可以把他們劃分到不同的虛擬主機中,裡面的任何資訊都是獨立存在,互不干擾。

2、Connection

連線,應用程式和伺服器之間的TCP連線。

3、Channel

通道,當你的應用程式和伺服器連線之後,就會建立TCP連線。一旦開啟了TCP連線,就可以建立一個Channel通道,所以說Channel通道是一個TCP連線的內部邏輯單元。
這是因為,建立和銷燬TCP連線是比較昂貴的開銷,每一次訪問都建立新的TCP連線的話,不僅是巨大浪費,而且還容易造成系統效能瓶頸。

4、Queue

佇列,所有的訊息最終都會被送到這裡,等待著被感興趣的人取走。

5、Exchange

交換器,訊息到達服務的第一站就是交換器,然後根據分發規則,匹配路由鍵,將訊息放到對應佇列中。值得注意的是,交換器的型別不止一種。

  • Direct
    直連交換器,只有在訊息中的路由鍵和繫結關係中的鍵一致時,交換器才把訊息發到相應佇列

  • Fanout
    廣播交換器,只要訊息被髮送到廣播交換器,它會將訊息發到所有的佇列

  • Topic
    主題交換器,根據路由鍵,通配規則(*和#),將訊息發到相應佇列

6、Binding

繫結,交換器和佇列之間的繫結關係,繫結中就包含路由鍵,繫結資訊被儲存到交換器的查詢表中,交換器根據它分發訊息。

瞭解到這些元件相關概念後,我們總結一下來看看,一條訊息在RabbitMQ中是如何流轉的。


13230160-1d5ffa2dd8474759.png
RabbitMQ訊息流轉

三、持久化和傳送方確認

1、持久化

事實上,上圖所示只是一個最基本的訊息流轉過程,交換器和佇列這些元件還有一個比較重要的屬性:持久化。

預設情況下,重啟RabbitMQ伺服器之後,我們建立的交換器和佇列都會消失不見,當然了,如果裡面還有未來得及消費的資料,也將難於倖免。
持久化交換器和佇列,為的是在AMQP伺服器重啟之後,重新建立它們並繫結關係,在RabbitMQ中,設定durable屬性為true即可。

不過,除了這些還不夠。雖然保證了交換器和佇列是安全的,但那些還未來得及消費的資料就變得岌岌可危。所以,我們還要設定訊息的投遞模式為持久的。

這樣,如果RabbitMQ伺服器重啟的話,我們的策略和相關資料才會確保無憂。所以,我們說能從AMQP伺服器崩潰中恢復的訊息,稱之為持久化訊息。那麼,它必須保證以下三點:

  • 設定投遞模式為持久的
  • 交換器為持久的
  • 佇列為持久的

2、傳送方確認

到目前為止,我們已經保證了訊息的安全性。但是,還有另外一個問題。由於釋出操作是不返回任何資訊給生產者的,我們怎麼知道伺服器是否正確接收訊息並持久化到硬碟上了呢?

為此,我們可以將通道設定為事務模式。事務是AMQP標準中的一部分,但RabbitMQ有更好的做法,那就是傳送方確認模式,publisher confirm。如果設定了confirm模式,釋出的訊息會被分配一個唯一的ID號,等訊息被投遞給匹配的佇列後,通道會傳送一個傳送方確認模式給生產者(包含訊息的唯一ID)。

四、與Spring整合例項

廢話了這麼多,只是為了下面的程式碼部分做下鋪墊。畢竟,瞭解到上面內容之後,程式碼其實已經快要躍然紙上了。

1、配置檔案

配置檔案中我們首先要宣告RabbitMQ伺服器的資訊,IP地址、埠號、使用者名稱密碼等,但尤為重要的是,設定釋出確認模式。

<bean id="rabbitConnectionFactory"
    class="org.springframework.amqp.rabbit.connection.CachingConnectionFactory">
    <constructor-arg value="127.0.0.1"/>
    <property name="username" value="shiqizhen"/>
    <property name="password" value="shiqizhen"/>
    <property name="port" value="5672"></property>
    <property name="virtualHost" value="shiqizhen"></property>
    <property name="publisherConfirms" value="true"></property>
    <property name="publisherReturns" value="true"></property>
</bean>

接著,還要宣告交換器和佇列,記得它們是持久化的哦,durable為true。

<rabbit:admin connection-factory="rabbitConnectionFactory"/>
    
//佇列的名字、持久化、不要自動刪除、不是獨享佇列
<rabbit:queue name="userInfoQueue" durable="true" auto-delete="false" exclusive="false"/>
//交換器,型別為direct。並繫結交換器和佇列的關係,路由鍵為10086
<rabbit:direct-exchange name="user-exchange" durable="true" auto-delete="false">
    <rabbit:bindings>
        <rabbit:binding queue="userInfoQueue" key="10086"/>
    </rabbit:bindings>
</rabbit:direct-exchange>

最後,配置消費者和訊息模板

//配置消費者 ref為bean的引用 queues指明瞭消費者與佇列的關係
//重要的是acknowledge 確認模式為手動確認
<rabbit:listener-container connection-factory="rabbitConnectionFactory" acknowledge="manual">
        <rabbit:listener ref="consumerListener" queues="userInfoQueue" method="onMessage" />
</rabbit:listener-container>

//配置Spring RabbitMQ訊息模板 
<bean id="rabbitTemplate" class="org.springframework.amqp.rabbit.core.RabbitTemplate">
    <constructor-arg ref="rabbitConnectionFactory"></constructor-arg>
    <property name="confirmCallback" ref="publisherConfirm"></property>
    <property name="returnCallback" ref="returnMsgCallBack"></property>
    <property name="mandatory" value="true"></property>
</bean>

2、生產者

上面我們宣告瞭rabbitTemplate,直接用它的send方法傳送訊息即可。不過它有幾個引數必須先要了解下。

  • exchange
    交換器名稱,訊息發到哪個交換器上
  • routingKey
    路由鍵,交換器怎樣分發訊息到對應佇列
  • Message
    訊息體物件,它包含訊息的主體和訊息屬性。訊息屬性包含很多附屬資訊,比如訊息內容型別、訊息ID、使用者ID等。
  • CorrelationData
    訊息相關資料,實際它只有一個ID的屬性。不過很重要,在釋出方確認的回撥方法裡,會帶有這個引數。我們可以根據它很直觀的看到哪條訊息傳送成功或失敗。
@Controller
public class IndexController {
    
    @Autowired
    RabbitTemplate rabbitTemplate;

    @RequestMapping("/send_msg")
    @ResponseBody
    public User send_msg() {    
        String exchange = "user-exchange";
        String routingKey = "10086";
        
        User user = new User();
        String id = IdUtil.getId();
        user.setUid(id);
        user.setUsername("小小沙彌");
        user.setPassword("1234");
        user.setCreatetime(DateUtil.getDateTime(new Date()));
        
        CorrelationData correlation = new CorrelationData(id);
        Message message = new Message(JSONObject.toJSONBytes(user, SerializerFeature.WriteNullStringAsEmpty), new MessageProperties());
        logger.info("已傳送訊息到RabbitMQ伺服器:{}",JSONObject.toJSONString(user));
        rabbitTemplate.send(exchange, routingKey,message,correlation);  
        return user;
    }
}

3、消費者

消費者就是上面我們配置的listener ref引用的Bean。還記得我們把確認模式設定了手動確認,所以在消費者端有個很重要的動作,就是確認訊息。

  • channel.basicAck(deliveryTag, false)
    第一個引數是RabbitMQ內部產生的訊息ID,第二個引數代表是否批量確認訊息。通過這個指令我們告訴生產者端,訊息已經被正確消費了,RabbitMQ就會將此訊息在磁碟上刪除。
  • channel.basicReject(deliveryTag, false)
    拒絕訊息。如果消費到的訊息不是我們想要的,或者處理的時候報錯,我們可以將訊息拒絕。但值得注意的是第二個引數。如果設定為false,說明拒絕訊息並將訊息從伺服器上刪除;如果設定為true,說明拒絕訊息並將訊息重新放回佇列。如果你的消費者只有一個,最好不要把它設定為true,否則訊息會一直重試,直到把消費者端伺服器搞死。如果因為處理失敗而拒絕的話,最好將訊息刪除,同時將訊息記錄到日誌檔案或者資料庫中。
@Service
public class ConsumerListener implements ChannelAwareMessageListener{

    Logger logger = LoggerFactory.getLogger(this.getClass());
    
    public void onMessage(Message message, Channel channel) throws Exception {
        
        logger.info("消費者監聽到RabbitMQ訊息...");
        MessageProperties properties = message.getMessageProperties();
        String msg = new String(message.getBody(),"utf-8");
        logger.info("交換器:{},路由鍵:{}",properties.getReceivedExchange(),properties.getReceivedRoutingKey());
        logger.info("訊息內容:{}",msg); 
        long deliveryTag = properties.getDeliveryTag();
        channel.basicAck(deliveryTag, false);//確認資訊,false為不批量確認
        //channel.basicReject(deliveryTag, true);//true為重入佇列 false為刪除訊息
    }
}

4、傳送方確認

我們傳送訊息給RabbitMQ,第一站就是交換器。RabbitMQ是否能正確接收訊息,我們就靠它來反饋。這裡的CorrelationData就是在生產者端設定的,我們可以將它當成訊息ID,也可以直接把訊息寫入這裡。

@Component
public class PublisherConfirm implements ConfirmCallback{

    Logger logger = LoggerFactory.getLogger(this.getClass());
    
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            logger.info("訊息投遞成功!");
        }else {
            logger.warn("訊息投遞失敗,原因:{},訊息ID:{}",cause,correlationData.getId());
        }
    }
}

如果我們把交換器的名字寫錯,那麼在這裡,你就會得到以下資訊:

22:57:51,635  WARN PublisherConfirm:19 - 訊息投遞失敗,原因:
channel error; protocol method: #method<channel.close>
(reply-code=404, reply-text=NOT_FOUND - no exchange 'user-exchange_xxx' in vhost 'shiqizhen', class-id=60, method-id=40),

訊息ID:516387069669408768
  22:57:51,638 ERROR CachingConnectionFactory:1278 - Channel shutdown: 
channel error; protocol method:
 #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'user-exchange_xxx' in vhost 'shiqizhen', class-id=60, method-id=40)

5、返回回撥

除了設定RabbitMQ的傳送方確認,在Spring中還有一個publisherReturns值的我們注意。雖然我們將訊息傳送到了交換器,但交換器是否能正確將訊息分發到對應佇列,還要打個問號。如果訊息無法傳送到指定的佇列,那麼publisherReturns就會發揮作用。記住,如果想應用這個特性,需要將mandatory設定為true。

@Component
public class ReturnMsgCallBack implements ReturnCallback{

    Logger logger = LoggerFactory.getLogger(this.getClass());
    
    public void returnedMessage(Message message, int replyCode, 
                    String replyText, String exchange, String routingKey) {
        logger.info("訊息內容:{}",new String(message.getBody()));
        logger.info("回覆文字:{},回覆程式碼:{}",replyText,replyCode);
        logger.info("交換器名稱:{},路由鍵:{}",exchange,routingKey); 
    }
}

如果我們不小心寫錯了路由鍵的名字,那就會呼叫到這裡。

23:24:27,813  INFO ReturnMsgCallBack:16 - 訊息內容:{"createtime":"2018-11-25 23:24:24","password":"1234","role":null,"uid":"516393749815754752","username":"小小沙彌"}
23:24:27,814  INFO ReturnMsgCallBack:17 - 回覆文字:NO_ROUTE,回覆程式碼:312
23:24:27,814  INFO ReturnMsgCallBack:18 - 交換器名稱:user-exchange,路由鍵:10086_xxx

//這裡是傳送方確認列印的資訊 說投遞到交換器成功
23:24:27,814  INFO PublisherConfirm:17 - 訊息投遞成功!

有個問題,如同第一個例子,如果寫錯了路由鍵的名稱,傳送方確認會列印ack為false的異常資訊,但為什麼不會呼叫到publisherReturns呢?

如果路由鍵錯誤,說明訊息壓根就沒有被接收到。這肯定是一個嚴重錯誤,所以RabbitMQ直接把當前通道關閉了。

Channel shutdown: 
channel error; protocol method:
reply-code=404, reply-text=NOT_FOUND - no exchange 'user-exchange_xxx' in vhost ...

五、監聽RabbitMQ伺服器狀態

如果你的RabbitMQ服務不是一個叢集,那麼當網路故障或其他原因導致RabbitMQ服務停掉的時候,我們怎麼做呢?當然,你可以在Send方法中加入try/catch,根據catch資訊返回你的狀態。但有個更好的思路,可以結合使用。
在建立RabbitMQ服務連線的時候,我們要配置一個Bean,CachingConnectionFactory它有個方法addConnectionListener,我們可以利用它來監聽伺服器的連線狀態。

public class RabbitMQConnectionListener implements ConnectionListener{
    public void onCreate(Connection connection) {
        System.out.println("伺服器已啟動...");
    }
    public void onClose(Connection connection) {
        System.out.println("伺服器已關閉...");
    }
}

並在合適的位置,比如Spring容器初始化方法裡,加入這麼一句rabbitConnectionFactory.addConnectionListener(new RabbitMQConnectionListener());

這樣,我們就可以掌握RabbitMQ伺服器的連線狀態了,那麼我們就可以根據此狀態,在生產者方呼叫send方法的時候,判斷此狀態。如果未連線,可以先將訊息儲存到資料庫或者快取中。當連線到RabbitMQ,我們先把快取的訊息拿出來傳送,再將此狀態重置為已連線。

六、總結

本文簡單介紹了AMQP協議標準中的相關概念,以及RabbitMQ在Spring中如何正確配置使用持久化訊息、傳送方模式和返回回撥等機制。並在最後,介紹了在Spring中如何監聽RabbitMQ的伺服器連線狀態。總而言之一句話,我們將要怎樣使用RabbitMQ,才能保證訊息不會丟失。希望本文對你使用RabbitMQ有所幫助!

相關文章