-
同步呼叫
- 基於OpenFeign的呼叫都屬於是同步呼叫,等待上一個需求結束,開始下一個需求。
- 有缺點:
- 擴充性差:每次有新的需求,現有支付邏輯都要跟著變化,程式碼經常變動,不符合開閉原則,擴充性不好。
- 效能下降:每次遠端呼叫,呼叫者都是阻塞等待狀態。
- 級聯失敗:當某一個服務出現故障時,整個事務都會回滾,交易失敗。
-
非同步呼叫:訊息傳送者,訊息Broker,訊息接收者
-
非同步呼叫的優勢包括:
- 耦合度更低
- 效能更好
- 業務擴充性強
- 故障隔離,避免級聯失敗
-
缺點:
- 完全依賴於Broker的可靠性、安全性和效能
- 架構複雜,後期維護和除錯麻煩
-
-
技術選型
目前國內訊息佇列使用最多的還是RabbitMQ,再加上其各方面都比較均衡,穩定性也好。 -
RabbitMQ是基於Erlang語言開發的開源訊息通訊中介軟體。
RabbitMQ對應的架構如圖:
- publisher:生產者,也就是傳送訊息的一方
- consumer:消費者,也就是消費訊息的一方
- queue:佇列,儲存訊息。生產者投遞的訊息會暫存在訊息佇列中,等待消費者處理
- exchange:交換機,負責訊息路由。生產者傳送的訊息由交換機決定投遞到哪個佇列。
- virtual host:虛擬主機,起到資料隔離的作用。每個虛擬主機相互獨立,有各自的exchange、queue
-
SpringAMQP:由於RabbitMQ採用了AMQP協議,因此它具備跨語言的特性。任何語言只要遵循AMQP協議收發訊息,都可以與RabbitMQ互動。並且RabbitMQ官方也提供了各種不同語言的客戶端。但是,RabbitMQ官方提供的Java客戶端編碼相對複雜,一般生產環境下我們更多會結合Spring來使用。而Spring的官方剛好基於RabbitMQ提供了這樣一套訊息收發的模板工具:SpringAMQP。並且還基於SpringBoot對其實現了自動裝配,使用起來非常方便。
-
SpringAMQP提供了三個功能:
- 自動宣告佇列、交換機及其繫結關係
- 基於註解的監聽器模式,非同步接收訊息
- 封裝了RabbitTemplate工具,用於傳送訊息
-
-
快速使用
- 引入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
- 新增配置
spring: rabbitmq: host: 192.168.150.101 # 你的虛擬機器IP port: 5672 # 埠 virtual-host: /hmall # 虛擬主機 username: hmall # 使用者名稱 password: 123 # 密碼
- 進行測試,訊息傳送
@SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSimpleQueue() { // 佇列名稱 String queueName = "simple.queue"; // 訊息 String message = "hello, spring amqp!"; // 傳送訊息 rabbitTemplate.convertAndSend(queueName, message); } }
- 訊息接收
package com.itheima.consumer.listener; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class SpringRabbitListener { // 利用RabbitListener來宣告要監聽的佇列資訊 // 將來一旦監聽的佇列中有了訊息,就會推送給當前服務,呼叫當前方法,處理訊息。 // 可以看到方法體中接收的就是訊息體的內容 @RabbitListener(queues = "simple.queue") public void listenSimpleQueueMessage(String msg) throws InterruptedException { System.out.println("spring 消費者接收到訊息:【" + msg + "】"); } }
-
WorkQueues模型: 讓多個消費者繫結到一個佇列,共同消費佇列中的訊息,能夠提高訊息處理的速度。但是這是訊息是平均分配給每個消費者的,並沒有考慮到消費者的處理能力。
- 修改consumer服務的application.yml檔案,新增配置
spring: rabbitmq: listener: simple: prefetch: 1 # 每次只能獲取一條訊息,處理完成才能獲取下一個訊息
這樣充分利用了每一個消費者的處理能力,可以有效避免訊息積壓問題。
Work模型的使用:
- 多個消費者繫結到一個佇列,同一條訊息只會被一個消費者處理
- 透過設定prefetch來控制消費者預取的訊息數量
-
Fanout交換機:廣播,FanoutExchange的會將訊息路由到每個繫結的佇列
- 可以有多個佇列
- 每個佇列都要繫結到Exchange(交換機)
- 生產者傳送的訊息,只能傳送到交換機
- 交換機把訊息傳送給繫結過的所有佇列
- 訂閱佇列的消費者都能拿到訊息
-
Direct交換機:不同的訊息被不同的佇列消費
- 佇列與交換機的繫結,不能是任意繫結了,而是要指定一個RoutingKey(路由key)
- 訊息的傳送方在 向 Exchange傳送訊息時,也必須指定訊息的 RoutingKey。
- Exchange不再把訊息交給每一個繫結的佇列,而是根據訊息的Routing Key進行判斷,只有佇列的Routingkey與訊息的 Routing key完全一致,才會接收到訊息
-
Topic交換機:可以讓佇列在繫結BindingKey 的時候使用萬用字元
萬用字元規則:
'#':匹配一個或多個詞
*:匹配不多不少恰好1個詞 -
Java程式碼的形式宣告佇列和交換機
- fanout示例
package com.itheima.consumer.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.FanoutExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FanoutConfig { /** * 宣告交換機 * @return Fanout型別交換機 */ @Bean public FanoutExchange fanoutExchange(){ return new FanoutExchange("hmall.fanout"); } /** * 第1個佇列 */ @Bean public Queue fanoutQueue1(){ return new Queue("fanout.queue1"); } /** * 繫結佇列和交換機 */ @Bean public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange); } /** * 第2個佇列 */ @Bean public Queue fanoutQueue2(){ return new Queue("fanout.queue2"); } /** * 繫結佇列和交換機 */ @Bean public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange); } }
- direct示例:direct模式由於要繫結多個KEY,會非常麻煩,每一個Key都要編寫一個binding
package com.itheima.consumer.config; import org.springframework.amqp.core.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class DirectConfig { /** * 宣告交換機 * @return Direct型別交換機 */ @Bean public DirectExchange directExchange(){ return ExchangeBuilder.directExchange("hmall.direct").build(); } /** * 第1個佇列 */ @Bean public Queue directQueue1(){ return new Queue("direct.queue1"); } /** * 繫結佇列和交換機 */ @Bean public Binding bindingQueue1WithRed(Queue directQueue1, DirectExchange directExchange){ return BindingBuilder.bind(directQueue1).to(directExchange).with("red"); } /** * 繫結佇列和交換機 */ @Bean public Binding bindingQueue1WithBlue(Queue directQueue1, DirectExchange directExchange){ return BindingBuilder.bind(directQueue1).to(directExchange).with("blue"); } /** * 第2個佇列 */ @Bean public Queue directQueue2(){ return new Queue("direct.queue2"); } /** * 繫結佇列和交換機 */ @Bean public Binding bindingQueue2WithRed(Queue directQueue2, DirectExchange directExchange){ return BindingBuilder.bind(directQueue2).to(directExchange).with("red"); } /** * 繫結佇列和交換機 */ @Bean public Binding bindingQueue2WithYellow(Queue directQueue2, DirectExchange directExchange){ return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow"); } }
- 基於註解宣告
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT), key = {"red", "blue"} )) public void listenDirectQueue1(String msg){ System.out.println("消費者1接收到direct.queue1的訊息:【" + msg + "】"); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue2"), exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT), key = {"red", "yellow"} )) public void listenDirectQueue2(String msg){ System.out.println("消費者2接收到direct.queue2的訊息:【" + msg + "】"); }
-
訊息轉換器:Spring的訊息傳送程式碼接收的訊息體是一個Object,而在資料傳輸時,它會把你傳送的訊息序列化為位元組傳送給MQ,接收訊息的時候,還會把位元組反序列化為Java物件。
預設情況下Spring採用的序列化方式是JDK序列化。眾所周知,JDK序列化存在下列問題:
-
資料體積過大:JDK 序列化會將物件轉換為位元組流進行傳輸,但這種方式可能導致序列化後的資料體積遠大於原始物件的大小
-
有安全漏洞:JDK 序列化在處理物件時存在一些安全漏洞,主要是由於它對反序列化的輸入信任過度,可能導致惡意程式碼注入、遠端程式碼執行等安全問題。
-
可讀性差:序列化後的資料通常是以位元組流的形式存在的
-
配置JSON轉換器
-
在publisher和consumer兩個服務中都引入依賴,如果專案中引入了spring-boot-starter-web依賴,則無需再次引入Jackson依賴。
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>xxx</version> </dependency>
- 配置訊息轉換器,在publisher和consumer兩個服務的啟動類中新增一個Bean即可:
@Bean public MessageConverter messageConverter(){ // 1.定義訊息轉換器 Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); // 2.配置自動建立訊息id,用於識別不同訊息,也可以在業務中基於ID判斷是否是重複訊息 jackson2JsonMessageConverter.setCreateMessageIds(true); return jackson2JsonMessageConverter; }
-
-
業務改造
-
案例需求:改造餘額支付功能,將支付成功後基於OpenFeign的交易服務的更新訂單狀態介面的同步呼叫,改為基於RabbitMQ的非同步通知。
-
定義direct型別交換機,命名為pay.direct
-
定義訊息佇列,命名為trade.pay.success.queue
-
將trade.pay.success.queue與pay.direct繫結,BindingKey為pay.success
-
支付成功時不再呼叫交易服務更新訂單狀態的介面,而是傳送一條訊息到pay.direct,傳送訊息的RoutingKey 為pay.success,訊息內容是訂單id
-
交易服務監聽trade.pay.success.queue佇列,接收到訊息後更新訂單狀態為已支付
-
配置MQ,新增依賴
<!--訊息傳送--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
- 配置MQ地址
spring: rabbitmq: host: 192.168.150.101 # 你的虛擬機器IP port: 5672 # 埠 virtual-host: /hmall # 虛擬主機 username: hmall # 使用者名稱 password: 123 # 密碼
- 在trade-service服務中定義一個訊息監聽類
package com.hmall.trade.listener; import com.hmall.trade.service.IOrderService; import lombok.RequiredArgsConstructor; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.QueueBinding; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class PayStatusListener { private final IOrderService orderService; @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "trade.pay.success.queue", durable = "true"), exchange = @Exchange(name = "pay.topic"), key = "pay.success" )) public void listenPaySuccess(Long orderId){ orderService.markOrderPaySuccess(orderId); } }
- 修改pay-service服務下的com.hmall.pay.service.impl.PayOrderServiceImpl類中的tryPayOrderByBalance方法
private final RabbitTemplate rabbitTemplate; @Override @Transactional public void tryPayOrderByBalance(PayOrderDTO payOrderDTO) { // 1.查詢支付單 PayOrder po = getById(payOrderDTO.getId()); // 2.判斷狀態 if(!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())){ // 訂單不是未支付,狀態異常 throw new BizIllegalException("交易已支付或關閉!"); } // 3.嘗試扣減餘額 userClient.deductMoney(payOrderDTO.getPw(), po.getAmount()); // 4.修改支付單狀態 boolean success = markPayOrderSuccess(payOrderDTO.getId(), LocalDateTime.now()); if (!success) { throw new BizIllegalException("交易已支付或關閉!"); } // 5.修改訂單狀態 // tradeClient.markOrderPaySuccess(po.getBizOrderNo()); try { rabbitTemplate.convertAndSend("pay.direct", "pay.success", po.getBizOrderNo()); } catch (Exception e) { log.error("支付成功的訊息傳送失敗,支付單id:{}, 交易單id:{}", po.getId(), po.getBizOrderNo(), e); } }
-
RabbitMQ簡介
相關文章
- RabbitMQ簡介及安裝MQ
- RabbitMQ 簡介以及使用場景MQ
- RabbitMQ簡介以及與SpringBoot整合示例MQSpring Boot
- RabbitMQ 介紹MQ
- RabbitMQ從零到叢集高可用(.NetCore5.0) - RabbitMQ簡介和六種工作模式詳解MQNetCore模式
- RabbitMQ(三):RabbitMQ與Spring Boot簡單整合MQSpring Boot
- Java教程之RabbitMQ介紹JavaMQ
- RabbitMQ系列(三)RabbitMQ交換器Exchange介紹與實踐MQ
- RabbitMQ-簡單佇列MQ佇列
- [我們一起來學 RabbitMQ 一 ]RabbitMQ 的基本介紹MQ
- RabbitMQ的web頁面介紹(三)MQWeb
- RabbitMQ系列(二)深入瞭解RabbitMQ工作原理及簡單使用MQ
- 關於RabbitMQ的簡單理解MQ
- RabbitMQ系列隨筆——介紹及安裝MQ
- 簡介
- rabbitmq簡易安裝詳細教程MQ
- Jira使用簡介 HP ALM使用簡介
- BookKeeper 介紹(1)--簡介
- loadsh簡介
- Knative 簡介
- Javascript 簡介JavaScript
- JanusGraph -- 簡介
- Linux簡介Linux
- CSS 簡介CSS
- 反射簡介反射
- CSS簡介CSS
- JUC簡介
- sass簡介
- APIGateway 簡介APIGateway
- Feign簡介
- Django簡介Django
- Virgilio 簡介
- 簡介JSXJS
- LVM : 簡介LVM
- Linux——簡介Linux
- Apache簡介Apache
- JAVA簡介Java
- NATS簡介