前言
在微服務的架構下,對於服務調用產生的分佈式事務問題,比較主流的解決方案有:基於XA協議的兩階段提交協議(2PC)、事務補償、消息隊列實現最終一致性(柔一致性)及阿里的GTS分佈式事務中間件。本篇文章主要採用最終一致性解決方案RabbitMQ消息中間件結合實例來深入解讀RabbitMQ的幾個高級特性
目錄
RabbitMQ介紹
RabbitMQ 即一個消息隊列遵循 AMQP(高級消息隊列協議)服務器端用Erlang語言編寫,主要是用來實現應用程序的異步和解耦,同時也能起到消息緩衝,消息分發的作用,消息的發送者無需知道消息使用者的存在,反之亦然。AMQP 的主要特徵是面向消息、隊列、路由(包括點對點和發佈/訂閱)、可靠性、安全>>>官網傳送門
RabbitMQ安裝
erlang下載
鏈接:https://pan.baidu.com/s/1EKoKLnT_0zTEWhc5JmICLg
提取碼:xk1e
可以通過訪問http://localhost:15672進行測試,默認的登陸賬號爲:guest,密碼爲:guest
管理界面可謂非常友好,在上面我們可以創建用戶、分配權限、創建交換機、創建隊列等、還有查看隊列消息,消費效率,推送效率等
RabbitMQ基本概念
生產者(Producer) > 交換器(Exchange) > 隊列(Queue) > 消費者(Consumer)
- Message
消息,消息是不具名的,它由消息頭和消息體組成。消息體是不透明的,而消息頭則由一系列的可選屬性組成,這些屬性包括 routing-key(路由鍵)、priority(相對於其他消息的優先權)、delivery-mode(指出該消息可能需要持久性存儲)等。
- Publisher
消息的生產者,也是一個向交換器發佈消息的客戶端應用程序。
- Exchange
交換器,用來接收生產者發送的消息並將這些消息路由給服務器中的隊列。
- Binding
綁定,用於消息隊列和交換器之間的關聯。一個綁定就是基於路由鍵將交換器和消息隊列連接起來的路由規則,所以可以將交換 器理解成一個由綁定構成的路由表。
- Queue
消息隊列,用來保存消息直到發送給消費者。它是消息的容器,也是消息的終點。一個消息可投入一個或多個隊列。消息一直在隊列裏面,等待消費者連接到這個隊列將其取走。
- Connection
網絡連接,比如一個TCP連接。
- Channel
信道,多路複用連接中的一條獨立的雙向數據流通道。信道是建立在真實的TCP連接內地虛擬連接,AMQP命令都是通過信道發出去的,不管是發佈消息、訂閱隊列還是接收消息,這些動作都是通過信道完成。因爲對於操作系統來說建立和銷燬 TCP 都是非常昂貴的開銷,所以引入了信道的概念,以複用一條 TCP 連接。
- Consumer
消息的消費者,表示一個從消息隊列中取得消息的客戶端應用程序。
- Virtual Host
虛擬主機,表示一批交換器、消息隊列和相關對象。虛擬主機是共享相同的身份認證和加密環境的獨立服務器域。每個 vhost本質上就是一個 mini 版的 RabbitMQ 服務器,擁有自己的隊列、交換器、綁定和權限機制。vhost 是 AMQP 概念的基礎,必須在連接時指定,RabbitMQ 默認的 vhost 是 / 。
- Broker
表示消息隊列服務器實體。
RabbitMQ三種模式
- Direct
消息中的路由鍵(routing key)如果和 Binding 中的 binding key 一致, 交換器就將消息發到對應的隊列中。路由鍵與隊列名完全匹配,如果一個隊列綁定到交換機要求路由鍵爲“dog”,則只轉發 routing key 標記爲“dog”的消息,不會轉發“dog.puppy”,也不會轉發“dog.guard”等等。它是完全匹配、單播的模式。
- Fanout
每個發到 fanout 類型交換器的消息都會分到所有綁定的隊列上去。fanout 交換器不處理路由鍵,只是簡單的將隊列綁定到交換器上,每個發送到交換器的消息都會被轉發到與該交換器綁定的所有隊列上。很像子網廣播,每臺子網內的主機都獲得了一份複製的消息。fanout 類型轉發消息是最快的。
- Topic
topic 交換器通過模式匹配分配消息的路由鍵屬性,將路由鍵和某個模式進行匹配,此時隊列需要綁定到一個模式上。它將路由鍵和綁定鍵的字符串切分成單詞,這些單詞之間用點隔開。它同樣也會識別兩個通配符:符號“#”和符號“”。#匹配0個或多個單詞,匹配不多不少一個單詞。
- headers
不常用,和direct功能接近,不討論。
上面只是簡單的介紹了一下RabbitMQ的三種模式,接下來結合代碼實例來看看
SpringBoot整合RabbitMQ
pom文件中導入amqp包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>1.5.8.RELEASE</version>
</dependency>
application.properties中配置如下信息這樣
#rabbitmq
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
這樣RabbitMQ就引入成功了,接下來以微服務下商品秒殺場景爲例,結合Direct模式來深入瞭解RabbitMQ的高級特性
創建一個RabbitMQConfig類,Direct就是一對一模式,從上面可以知道,RabbitMQ有發送者,交換機,隊列,接收者。Direct就是一個發送者對應一個接收者。如果有多個,只會有一個接收到消息
package com.giantfind.common.mq;
import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @Package: com.giantfind.common.mq
* @ClassName: RabbitMQConfig
* @Author: liuyaolong
* @Description: 配置rabbitmq
* @Version: 1.0
*/
@Configuration
public class RabbitMQConfig {
/**業務交換機*/
public static final String BUSINESS_EXCHANGE = "business.exchange";
/**常規超時時間*/
public static Long QUEUE_EXPIRATION = 20000L;
/**生成訂單隊列*/
public static final String ORDER_CREATE_QUEUE = "order.create.queue";
/**生成訂單路由鍵*/
public static final String ORDER_CREATE_ROUTING_KEY = "order.create.routing.key";
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**業務交換機*/
@Bean
public Exchange getBusinessExchange(){
return ExchangeBuilder.directExchange(BUSINESS_EXCHANGE).durable(true).build();
}
/**生成訂單隊列*/
@Bean
public Queue getOrderQueue(){
return new Queue(ORDER_CREATE_QUEUE);
}
/**綁定業務交換機和生成訂單隊列*/
@Bean
public Binding bindOrder(){
return BindingBuilder.bind(getOrderQueue()).to(getBusinessExchange()).with(ORDER_CREATE_ROUTING_KEY).noargs();
}
}
模擬秒殺下單接口(消息生產者),因爲併發性很高所以創建訂單信息放入消息隊列裏解耦,異步下單,常見的搶票軟件等等都是類似功能
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 商品秒殺
* @param orderInfo
* @return
*/
@PostMapping("/seckill")
public String seckill(@RequestBody RequestMsg<OrderInfoIn> orderInfo) throws CommonRuntimeException {
//驗證庫存
//減庫存(樂觀鎖機制)
//創建訂單消息放入創建訂單隊列...
rabbitTemplate.convertAndSend(RabbitMQConfig.BUSINESS_EXCHANGE,RabbitMQConfig.ORDER_CREATE_ROUTING_KEY,orderInfoIn);
return null;
}
訂單服務進行隊列監聽(消費者)
/**
* 生成訂單消息監聽
* @param orderInfoIn
* @param message
* @param channel
* @throws CommonRuntimeException
*/
@RabbitListener(queues = RabbitMQConfig.ORDER_CREATE_QUEUE)
public void createOrderListener(OrderInfoIn orderInfoIn,Message message, Channel channel) throws CommonRuntimeException, IOException {
try {
//創建訂單...
} catch (Exception e){
logger.error("消費創建訂單消息失敗【】error:"+ message.getBody());
logger.error("OrderConsumer handleMessage {} , error:",message,e);
}
}
這樣一個簡單的創建訂單的消息通過@RabbitListener(queues="")監聽即可進行消費。這種做法會有什麼問題呢?大家可以思考下幾個問題:隊列是如何知道該消息是否消費掉?如果沒有成功消費掉如何重試?假如消費端一直消費失敗會不會導致重複消費死循環?如何保證消費消息冥等?有沒有對應的補償機制呢?那麼接下來就是本文的重點:RabbitMQ的高級特性介紹
ACK應答機制配置
手動確認消息是否成功消費
#開啓ACK 手動確認 spring.rabbitmq.listener.direct.acknowledge-mode=manual spring.rabbitmq.listener.simple.acknowledge-mode=manual spring.rabbitmq.listener.simple.default-requeue-rejected=false
如果消費端消費失敗我們通常結合場景會重新放入隊列,代碼實例
/** * 生成訂單消息監聽 * @param orderInfoIn * @param message * @param channel * @throws CommonRuntimeException */ @RabbitListener(queues = RabbitMQConfig.ORDER_CREATE_QUEUE) public void createOrderListener(OrderInfoIn orderInfoIn,Message message, Channel channel) throws CommonRuntimeException, IOException { try { //創建訂單... //成功消費 channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } catch (Exception e){ logger.error("消費創建訂單消息失敗【】error:"+ message.getBody()); logger.error("OrderConsumer handleMessage {} , error:",message,e); //處理消息失敗,將消息重新放回隊列 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true); } }
重試機制配置
(如果消費端一直消費失敗,我們可採用重試機制來限制重複消費,從而避免消費死循環)
#開啓消費者重試 spring.rabbitmq.listener.simple.retry.enabled=true #最大重試次數 spring.rabbitmq.listener.simple.retry.max-attempts=5 #重試間隔時間(單位毫秒) spring.rabbitmq.listener.simple.retry.initial-interval=3000 #重試最大時間間隔(單位毫秒) spring.rabbitmq.listener.simple.retry.max-interval=3000 #應用於前一重試間隔的乘法器 spring.rabbitmq.listener.simple.retry.multiplier=5
TTL隊列/消息
是 Time To Live 的縮寫,也就是生存時間。1、RabbitMQ支持消息的過期時間設置,在消息發送時指定。2、RabbitMQ支持隊列的過期時間設置,從消息入隊時開始計算,只要超過隊列的超時時間配置,消息自動清除
/**生成訂單隊列*/ @Bean public Queue getOrderQueue(){ Map<String, Object> args = new HashMap<>(); //設置過期時間 args.put("x-message-ttl", QUEUE_EXPIRATION); return QueueBuilder.durable(ORDER_CREATE_QUEUE).withArguments(args).build(); }
死信隊列DLX
全稱爲 Dead-Letter-Exchange,可以稱之爲死信交換器,也有人稱之爲死信郵箱。當消息在一個隊列中變成死信 (dead message) 之後,它能被重新被髮送到另一個交換器中,這個交換器就是 DLX,綁定 DLX 的隊列就稱之爲死信隊列。消息被拒絕、消息過期、無法入隊,該何去何從,死信隊列這裏來 ,這裏可結合場景做數據落庫,或者通過管理後臺進行人工補償機制
package com.giantfind.common.mq; import org.springframework.amqp.core.*; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * @Package: com.giantfind.common.mq * @ClassName: RabbitMQConfig * @Author: liuyaolong * @Description: 配置rabbitmq * @Version: 1.0 */ @Configuration public class RabbitMQConfig { /**業務交換機*/ public static final String BUSINESS_EXCHANGE = "business.exchange"; /**死信交換機*/ public static final String DEAD_LETTER_EXCHANGE = "dead.letter.exchange"; /**常規超時時間*/ public static Long QUEUE_EXPIRATION = 20000L; /**生成訂單隊列*/ public static final String ORDER_CREATE_QUEUE = "order.create.queue"; /**生成訂單死信隊列*/ public static final String ORDER_CREATE_DEAD_LETTER_QUEUE = "order.create.dead.letter.queue"; /**生成訂單路由鍵*/ public static final String ORDER_CREATE_ROUTING_KEY = "order.create.routing.key"; /**生成訂單死信路由鍵*/ public static final String ORDER_CREATE_DEAD_LETTER_ROUTING_KEY = "order.create.dead.letter.routing.key"; @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); } /**業務交換機*/ @Bean public Exchange getBusinessExchange(){ return ExchangeBuilder.directExchange(BUSINESS_EXCHANGE).durable(true).build(); } /**死信交換機*/ @Bean public Exchange getDeadLetterExchange(){return ExchangeBuilder.directExchange(DEAD_LETTER_EXCHANGE).durable(true).build();} /**生成訂單隊列*/ @Bean public Queue getOrderQueue(){ Map<String, Object> args = new HashMap<>(); //x-dead-letter-exchange 聲明當前隊列綁定的死信交換機 args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE); //x-dead-letter-routing-key 聲明當前隊列的死信路由key args.put("x-dead-letter-routing-key", ORDER_CREATE_DEAD_LETTER_ROUTING_KEY); //設置過期時間 args.put("x-message-ttl", QUEUE_EXPIRATION); return QueueBuilder.durable(ORDER_CREATE_QUEUE).withArguments(args).build(); } /**綁定業務交換機和生成訂單隊列*/ @Bean public Binding bindOrder(){ return BindingBuilder.bind(getOrderQueue()).to(getBusinessExchange()).with(ORDER_CREATE_ROUTING_KEY).noargs(); } /**生成訂單死信隊列*/ @Bean public Queue getOrderDeadLetterQueue(){return new Queue(ORDER_CREATE_DEAD_LETTER_QUEUE);} /**綁定死信交換機和生成訂單死信隊列*/ @Bean public Binding bingOrderDeadLetter(){ return BindingBuilder.bind(getOrderDeadLetterQueue()).to(getDeadLetterExchange()).with(ORDER_CREATE_DEAD_LETTER_ROUTING_KEY).noargs(); } }
如何保證只成功消費一次(冥等性)
場景:消費端成功消費信息,但是在ack時發生網絡抖動等原因,導致消息已被消費掉,然而還存在於隊列當中。主流解決方案:唯一ID+指紋碼機制或利用Redis的原子性去實現
/** * 生成訂單消息監聽 * @param orderInfoIn * @param message * @param channel * @throws CommonRuntimeException */ @RabbitListener(queues = RabbitMQConfig.ORDER_CREATE_QUEUE) public void createOrderListener(OrderInfoIn orderInfoIn,Message message, Channel channel) throws CommonRuntimeException, IOException { //消息生產端加入雪花算法 //消費端消費前先驗證該消息是否被消費 String messageId = (String)redisTemplate.opsForValue().get(orderInfoIn.getMessageId().toString()); if(messageId == null){//保證冥等性 try { //創建訂單 redisTemplate.opsForValue().set(orderInfoIn.getMessageId().toString(), "ack"); //成功消費 channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } catch (Exception e){ logger.error("消費創建訂單消息失敗【】error:"+ message.getBody()); logger.error("OrderConsumer handleMessage {} , error:",message,e); //處理消息失敗,將消息重新放回隊列 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true); } }else{ //已消費 channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } }
延遲隊列
延遲隊列指的就是可以在固定時間長度之後纔可以被消費到,比如在訂單系統中, 一個用戶下單之後通常有 30 分鐘的時間進行支付,如果 30 分鐘之內沒有支付成功,那麼這個訂單將進行異常處理,這時就可以使用延遲隊列來處理這些訂單,延遲隊列的實現就是通過
TTL + DLX
來實現package com.giantfind.common.mq; import org.springframework.amqp.core.*; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * @Package: com.giantfind.common.mq * @ClassName: RabbitMQConfig * @Author: liuyaolong * @Description: 配置rabbitmq * @Version: 1.0 */ @Configuration public class RabbitMQConfig { /**業務交換機*/ public static final String BUSINESS_EXCHANGE = "business.exchange"; /**死信交換機*/ public static final String DEAD_LETTER_EXCHANGE = "dead.letter.exchange"; /**常規超時時間*/ public static Long QUEUE_EXPIRATION = 20000L; /**支付超時時間*/ public static Long QUEUE_PAID_EXPIRATION = 7200000L;//2小時 /**訂單待支付隊列*/ public static final String UNPAID_QUEUE = "unpaid.queue"; /**訂單待支付死信隊列*/ public static final String UNPAID_DEAD_LETTER_QUEUE = "unpaid.dead.letter.queue"; /**訂單待支付路由鍵*/ public static final String UNPAID_ROUTING_KEY = "unpaid.routing.key"; /**訂單待支付死信路由鍵*/ public static final String UNPAID_DEAD_LETTER_ROUTING_KEY = "unpaid.dead.letter.routing.key"; @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); } /**業務交換機*/ @Bean public Exchange getBusinessExchange(){ return ExchangeBuilder.directExchange(BUSINESS_EXCHANGE).durable(true).build(); } /**死信交換機*/ @Bean public Exchange getDeadLetterExchange(){return ExchangeBuilder.directExchange(DEAD_LETTER_EXCHANGE).durable(true).build();} /**訂單未支付隊列*/ @Bean public Queue getUnpaidQueue(){ Map<String,Object> args = new HashMap<>(); //x-dead-letter-exchange 聲明當前隊列綁定的死信交換機 args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE); //x-dead-letter-routing-key 聲明當前隊列的死信路由key args.put("x-dead-letter-routing-key", UNPAID_DEAD_LETTER_ROUTING_KEY); //設置過期時間 args.put("x-message-ttl", QUEUE_PAID_EXPIRATION); return QueueBuilder.durable(UNPAID_QUEUE).withArguments(args).build(); } /**綁定業務交換機和訂單未支付隊列*/ @Bean public Binding bindUnpaid(){ return BindingBuilder.bind(getUnpaidQueue()).to(getBusinessExchange()).with(UNPAID_ROUTING_KEY).noargs(); } /**訂單待支付死信隊列*/ @Bean public Queue getUnpaidDeadLetterQueue(){return new Queue(UNPAID_DEAD_LETTER_QUEUE);} /**綁定死信交換機和待支付死信隊列*/ @Bean public Binding bingUnpaidDeadLetter(){ return BindingBuilder.bind(getUnpaidDeadLetterQueue()).to(getDeadLetterExchange()).with(UNPAID_DEAD_LETTER_ROUTING_KEY).noargs(); } }
/** * 生成訂單消息監聽 * @param orderInfoIn * @param message * @param channel * @throws CommonRuntimeException */ @RabbitListener(queues = RabbitMQConfig.ORDER_CREATE_QUEUE) public void createOrderListener(OrderInfoIn orderInfoIn,Message message, Channel channel) throws CommonRuntimeException, IOException { String messageId = (String)redisTemplate.opsForValue().get(orderInfoIn.getMessageId().toString()); if(messageId == null){//保證冥等性 try { this.createOrder(orderInfoIn); redisTemplate.opsForValue().set(orderInfoIn.getMessageId().toString(), "ack"); channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); //存入訂單待支付隊列 orderInfoIn.setMessageId(idGenerator.getGlobalId());//雪花算法保證冥等性 rabbitTemplate.convertAndSend(RabbitMQConfig.BUSINESS_EXCHANGE,RabbitMQConfig.UNPAID_ROUTING_KEY,orderInfoIn); } catch (Exception e){ logger.error("消費創建訂單消息失敗【】error:"+ message.getBody()); logger.error("OrderConsumer handleMessage {} , error:",message,e); //處理消息失敗,將消息重新放回隊列 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true); } }else{ channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } } /** * 訂單未支付消息監聽 * @param message * @param channel * @throws CommonRuntimeException * */ @RabbitListener(queues = RabbitMQConfig.UNPAID_DEAD_LETTER_QUEUE) public void OrderDeadLetterListener(OrderInfoIn orderInfoIn,Message message, Channel channel) throws CommonRuntimeException, IOException { String messageId = (String)redisTemplate.opsForValue().get(orderInfoIn.getMessageId().toString()); if(messageId == null){//保證冥等性 try { //檢測訂單狀態是否爲已支付,否則回滾庫存、訂單支付超時等操作... redisTemplate.opsForValue().set(orderInfoIn.getMessageId().toString(), "ack"); //消費成功 channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } catch (Exception e){ logger.error("消費訂單未支付消息失敗【】error:"+ message.getBody()); logger.error("handleMessage {} , error:",message,e); //處理消息失敗,將消息重新放回隊列 channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,true); } }else{ //已消費 channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } }
到這裏爲止,基於SpringBoot的微服務項目裏集成RabbitMQ應用就差不多搭建完成,本文結合一些網上的理論知識概念,及實例給大家介紹其中幾個常用特性,後面有時間再介紹其他的高級特性,比如:消息100%投遞成功方案、消息持久化、消息限流等功能, 以上就是本文的全部內容,希望對大家的學習有所幫助,歡迎評論交流。能get到知識點不要忘了關注點贊~~~