原筆記鏈接:https://files.cnblogs.com/files/henuliulei/Rabbitmq%E5%85%A5%E9%97%A8%E5%88%B0%E7%B2%BE%E9%80%9A.zip
原視頻鏈接:2021年RabbitMQ入門到精通 餘勝軍(通俗易懂版本)_嗶哩嗶哩_bilibili
MQ架構設計原理
什麼是消息中間件
消息中間件基於隊列模型實現異步/同步傳輸數據
作用:可以實現支撐高併發、異步解耦、流量削峯、降低耦合度。
在瞭解中間件之前,我們先了解一下什麼是同步?
首先我們想一下,兩個公司之間如果有互相調用接口的業務需求,如果沒有引入中間件技術,是怎麼實現的呢?
用戶發起請求給系統A,系統A接到請求直接調用系統B,系統B返回結果後,系統A才能返回結果給用戶,這種模式就是同步調用。
所謂同步調用就是各個系統之間互相依賴,一個系統發送請求,其他系統也會跟着依次進行處理,只有所有系統處理完成後對於用戶來講纔算完成了一次請求。只要其他系統出現故障,就會報錯給用戶。
那麼引入中間件後,是如何做到異步調用的呢?
用戶發起請求給系統A,此時系統A發送消息給MQ,然後就返回結果給用戶,不去管系統B了。然後系統B根據自己的情況,去MQ中獲取消息,獲取到消息的時候可能已經過了1分鐘甚至1小時,再根據消息的指示執行相應的操作。
那麼想一想,系統A和系統B互相之間是否有通信?這種調用方式是同步調用嗎?
系統A發送消息給中間件後,自己的工作已經完成了,不用再去管系統B什麼時候完成操作。而系統B拉去消息後,執行自己的操作也不用告訴系統A執行結果,所以整個的通信過程是異步調用的。
說到這裏,我們可以做個總結,消息中間件到底是什麼呢?
其實消息中間件就是一個獨立部署的系統。可以實現各個系統之間的異步調用。當然它的作用可不止這些,通過它可以解決大量的技術痛點,我們接下來會進行介紹。
消息中間件,總結起來作用有三個:異步化提升性能、降低耦合度、流量削峯。
異步化提升性能
先來說說異步化提升性能,上邊我們介紹中間件的時候已經解釋了引入中間件後,是如何實現異步化的,但沒有解釋具體性能是怎麼提升的,我們來看一下下邊的圖。
沒有引入中間件的時候,用戶發起請求到系統A,系統A耗時20ms,接下來系統A調用系統B,系統B耗時200ms,帶給用戶的體驗就是,一個操作全部結束一共耗時220ms。
如果引入中間件之後呢?看下邊的圖。
用戶發起請求到系統A,系統A耗時20ms,發送消息到MQ耗時5ms,返回結果一共用了25ms,用戶體驗一個操作只用了25ms,而不用管系統B什麼時候去獲取消息執行對應操作,這樣比較下來,性能自然大大提高
降低耦合度
再來聊聊解耦的場景,看下圖。
如果沒有引入中間件,那麼系統A調用系統B的時候,系統B出現故障,導致調用失敗,那麼系統A就會接到異常信息,接到異常信息後肯定要再處理一下,返回給用戶失敗請稍後再試,這時候就得等待系統B的工程師解決問題,一切都解決好後再告知用戶可以了,再重新操作一次吧。
這樣的架構,兩個系統耦合再一起,用戶體驗極差。
那麼我們引入中間件後是什麼樣的場景呢,看下面的流程:
對於系統A,發送消息後直接返回結果,不再管系統B後邊怎麼操作。而系統B故障恢復後重新到MQ中拉取消息,重新執行未完成的操作,這樣一個流程,系統之間沒有影響,也就實現瞭解耦。
流量削峯
下面我們再聊聊最後一個場景,流量削峯
假如我們的系統A是一個集羣,不連接數據庫,這個集羣本身可以抗下1萬QPS
系統B操作的是數據庫,這個數據庫只能抗下6000QPS,這就導致無論系統B如何擴容集羣,都只能抗下6000QPS,它的瓶頸在於數據庫。
假如突然系統QPS達到1萬,就會直接導致數據庫崩潰,那麼引入MQ後是怎麼解決的呢,見下圖:
引入MQ後,對於系統A沒有什麼影響,給MQ發送消息可以直接發送1萬QPS。
此時對於系統B,可以自己控制獲取消息的速度,保持在6000QPS一下,以一個數據庫能夠承受的速度執行操作。這樣就可以保證數據庫不會被壓垮。
當然,這種情況MQ中可能會積壓大量消息。但對於MQ來說,是允許消息積壓的,等到系統A峯值過去,恢復成1000QPS時,系統B還是在以6000QPS的速度去拉取消息,自然MQ中的消息就慢慢被釋放掉了。
這就是流量削峯的過程。在電商秒殺、搶票等等具有流量峯值的場景下可以使用這麼一套架構。
傳統的http請求存在那些缺點
1.Http請求基於請求與響應的模型,在高併發的情況下,客戶端發送大量的請求達到
服務器端有可能會導致我們服務器端處理請求堆積。
2.Tomcat服務器處理每個請求都有自己獨立的線程,如果超過最大線程數會將該請求緩存到隊列中,如果請求堆積過多的情況下,有可能會導致tomcat服務器崩潰的問題。
所以一般都會在nginx入口實現限流,整合服務保護框架。
http請求處理業務邏輯如果比較耗時的情況下,容易造成客戶端一直等待,阻塞等待 過程中會導致客戶端超時發生重試策略,有可能會引發冪等性問題。
注意事項:接口是爲http協議的情況下,最好不要處理比較耗時的業務邏輯,耗時的業務邏輯應該單獨交給多線程或者是mq處理。
Mq應用場景有那些
- 異步發送短信
- 異步發送新人優惠券
- 處理一些比較耗時的操作
爲什麼需要使用mq
可以實現支撐高併發、異步解耦、流量削峯、降低耦合度。
同步發送http請求
客戶端發送請求到達服務器端,服務器端實現會員註冊業務邏輯,
1.insertMember() --插入會員數據 1s
2.sendSms()----發送登陸短信提醒 3s
3.sendCoupons()----發送新人優惠券 3s
總共響應需要6s時間,可能會導致客戶端阻塞6s時間,對用戶體驗
不是很好。
多線程與MQ方式實現異步?
互聯網項目:
客戶端 安卓/IOS
服務器端:php/java
最好使用mq實現異步
多線程處理業務邏輯
用戶向數據庫中插入一條數據之後,在單獨開啓一個線程異步發送短信和優惠操作。
客戶端只需要等待1s時間
優點:適合於小項目 實現異步
缺點:有可能會消耗服務器cpu資源資源
Mq處理業務邏輯
先向數據庫中插入一條會員數據,讓後再向MQ中投遞一個消息,MQ服務器端在將消息推送給消費者異步解耦處理髮送短信和優惠券。
Mq與多線程之間區別
MQ可以實現異步/解耦/流量削峯問題;
多線程也可以實現異步,但是消耗到cpu資源,沒有實現解耦。
Mq消息中間件名詞
Producer 生產者:投遞消息到MQ服務器端;
Consumer 消費者:從MQ服務器端獲取消息處理業務邏輯;
Broker MQ服務器端
Topic 主題:分類業務邏輯發送短信主題、發送優惠券主題
Queue 存放消息模型 隊列 先進先出 後進後出原則 數組/鏈表
Message 生產者投遞消息報文:json
主流mq區別對比
簡單的比較
特性 |
ActiveMQ |
RabbitMQ |
RocketMQ |
kafka |
開發語言 |
java |
erlang |
java |
scala |
單機吞吐量 |
萬級 |
萬級 |
10萬級 |
10萬級 |
時效性 |
ms級 |
us級 |
ms級 |
ms級以內 |
可用性 |
高(主從架構) |
高(主從架構) |
非常高(分佈式架構) |
非常高(分佈式架構) |
功能特性 |
成熟的產品,在很多公司得到應用;有較多的文檔;各種協議支持較好 |
基於erlang開發,所以併發能力很強,性能極其好,延時很低管理界面較豐富 |
MQ功能比較完備,擴展性佳 |
只支持主要的MQ功能,像一些消息查詢,消息回溯等功能沒有提供,畢竟是爲大數據準備的,在大數據領域應用廣。 |
https://copyfuture.com/blogs-details/20190816154718064u4lgjgg20z1o6id
Mq設計基礎知識
多線程版本mq;
使用LinkedBlockingDeque模擬實現多線程的方式讀寫中間件
package com.mayikt.netty; import com.alibaba.fastjson.JSONObject; import java.util.concurrent.LinkedBlockingDeque; /** * @ClassName MayiktThreadMQ * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class MayiktThreadMQ { private static LinkedBlockingDeque<JSONObject> msgs = new LinkedBlockingDeque<JSONObject>(); public static void main(String[] args) { // 生產線程 Thread producerThread = new Thread(new Runnable() { @Override public void run() { try { while (true) { Thread.sleep(1000); JSONObject data = new JSONObject(); data.put("userId", "1234"); // 存入消息 msgs.offer(data); } } catch (Exception e) { } } }, "生產者"); producerThread.start(); // 消費者線程 Thread consumerThread = new Thread(new Runnable() { @Override public void run() { try { while (true) { JSONObject data = msgs.poll(); if (data != null) { System.out.println(Thread.currentThread().getName() + "," + data); } } } catch (Exception e) { } } }, "消費者"); consumerThread.start(); } }
基於網絡通訊版本mq netty實現
生產者
package com.mayikt.netty; import com.alibaba.fastjson.JSONObject; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; /** * @ClassName MayiktNettyMQProducer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class MayiktNettyMQProducer { public void connect(int port, String host) throws Exception { //配置客戶端NIO 線程組 EventLoopGroup group = new NioEventLoopGroup(); Bootstrap client = new Bootstrap(); try { client.group(group) // 設置爲Netty客戶端 .channel(NioSocketChannel.class) /** * ChannelOption.TCP_NODELAY參數對應於套接字選項中的TCP_NODELAY,該參數的使用與Nagle算法有關。 * Nagle算法是將小的數據包組裝爲更大的幀然後進行發送,而不是輸入一次發送一次,因此在數據包不足的時候會等待其他數據的到來,組裝成大的數據包進行發送,雖然該算法有效提高了網絡的有效負載,但是卻造成了延時。 * 而該參數的作用就是禁止使用Nagle算法,使用於小數據即時傳輸。和TCP_NODELAY相對應的是TCP_CORK,該選項是需要等到發送的數據量最大的時候,一次性發送數據,適用於文件傳輸。 */ .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new MayiktNettyMQProducer.NettyClientHandler()); //// 1. 演示LineBasedFrameDecoder編碼器 // ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); // ch.pipeline().addLast(new StringDecoder()); } }); //綁定端口, 異步連接操作 ChannelFuture future = client.connect(host, port).sync(); //等待客戶端連接端口關閉 future.channel().closeFuture().sync(); } finally { //優雅關閉 線程組 group.shutdownGracefully(); } } public static void main(String[] args) { int port = 9008; MayiktNettyMQProducer client = new MayiktNettyMQProducer(); try { client.connect(port, "127.0.0.1"); } catch (Exception e) { e.printStackTrace(); } } public class NettyClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { JSONObject data = new JSONObject(); data.put("type", "producer"); JSONObject msg = new JSONObject(); msg.put("userId", "123456"); msg.put("age", "23"); data.put("msg", msg); // 生產發送數據 byte[] req = data.toJSONString().getBytes(); ByteBuf firstMSG = Unpooled.buffer(req.length); firstMSG.writeBytes(req); ctx.writeAndFlush(firstMSG); } /** * 客戶端讀取到服務器端數據 * * @param ctx * @param msg * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); System.out.println("客戶端接收到服務器端請求:" + body); } // tcp屬於雙向傳輸 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
MQ服務器
package com.mayikt.netty; import com.alibaba.fastjson.JSONObject; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import org.apache.commons.lang3.StringUtils; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.concurrent.LinkedBlockingDeque; /** * @ClassName NettyMQServer2021 * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class MayiktNettyMQServer { public void bind(int port) throws Exception { /** * Netty 抽象出兩組線程池BossGroup和WorkerGroup * BossGroup專門負責接收客戶端的連接, WorkerGroup專門負責網絡的讀寫。 */ EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); try { bootstrap.group(bossGroup, workerGroup) // 設定NioServerSocketChannel 爲服務器端 .channel(NioServerSocketChannel.class) //BACKLOG用於構造服務端套接字ServerSocket對象,標識當服務器請求處理線程全滿時, //用於臨時存放已完成三次握手的請求的隊列的最大長度。如果未設置或所設置的值小於1,Java將使用默認值50。 .option(ChannelOption.SO_BACKLOG, 100) // 服務器端監聽數據回調Handler .childHandler(new MayiktNettyMQServer.ChildChannelHandler()); //綁定端口, 同步等待成功; ChannelFuture future = bootstrap.bind(port).sync(); System.out.println("當前服務器端啓動成功..."); //等待服務端監聽端口關閉 future.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { //優雅關閉 線程組 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { // 設置異步回調監聽 ch.pipeline().addLast(new MayiktNettyMQServer.MayiktServerHandler()); } } public static void main(String[] args) throws Exception { int port = 9008; new MayiktNettyMQServer().bind(port); } private static final String type_consumer = "consumer"; private static final String type_producer = "producer"; private static LinkedBlockingDeque<String> msgs = new LinkedBlockingDeque<>(); private static ArrayList<ChannelHandlerContext> ctxs = new ArrayList<>(); // 生產者投遞消息的:topicName public class MayiktServerHandler extends SimpleChannelInboundHandler<Object> { /** * 服務器接收客戶端請求 * * @param ctx * @param data * @throws Exception */ @Override protected void channelRead0(ChannelHandlerContext ctx, Object data) throws Exception { JSONObject clientMsg = getData(data); String type = clientMsg.getString("type"); switch (type) { case type_producer: producer(clientMsg); break; case type_consumer: consumer(ctx); break; } } private void consumer(ChannelHandlerContext ctx) { // 保存消費者連接 ctxs.add(ctx); // 主動拉取mq服務器端緩存中沒有被消費的消息 String data = msgs.poll(); if (StringUtils.isEmpty(data)) { return; } // 將該消息發送給消費者 byte[] req = data.getBytes(); ByteBuf firstMSG = Unpooled.buffer(req.length); firstMSG.writeBytes(req); ctx.writeAndFlush(firstMSG); } private void producer(JSONObject clientMsg) { // 緩存生產者投遞 消息 String msg = clientMsg.getString("msg"); msgs.offer(msg); //需要將該消息推送消費者 ctxs.forEach((ctx) -> { // 將該消息發送給消費者 String data = msgs.poll(); if (data == null) { return; } byte[] req = data.getBytes(); ByteBuf firstMSG = Unpooled.buffer(req.length); firstMSG.writeBytes(req); ctx.writeAndFlush(firstMSG); }); } private JSONObject getData(Object data) throws UnsupportedEncodingException { ByteBuf buf = (ByteBuf) data; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); return JSONObject.parseObject(body); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
消費者
package com.mayikt.netty; import com.alibaba.fastjson.JSONObject; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; /** * @ClassName MayiktNettyMQProducer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class MayiktNettyMQConsumer { public void connect(int port, String host) throws Exception { //配置客戶端NIO 線程組 EventLoopGroup group = new NioEventLoopGroup(); Bootstrap client = new Bootstrap(); try { client.group(group) // 設置爲Netty客戶端 .channel(NioSocketChannel.class) /** * ChannelOption.TCP_NODELAY參數對應於套接字選項中的TCP_NODELAY,該參數的使用與Nagle算法有關。 * Nagle算法是將小的數據包組裝爲更大的幀然後進行發送,而不是輸入一次發送一次,因此在數據包不足的時候會等待其他數據的到來,組裝成大的數據包進行發送,雖然該算法有效提高了網絡的有效負載,但是卻造成了延時。 * 而該參數的作用就是禁止使用Nagle算法,使用於小數據即時傳輸。和TCP_NODELAY相對應的是TCP_CORK,該選項是需要等到發送的數據量最大的時候,一次性發送數據,適用於文件傳輸。 */ .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new MayiktNettyMQConsumer.NettyClientHandler()); //// 1. 演示LineBasedFrameDecoder編碼器 // ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); // ch.pipeline().addLast(new StringDecoder()); } }); //綁定端口, 異步連接操作 ChannelFuture future = client.connect(host, port).sync(); //等待客戶端連接端口關閉 future.channel().closeFuture().sync(); } finally { //優雅關閉 線程組 group.shutdownGracefully(); } } public static void main(String[] args) { int port = 9008; MayiktNettyMQConsumer client = new MayiktNettyMQConsumer(); try { client.connect(port, "127.0.0.1"); } catch (Exception e) { e.printStackTrace(); } } public class NettyClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { JSONObject data = new JSONObject(); data.put("type", "consumer"); // 生產發送數據 byte[] req = data.toJSONString().getBytes(); ByteBuf firstMSG = Unpooled.buffer(req.length); firstMSG.writeBytes(req); ctx.writeAndFlush(firstMSG); } /** * 客戶端讀取到服務器端數據 * * @param ctx * @param msg * @throws Exception */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); System.out.println("客戶端接收到服務器端請求:" + body); } // tcp屬於雙向傳輸 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } }
基於netty實現mq
消費者netty客戶端與nettyServer端MQ服務器端保持長連接,MQ服務器端保存
消費者連接。
生產者netty客戶端發送請求給nettyServer端MQ服務器端,MQ服務器端在將該
消息內容發送給消費者。
body:{"msg":{"userId":"123456","age":"23"},"type":"producer",”topic”:””}
生產者投遞消息給MQ服務器端,MQ服務器端需要緩存該消息
如果mq服務器端宕機之後,消息如何保證不丟失:
持久化機制
如果mq接收到生產者投遞消息,如果消費者不在的情況下,該消息是否會丟失?
不會丟失,消息確認機制 必須要消費者消費該消息成功之後,在通知給mq服務器端,刪除該消息。而kafka即使讀取消息後也不會立刻刪除,而是設置該消息被消費了,一定時間後統一刪除。
消費者已經和mq服務器保持長連接:Mq服務器端將該消息推送消費者。
消費者第一次剛啓動的時候,消費者會主動拉取消息。
Mq如何實現抗高併發思想
Mq消費者根據自身能力情況 ,拉取mq服務器端消息消費。
默認的情況下是取出一條消息。
缺點:存在延遲的問題
需要考慮mq消費者提高速率的問題:
如何消費者提高速率:消費者實現集羣、消費者批量獲取消息即可。
RabbitMQ
RabbitMQ基本介紹
RabbitMQ是實現了高級消息隊列協議(AMQP)的開源消息代理軟件(亦稱面向消息的中間件),RabbitMQ服務器是用Erlang語言編寫的。
RabitMQ官方網站:
1.點對點(簡單)的隊列
2.工作(公平性)隊列模式
3.發佈訂閱模式
4.路由模式Routing
5.通配符模式Topics
6.RPC
https://www.rabbitmq.com/getstarted.html
RabbitMQ環境的基本安裝
1.下載並安裝erlang,下載地址:http://www.erlang.org/download
2.配置erlang環境變量信息
新增環境變量ERLANG_HOME=erlang的安裝地址
將%ERLANG_HOME%\bin加入到path中
3.下載並安裝RabbitMQ,下載地址:http://www.rabbitmq.com/download.html
注意: RabbitMQ 它依賴於Erlang,需要先安裝Erlang。
https://www.rabbitmq.com/install-windows.html
如何啓動Rabbitmq
net start RabbitMQ
啓動Rabbitmq常見問題
如果rabbitmq 啓動成功無法訪問 管理平臺頁面
進入到F:\path\rabbitmq\rabbitmq\rabbitmq_server-3.6.9\sbin>
執行
rabbitmq-plugins enable rabbitmq_management
rabbitmqctl start_app
Rabbitmq管理平臺中心
RabbitMQ 管理平臺地址 http://127.0.0.1:15672
默認賬號:guest/guest 用戶可以自己創建新的賬號
Virtual Hosts:
像mysql有數據庫的概念並且可以指定用戶對庫和表等操作的權限。那RabbitMQ呢?
RabbitMQ也有類似的權限管理。在RabbitMQ中可以虛擬消息服務器VirtualHost,每
個VirtualHost相當月一個相對獨立的RabbitMQ服務器,每個VirtualHost之間是相互
隔離。exchange、queue、message不能互通。
默認的端口15672:rabbitmq管理平臺端口號
默認的端口5672: rabbitmq消息中間內部通訊的端口
默認的端口號25672 rabbitmq集羣的端口號
RabbitMQ常見名詞
/Virtual Hosts---分類
/隊列 存放我們消息
Exchange 分派我們消息在那個隊列存放起來 類似於nginx
15672---rabbitmq控制檯管理平臺 http協議
25672rabbitmq 集羣通信端口號
Amqp 5672 rabbitmq內部通信的一個端口號
RabbitMQ創建賬戶
RabbitMQ平臺創建Virtual Hosts
RabbitMQ平臺創建消息隊列
快速入門RabbitMQ簡單隊列
首先需要再RabbitMQ平臺創建Virtual Hosts 和隊列。
/myVirtualHosts
----訂單隊列
----支付隊列
- 在RabbitMQ平臺創建一個隊列;
- 在編寫生產者代碼
- 在編寫消費者代碼
下列模式用到的所有的依賴
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mayikt</groupId> <artifactId>mayikt-mq</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> <dependencies> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency> <!-- https://mvnrepository.com/artifact/io.netty/netty-all --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.54.Final</version> </dependency> <!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> <!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client --> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.25</version> <scope>compile</scope> </dependency> </dependencies> </project>
下面的程序是在普通maven工程下創建,使用到的隊列,交換機,虛擬主機都要我們手動在mq管理界面(port:15672)創建
公共類:
package com.mayikt.rabbitmq; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @ClassName RabbitMQConnection * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class RabbitMQConnection { /** * 創建連接 * * @return * @throws IOException * @throws TimeoutException */ public static Connection getConnection() throws IOException, TimeoutException { //1.創建connectionFactory ConnectionFactory connectionFactory = new ConnectionFactory(); //2.配置Host connectionFactory.setHost("127.0.0.1"); //3.設置Port connectionFactory.setPort(5672); //4.設置賬戶和密碼 connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); //5.設置VirtualHost connectionFactory.setVirtualHost("/myVirtualHost"); return connectionFactory.newConnection(); } }
點對點模式
package com.mayikt.demo00; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @ClassName Producer 演示消息確認機制 * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class Producer { private static final String QUEUE_NAME = "test_quene"; public static void main(String[] args) { try { //1.創建一個新連接 Connection connection = RabbitMQConnection.getConnection(); //2.設置channel Channel channel = connection.createChannel(); //3.發送消息 String msg = "每特教育6666"; channel.basicPublish("", QUEUE_NAME, null, msg.getBytes()); boolean result = channel.waitForConfirms(); if (result) { System.out.println("消息投遞成功"); } else { System.out.println("消息投遞失敗"); } channel.close(); connection.close(); } catch (Exception e) { } } }
package com.mayikt.demo00; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer { private static final String QUEUE_NAME = "test_quene"; public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException { // 1.創建連接 Connection connection = Producer1.getConnection(); // 2.設置通道 final Channel channel = connection.createChannel(); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("消費者獲取消息:" + msg); // // 消費者完成 消費者通知給mq服務器端刪除該消息 channel.basicAck(envelope.getDeliveryTag(), false); } }; // 3.監聽隊列 channel.basicConsume(QUEUE_NAME, false, defaultConsumer); } }
Work模式
Producer
package com.mayikt.demo01; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @ClassName Producer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class Producer { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException { //1.創建一個新連接 Connection connection = RabbitMQConnection.getConnection(); //2.設置channel Channel channel = connection.createChannel(); //3.發送消息 for (int i = 0; i < 100; i++) { String msg = "每特教育6666:i" + i; channel.basicPublish("", QUEUE_NAME, null, msg.getBytes()); } System.out.println("消息投遞成功"); channel.close(); connection.close(); } }
Consumer01
package com.mayikt.demo01; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer01 { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException { // 1.創建連接 Connection connection = RabbitMQConnection.getConnection(); // 2.設置通道 Channel channel = connection.createChannel(); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(1000); } catch (Exception e) { } String msg = new String(body, "UTF-8"); System.out.println("消費者獲取消息:" + msg); } }; // 3.監聽隊列 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
Consumer02
package com.mayikt.demo01; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer02 { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException { // 1.創建連接 Connection connection = RabbitMQConnection.getConnection(); // 2.設置通道 Channel channel = connection.createChannel(); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("消費者獲取消息:" + msg); } }; // 3.監聽隊列 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
測試結果
測試結果:
1、 消費者1和消費者2獲取到的消息內容是不同的,同一個消息只能被一個消費者獲取。
2、 消費者1和消費者2獲取到的消息的數量是相同的,一個是奇數一個是偶數。
其實,這樣是不合理的,應該是消費者1要比消費者2獲取到的消息多才對。
Work模式的“能者多勞”
Consumer1
package com.mayikt.demo02; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer1 { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException { // 1.創建連接 Connection connection = RabbitMQConnection.getConnection(); // 2.設置通道 Channel channel = connection.createChannel(); //指定我們消費者每次批量獲取消息 channel.basicQos(2); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("消費者獲取消息:" + msg); try { // 消費者完成 刪除該消息 channel.basicAck(envelope.getDeliveryTag(), false); }catch (Exception e){ } // } }; // 3.監聽隊列 channel.basicConsume(QUEUE_NAME, false, defaultConsumer); } }
Consumer2
package com.mayikt.demo02; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer2 { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException, IOException, TimeoutException { // 1.創建連接 Connection connection = RabbitMQConnection.getConnection(); // 2.設置通道 Channel channel = connection.createChannel(); channel.basicQos(1); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(1000); } catch (Exception e) { } String msg = new String(body, "UTF-8"); System.out.println("消費者獲取消息:" + msg); // 消費者完成 刪除該消息 channel.basicAck(envelope.getDeliveryTag(), false); } }; // 3.監聽隊列 channel.basicConsume(QUEUE_NAME, false, defaultConsumer); } }
package com.mayikt.demo02; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.concurrent.TimeoutException; /** * @ClassName Producer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ public class Producer { private static final String QUEUE_NAME = "mayikt-queue"; public static void main(String[] args) throws IOException, TimeoutException { //1.創建一個新連接 Connection connection = RabbitMQConnection.getConnection(); //2.設置channel Channel channel = connection.createChannel(); //3.發送消息 for (int i = 0; i < 10; i++) { String msg = "每特教育6666:i" + i; channel.basicPublish("", QUEUE_NAME, null, msg.getBytes()); } System.out.println("消息投遞成功"); channel.close(); connection.close(); } }
ProducerFanout
package com.mayikt.demo03; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.concurrent.TimeoutException; public class ProducerFanout { /** * 定義交換機的名稱 */ private static final String EXCHANGE_NAME = "fanout_exchange"; public static void main(String[] args) throws IOException, TimeoutException { // 創建Connection Connection connection = RabbitMQConnection.getConnection(); // 創建Channel Channel channel = connection.createChannel(); // 通道關聯交換機 channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true); //第二個參數type:交換機類型,常見的如fanout、direct、topic String msg = "每特教育6666"; channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes()); channel.close(); connection.close(); } }
package com.mayikt.demo03; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class MailConsumer { /** * 定義郵件隊列 */ private static final String QUEUE_NAME = "fanout_email_queue"; /** * 定義交換機的名稱 */ private static final String EXCHANGE_NAME = "fanout_exchange"; public static void main(String[] args) throws IOException, TimeoutException { System.out.println("郵件消費者..."); // 創建我們的連接 Connection connection = RabbitMQConnection.getConnection(); // 創建我們通道 final Channel channel = connection.createChannel(); // 關聯隊列消費者關聯隊列 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("郵件消費者獲取消息:" + msg); } }; // 開始監聽消息 自動簽收 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
SmsConsumer
package com.mayikt.demo03; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class SmsConsumer { /** * 定義短信隊列 */ private static final String QUEUE_NAME = "fanout_email_sms"; /** * 定義交換機的名稱 */ private static final String EXCHANGE_NAME = "fanout_exchange"; public static void main(String[] args) throws IOException, TimeoutException { System.out.println("短信消費者..."); // 創建我們的連接 Connection connection = RabbitMQConnection.getConnection(); // 創建我們通道 final Channel channel = connection.createChannel(); // 關聯隊列消費者關聯隊列 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("短信消費者獲取消息:" + msg); } }; // 開始監聽消息 自動簽收 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
四、路由模式
1、圖示
註釋:Direct Exchange – 處理路由鍵。需要將一個隊列綁定到交換機上,要求該消息與一個特定的路由鍵完全匹配。這是一個完整的匹配。如果一個隊列綁定到該交換機上要求路由鍵 “dog”,則只有被標記爲“dog”的消息才被轉發,不會轉發dog.puppy,也不會轉發dog.guard,只會轉發dog。
ProducerDirect
package com.mayikt.demo04; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import java.io.IOException; import java.util.concurrent.TimeoutException; public class ProducerDirect { /** * 定義交換機的名稱 */ private static final String EXCHANGE_NAME = "direct_exchange"; public static void main(String[] args) throws IOException, TimeoutException { // 創建Connection Connection connection = RabbitMQConnection.getConnection(); // 創建Channel Channel channel = connection.createChannel(); // 通道關聯交換機 channel.exchangeDeclare(EXCHANGE_NAME, "direct", true); String msg = "每特教育6666"; channel.basicPublish(EXCHANGE_NAME, "email", null, msg.getBytes()); //只發送給郵件這個隊列 channel.close(); connection.close(); } }
MailConsumer
package com.mayikt.demo04; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class MailConsumer { /** * 定義郵件隊列 */ private static final String QUEUE_NAME = "direct_email_queue"; /** * 定義交換機的名稱 */ private static final String EXCHANGE_NAME = "direct_exchange"; public static void main(String[] args) throws IOException, TimeoutException { System.out.println("郵件消費者..."); // 創建我們的連接 Connection connection = RabbitMQConnection.getConnection(); // 創建我們通道 final Channel channel = connection.createChannel(); // 關聯隊列消費者關聯隊列 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "email"); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("郵件消費者獲取消息:" + msg); } }; // 開始監聽消息 自動簽收 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
SmsConsumer
package com.mayikt.demo04; import com.mayikt.rabbitmq.RabbitMQConnection; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class SmsConsumer { /** * 定義短信隊列 */ private static final String QUEUE_NAME = "direct_sms_queue"; /** * 定義交換機的名稱 */ private static final String EXCHANGE_NAME = "direct_exchange"; public static void main(String[] args) throws IOException, TimeoutException { System.out.println("短信消費者..."); // 創建我們的連接 Connection connection = RabbitMQConnection.getConnection(); // 創建我們通道 final Channel channel = connection.createChannel(); // 關聯隊列消費者關聯隊列 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "sms"); DefaultConsumer defaultConsumer = new DefaultConsumer(channel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String msg = new String(body, "UTF-8"); System.out.println("短信消費者獲取消息:" + msg); } }; // 開始監聽消息 自動簽收 channel.basicConsume(QUEUE_NAME, true, defaultConsumer); } }
ProducerTopic
package com.mayikt.demo06;
import com.mayikt.rabbitmq.RabbitMQConnection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ProducerTopic {
/**
* 定義交換機的名稱
*/
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 創建Connection
Connection connection = RabbitMQConnection.getConnection();
// 創建Channel
Channel channel = connection.createChannel();
// 通道關聯交換機
channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
String msg = "每特教育6666";
channel.basicPublish(EXCHANGE_NAME, "mayikt.sms", null, msg.getBytes());
channel.close();
connection.close();
}
}
MailConsumer
package com.mayikt.demo06;
import com.mayikt.rabbitmq.RabbitMQConnection;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class MailConsumer {
/**
* 定義郵件隊列
*/
private static final String QUEUE_NAME = "topic_email_queue";
/**
* 定義交換機的名稱
*/
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("郵件消費者...");
// 創建我們的連接
Connection connection = RabbitMQConnection.getConnection();
// 創建我們通道
final Channel channel = connection.createChannel();
// 關聯隊列消費者關聯隊列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "mayikt.*");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("郵件消費者獲取消息:" + msg);
}
};
// 開始監聽消息 自動簽收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
SmsConsumer
package com.mayikt.demo06;
import com.mayikt.rabbitmq.RabbitMQConnection;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class SmsConsumer {
/**
* 定義短信隊列
*/
private static final String QUEUE_NAME = "topic_sms_queue";
/**
* 定義交換機的名稱
*/
private static final String EXCHANGE_NAME = "topic_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("短信消費者...");
// 創建我們的連接
Connection connection = RabbitMQConnection.getConnection();
// 創建我們通道
final Channel channel = connection.createChannel();
// 關聯隊列消費者關聯隊列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "meite.*");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("短信消費者獲取消息:" + msg);
}
};
// 開始監聽消息 自動簽收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
RPC模式
https://blog.csdn.net/hry2015/article/details/79199294
1. RPC客戶端啓動後,創建一個匿名、獨佔的、回調的隊列 2. RPC客戶端設置消息的2個屬性:replyTo和correlationId,然後將消息發送到隊列rpc_queue 3. RPC服務端在隊列rpc_queue上等待消息。RPC服務端處理完收到消息後,然後將處理結果封裝成消息發送到replyTo指定的隊列上,並且此消息帶上correlationId(此值爲收到消息裏的correlationId) 4. RPC客戶端在隊列replyTo上等待消息,當收到消息後,它會判斷收到消息的correlationId。如果值和自己之前發送的一樣,則這個值就是RPC的處理結果
在rabbitmq情況下:RabbitMQ如何保證消息不丟失
使用消息確認機制+持久技術
A.消費者確認收到消息機制
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
注:第二個參數值爲false代表關閉RabbitMQ的自動應答機制,改爲手動應答。
在處理完消息時,返回應答狀態,true表示爲自動應答模式。
channel.basicAck(envelope.getDeliveryTag(), false);
B.生產者確認投遞消息成功 使用Confirm機制 或者事務消息
Confirm機制 同步或者是異步的形式
2.RabbitMQ默認創建是持久化的
代碼中設置 durable爲true
參數名稱詳解:
durable是否持久化 durable爲持久化、 Transient 不持久化
autoDelete 是否自動刪除,當最後一個消費者斷開連接之後隊列是否自動被刪除,可以通過RabbitMQ Management,查看某個隊列的消費者數量,當consumers = 0時隊列就會自動刪除
- 使用rabbitmq事務消息;
channel.txSelect();
|
相關核心代碼
生產者
public class Producer { channel.confirmSelect();
|
消費者
public class Consumer {
|
SpringBoot整合RabbitMQ
兩個消費者,一個生產者
Maven依賴
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mayikt</groupId> <artifactId>mayikt-sp-rabbitmq</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <modules> <module>mayikt-producer</module> <module>email-consumer</module> <module>sms-consumer</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <!-- springboot-web組件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 添加springboot對amqp的支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <!--fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.49</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project>
生產者
yml
spring: rabbitmq: ####連接地址 host: 127.0.0.1 ####端口號 port: 5672 ####賬號 username: guest ####密碼 password: guest ### virtual-host: /myVirtualHost server: port: 9092
package com.mayikt.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.stereotype.Component; /** * @ClassName RabbitMQConfig * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Component public class RabbitMQConfig { /** * 定義交換機 */ private String EXCHANGE_SPRINGBOOT_NAME = "/mayikt_ex"; /** * 短信隊列 */ private String FANOUT_SMS_QUEUE = "fanout_sms_queue"; /** * 郵件隊列 */ private String FANOUT_EMAIL_QUEUE = "fanout_email_queue"; // 1.注入隊列和交換機注入到spring容器中 // 2.關聯交換機 <bean id="smsQueue" class="";> /** * 郵件和短信隊列注入到spring容器中 * * @return */ @Bean public Queue smsQueue() { return new Queue(FANOUT_SMS_QUEUE); } @Bean public Queue emailQueue() { return new Queue(FANOUT_EMAIL_QUEUE); } @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange(EXCHANGE_SPRINGBOOT_NAME); } /** * 關聯交換機 * 根據參數名稱 ioc獲取 Queue對象 */ @Bean public Binding BindingSmsFanoutExchange(Queue smsQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(smsQueue).to(fanoutExchange); } @Bean public Binding BindingEmailFanoutExchange(Queue emailQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(emailQueue).to(fanoutExchange); } }
package com.mayikt.entity; import lombok.Data; import org.springframework.stereotype.Component; import java.io.Serializable; /** * @ClassName MsgEntity * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Data public class MsgEntity implements Serializable { private String msgId; private String userId; private String phone; private String email; public MsgEntity(String msgId, String userId, String phone, String email) { this.msgId = msgId; this.userId = userId; this.phone = phone; this.email = email; } }
package com.mayikt.service; import com.mayikt.entity.MsgEntity; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; /** * @ClassName ProducerService * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @RestController public class ProducerService { @Autowired private AmqpTemplate amqpTemplate; @RequestMapping("/sendMsg") public void sendMsg() { /** * 參數1 交換機名稱 * 參數2 路由key * 參數3 發送內容 */ MsgEntity msgEntity = new MsgEntity(UUID.randomUUID().toString(), "1234", "181111111", "[email protected]"); amqpTemplate.convertAndSend("/mayikt_ex", "", msgEntity); } }
package com.mayikt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @ClassName AppProducer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @SpringBootApplication public class AppProducer { public static void main(String[] args) { SpringApplication.run(AppProducer.class); } }
spring: rabbitmq: ####連接地址 host: 127.0.0.1 ####端口號 port: 5672 ####賬號 username: guest ####密碼 password: guest ### virtual-host: /myVirtualHost server: port: 8081
package com.mayikt.entity; import lombok.Data; import java.io.Serializable; /** * @ClassName MsgEntity * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Data public class MsgEntity implements Serializable { private String msgId; private String userId; private String phone; private String email; public MsgEntity(String msgId, String userId, String phone, String email) { this.msgId = msgId; this.userId = userId; this.phone = phone; this.email = email; } }
package com.mayikt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @ClassName AppEmailConsumer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @SpringBootApplication public class AppEmailConsumer { public static void main(String[] args) { SpringApplication.run(AppEmailConsumer.class); } }
package com.mayikt; import com.mayikt.entity.MsgEntity; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @ClassName FanoutSmsConsumer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Slf4j @Component @RabbitListener(queues = "fanout_email_queue") public class FanoutEmailConsumer { @RabbitHandler public void process(MsgEntity msgEntity) { log.info("email:msgEntity:" + msgEntity); } }
spring: rabbitmq: ####連接地址 host: 127.0.0.1 ####端口號 port: 5672 ####賬號 username: guest ####密碼 password: guest ### virtual-host: /myVirtualHost server: port: 8082
package com.mayikt.entity; import lombok.Data; import java.io.Serializable; /** * @ClassName MsgEntity * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Data public class MsgEntity implements Serializable { private String msgId; private String userId; private String phone; private String email; public MsgEntity(String msgId, String userId, String phone, String email) { this.msgId = msgId; this.userId = userId; this.phone = phone; this.email = email; } }
package com.mayikt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @ClassName AppEmailConsumer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @SpringBootApplication public class AppSMSConsumer { public static void main(String[] args) { SpringApplication.run(AppSMSConsumer.class); } }
package com.mayikt; import com.mayikt.entity.MsgEntity; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @ClassName FanoutSmsConsumer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Slf4j @Component @RabbitListener(queues = "fanout_sms_queue") public class FanoutEmailConsumer { @RabbitHandler public void process(MsgEntity msgEntity) { log.info("sms:msgEntity:" + msgEntity); } }
需要注意的是springboot可以自動生成交換機和隊列,無需手動配置。
生產者如何獲取消費結果
RabbitMQ實戰解決方案
下面的代碼演示的是生產者生產消息給mq中間件(通過服務的方式),消費者訂閱消費,並插入數據庫,生產者通過接口和id判斷數據庫有沒有相對應的消息來判斷有沒有被消費。
yml
這裏面加入了自定義消費失敗重試策略
spring: rabbitmq: ####連接地址 host: 127.0.0.1 ####端口號 port: 5672 ####賬號 username: guest ####密碼 password: guest ### 地址 virtual-host: /myVirtualHost listener: simple: retry: ####開啓消費者(程序出現異常的情況下會)進行重試 enabled: true ####最大重試次數 max-attempts: 5 ####重試間隔時間 initial-interval: 3000 acknowledge-mode: manual datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver server: port: 9001
package com.mayikt.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.stereotype.Component; /** * @ClassName RabbitMQConfig * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Component public class RabbitMQConfig { /** * 定義交換機 */ private String EXCHANGE_SPRINGBOOT_NAME = "/mayikt_order"; /** * 訂單隊列 */ private String FANOUT_ORDER_QUEUE = "fanout_order_queue"; /** * 配置orderQueue * * @return */ @Bean public Queue orderQueue() { return new Queue(FANOUT_ORDER_QUEUE); } /** * 配置fanoutExchange * * @return */ @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange(EXCHANGE_SPRINGBOOT_NAME); } // 綁定交換機 orderQueue @Bean public Binding bindingOrderFanoutExchange(Queue orderQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(orderQueue).to(fanoutExchange); } }
消費者處理了重複提交的問題,但是這種方法仍有風險,可以靠唯一id解決。
int i = 1 / 0; 可以演示失敗重試。
package com.mayikt.consumer; import com.alibaba.fastjson.JSONObject; import com.mayikt.entity.OrderEntity; import com.mayikt.manager.OrderManager; import com.mayikt.mapper.OrderMapper; import com.rabbitmq.client.Channel; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; /** * @ClassName fanout_sms_queue * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Slf4j @Component @RabbitListener(queues = "fanout_order_queue") public class FanoutOrderConsumer { @Autowired private OrderManager orderManager; @Autowired private OrderMapper orderMapper; @RabbitHandler public void process(OrderEntity orderEntity, Message message, Channel channel) throws IOException { // try { log.info(">>orderEntity:{}<<", orderEntity.toString()); String orderId = orderEntity.getOrderId(); if (StringUtils.isEmpty(orderId)) { return; } OrderEntity dbOrderEntity = orderMapper.getOrder(orderId); if (dbOrderEntity != null) {//重試的過程中,爲了避免業務邏輯重複執行,建議提前全局id提前查詢,如果存在的情況下,就無需再繼續做該流程。 log.info("另外消費者已經處理過該業務邏輯"); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } // int i = 1 / 0; int result = orderManager.addOrder(orderEntity); log.info(">>插入數據庫中數據成功<<"); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); // } catch (Exception e) { // // 記錄該消息日誌形式 存放數據庫db中、後期通過定時任務實現消息補償、人工實現補償 // // //將該消息存放到死信隊列中,單獨寫一個死信消費者實現消費。 // } } }
package com.mayikt.entity; import lombok.Data; import java.io.Serializable; @Data public class OrderEntity implements Serializable { private int id; private String orderName; private String orderId; public OrderEntity(String orderName, String orderId) { this.orderName = orderName; this.orderId = orderId; } public OrderEntity() { } }
package com.mayikt.manager; import com.mayikt.entity.OrderEntity; import com.mayikt.mapper.OrderMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; /** * @ClassName OrderManager * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Component public class OrderManager { @Autowired private OrderMapper orderMapper; @Transactional public int addOrder(OrderEntity orderEntity) { return orderMapper.addOrder(orderEntity); } }
package com.mayikt.mapper; import com.mayikt.entity.OrderEntity; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; public interface OrderMapper { @Insert("insert order_info values (null,#{orderName},#{orderId})") int addOrder(OrderEntity orderEntity); @Select("SELECT * from order_info where orderId=#{orderId} ") OrderEntity getOrder(String orderId); }
package com.mayikt.producer; import com.alibaba.fastjson.JSONObject; import com.mayikt.entity.OrderEntity; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.support.CorrelationData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * @ClassName OrderProducer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Component @Slf4j public class OrderProducer implements RabbitTemplate.ConfirmCallback { @Autowired private RabbitTemplate rabbitTemplate; @Override public void confirm(CorrelationData correlationData, boolean b, String s) { String id = correlationData.getId(); log.info("id:" + id); System.out.println(id); } /** * 使用mq發送消息 * * @param orderName * @param orderId */ public void sendMsg(String orderName, String orderId) { OrderEntity orderEntity = new OrderEntity(orderName, orderId); rabbitTemplate.convertAndSend("/mayikt_order", "", orderEntity, message -> { return message; }); // CorrelationData correlationData = new CorrelationData(); // correlationData.setId(JSONObject.toJSONString(orderEntity)); // rabbitTemplate.convertAndSend("/mayikt_order", "", orderEntity,correlationData); } }
package com.mayikt.service; import com.alibaba.fastjson.JSONObject; import com.mayikt.entity.OrderEntity; import com.mayikt.mapper.OrderMapper; import com.mayikt.producer.OrderProducer; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.support.CorrelationData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderProducer orderProducer; @RequestMapping("/sendOrder") public String sendOrder() { // 生成全局id String orderId = System.currentTimeMillis() + ""; log.info("orderId:{}", orderId); String orderName = "每特教育svip課程報名"; orderProducer.sendMsg(orderName, orderId); return orderId; } /** * 前端主動根據orderId定時查詢 * * @param orderId * @return */ @RequestMapping("/getOrder") public Object getOrder(String orderId) { System.out.println(orderId); OrderEntity order = orderMapper.getOrder(orderId); if (order == null) { return "該訂單沒有被消費或者訂單號錯誤!"; } return order; } }
RabbitMQ死信隊列
死信隊列產生的背景
RabbitMQ死信隊列俗稱,備胎隊列;消息中間件因爲某種原因拒收該消息後,可以轉移到死信隊列中存放,死信隊列也可以有交換機和路由key等。
產生死信隊列的原因
- 消息投遞到MQ中存放 消息已經過期 消費者沒有及時的獲取到我們消息,消息如果存放到mq服務器中過期之後,會轉移到備胎死信隊列存放。
- 隊列達到最大的長度 (隊列容器已經滿了)
- 3. 消費者消費多次消息失敗,就會轉移存放到死信隊列中
代碼整合 參考 mayikt-springboot-rabbitmq|#中order-dead-letter-queue項目
死信隊列的架構原理
死信隊列和普通隊列區別不是很大
普通與死信隊列都有自己獨立的交換機和路由key、隊列和消費者。
區別:
1.生產者投遞消息先投遞到我們普通交換機中,普通交換機在將該消息投到
普通隊列中緩存起來,普通隊列對應有自己獨立普通消費者。
2.如果生產者投遞消息到普通隊列中,普通隊列發現該消息一直沒有被消費者消費
的情況下,在這時候會將該消息轉移到死信(備胎)交換機中,死信(備胎)交換機
對應有自己獨立的 死信(備胎)隊列 對應獨立死信(備胎)消費者。
死信隊列應用場景
1.30分鐘訂單超時設計
- Redis過期key :
- 死信延遲隊列實現:
採用死信隊列,創建一個普通隊列沒有對應的消費者消費消息,在30分鐘過後
就會將該消息轉移到死信備胎消費者實現消費。
備胎死信消費者會根據該訂單號碼查詢是否已經支付過,如果沒有支付的情況下
則會開始回滾庫存操作。
spring: rabbitmq: ####連接地址 host: 127.0.0.1 ####端口號 port: 5672 ####賬號 username: guest ####密碼 password: guest ### 地址 virtual-host: /myVirtualHost server: port: 8080 ###模擬演示死信隊列 mayikt: dlx: exchange: mayikt_dlx_exchange queue: mayikt_order_dlx_queue routingKey: dlx ###備胎交換機 order: exchange: mayikt_order_exchange queue: mayikt_order_queue routingKey: mayikt.order
package com.mayikt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @ClassName AppDeadLetter * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @SpringBootApplication public class AppDeadLetter { public static void main(String[] args) { SpringApplication.run(AppDeadLetter.class); } }
package com.mayikt.producer; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @ClassName OrderProducer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @RestController public class OrderProducer { @Autowired private RabbitTemplate rabbitTemplate; /** * 訂單交換機 */ @Value("${mayikt.order.exchange}") private String orderExchange; /** * 訂單路由key */ @Value("${mayikt.order.routingKey}") private String orderRoutingKey; @RequestMapping("/sendOrder") public String sendOrder() { String msg = "每特教育牛逼"; rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg, message -> { // 設置消息過期時間 10秒過期 message.getMessageProperties().setExpiration("10000"); return message; }); return "success"; } }
package com.mayikt.consumer; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Slf4j @Component public class OrderDlxConsumer { /** * 死信隊列監聽隊列回調的方法 * * @param msg */ @RabbitListener(queues = "mayikt_order_dlx_queue") public void orderConsumer(String msg) { log.info(">死信隊列消費訂單消息:msg{}<<", msg); } }
註釋掉模式消費失敗
//package com.mayikt.consumer; // //import lombok.extern.slf4j.Slf4j; //import org.springframework.amqp.rabbit.annotation.RabbitListener; //import org.springframework.stereotype.Component; // ///** // * 訂單消費者 // */ //@Component //@Slf4j //public class OrderConsumer { // // /** // * 監聽隊列回調的方法 // * // * @param msg // */ // @RabbitListener(queues = "mayikt_order_queue") // public void orderConsumer(String msg) { // log.info(">>正常訂單消費者消息MSG:{}<<", msg); // } //}
package com.mayikt.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Component public class DeadLetterMQConfig { /** * 訂單交換機 */ @Value("${mayikt.order.exchange}") private String orderExchange; /** * 訂單隊列 */ @Value("${mayikt.order.queue}") private String orderQueue; /** * 訂單路由key */ @Value("${mayikt.order.routingKey}") private String orderRoutingKey; /** * 死信交換機 */ @Value("${mayikt.dlx.exchange}") private String dlxExchange; /** * 死信隊列 */ @Value("${mayikt.dlx.queue}") private String dlxQueue; /** * 死信路由 */ @Value("${mayikt.dlx.routingKey}") private String dlxRoutingKey; /** * 聲明死信交換機 * * @return DirectExchange */ @Bean public DirectExchange dlxExchange() { return new DirectExchange(dlxExchange); } /** * 聲明死信隊列 * * @return Queue */ @Bean public Queue dlxQueue() { return new Queue(dlxQueue); } /** * 聲明訂單業務交換機 * * @return DirectExchange */ @Bean public DirectExchange orderExchange() { return new DirectExchange(orderExchange); } /** * 聲明訂單隊列 * * @return Queue */ @Bean public Queue orderQueue() { // 訂單隊列綁定我們的死信交換機 Map<String, Object> arguments = new HashMap<>(2); arguments.put("x-dead-letter-exchange", dlxExchange); arguments.put("x-dead-letter-routing-key", dlxRoutingKey); return new Queue(orderQueue, true, false, false, arguments); } /** * 綁定死信隊列到死信交換機 * * @return Binding */ @Bean public Binding binding() { return BindingBuilder.bind(dlxQueue()) .to(dlxExchange()) .with(dlxRoutingKey); } /** * 綁定訂單隊列到訂單交換機 * * @return Binding */ @Bean public Binding orderBinding() { return BindingBuilder.bind(orderQueue()) .to(orderExchange()) .with(orderRoutingKey); } }
下面的代碼是使用springboot演示訂閱模式實現異步發送郵件和短信,當然生產者和消費者進行拆開成微服務。
微服務形式
spring: rabbitmq: ####連接地址 host: 127.0.0.1 ####端口號 port: 5672 ####賬號 username: guest ####密碼 password: guest ### 地址 virtual-host: /myVirtualHost
package com.mayikt; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @ClassName App * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class); } }
package com.mayikt.producer; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @ClassName FanoutProducer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @RestController public class FanoutProducer { @Autowired private AmqpTemplate amqpTemplate; /** * 發送消息 * * @return */ @RequestMapping("/sendMsg") public String sendMsg(String msg) { /** * 1.交換機名稱 * 2.路由key名稱 * 3.發送內容 */ amqpTemplate.convertAndSend("/mayikt_ex", "", msg); return "success"; } }
package com.mayikt.consumer; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @ClassName fanout_sms_queue * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Slf4j @Component @RabbitListener(queues = "fanout_sms_queue") public class FanoutSmsConsumer { @RabbitHandler public void process(String msg) { log.info(">>短信消費者消息msg:{}<<", msg); } }
package com.mayikt.consumer; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; /** * @ClassName FanoutEmailConsumer * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Slf4j @Component @RabbitListener(queues = "fanout_email_queue") public class FanoutEmailConsumer { @RabbitHandler public void process(String msg) { log.info(">>郵件消費者消息msg:{}<<", msg); } }
package com.mayikt.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.stereotype.Component; /** * @ClassName RabbitMQConfig * @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com * @Version V1.0 **/ @Component public class RabbitMQConfig { /** * 定義交換機 */ private String EXCHANGE_SPRINGBOOT_NAME = "/mayikt_ex"; /** * 短信隊列 */ private String FANOUT_SMS_QUEUE = "fanout_sms_queue"; /** * 郵件隊列 */ private String FANOUT_EMAIL_QUEUE = "fanout_email_queue"; /** * 配置smsQueue * * @return */ @Bean public Queue smsQueue() { return new Queue(FANOUT_SMS_QUEUE); } /** * 配置emailQueue * * @return */ @Bean public Queue emailQueue() { return new Queue(FANOUT_EMAIL_QUEUE); } /** * 配置fanoutExchange * * @return */ @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange(EXCHANGE_SPRINGBOOT_NAME); } // 綁定交換機 sms @Bean public Binding bindingSmsFanoutExchange(Queue smsQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(smsQueue).to(fanoutExchange); } // 綁定交換機 email @Bean public Binding bindingEmailFanoutExchange(Queue emailQueue, FanoutExchange fanoutExchange) { return BindingBuilder.bind(emailQueue).to(fanoutExchange); } }
RabbitMQ消息冪等問題
RabbitMQ消息自動重試機制
- 當我們消費者處理執行我們業務代碼的時候,如果拋出異常的情況下
在這時候mq會自動觸發重試機制,默認的情況下rabbitmq是無限次數的重試。
需要人爲指定重試次數限制問題
- 在什麼情況下消費者需要實現重試策略?
A.消費者獲取消息後,調用第三方接口,但是調用第三方接口失敗呢?是否需要重試?
該情況下需要實現重試策略,網絡延遲只是暫時調用不通,重試多次有可能會調用通。
B.消費者獲取消息後,因爲代碼問題拋出數據異常,是否需要重試?
該情況下是不需要實現重試策略,就算重試多次,最終還是失敗的。可以將日誌存放起來,後期通過定時任務或者人工補償形式。如果是重試多次還是失敗消息,需要重新發布消費者版本實現消費。
可以使用死信隊列
Mq在重試的過程中,有可能會引發消費者重複消費的問題。
Mq消費者需要解決 冪等性問題
冪等性 保證數據唯一
方式1:
生產者在投遞消息的時候,生成一個全局唯一id,放在我們消息中。
Msg id=123456
Msg id=123456
Msg id=123456
消費者獲取到我們該消息,可以根據該全局唯一id實現去重複。
全局唯一id 根據業務來定的 訂單號碼作爲全局的id
實際上還是需要再db層面解決數據防重複。
業務邏輯是在做insert操作 使用唯一主鍵約束
業務邏輯是在做update操作 使用樂觀鎖
- 當消費者業務邏輯代碼中,拋出異常自動實現重試 (默認是無數次重試)
- 應該對RabbitMQ重試次數實現限制,比如最多重試5次,每次間隔3s;重試多次還是失敗的情況下,存放到死信隊列或者存放到數據庫表中記錄後期人工補償
- 消費者獲取消息後,調用第三方接口,但是調用第三方接口失敗呢?是否需要重試 ?
- 消費者獲取消息後,應該代碼問題拋出數據異常,是否需要重試?
如何合理選擇消息重試
總結:如果消費者處理消息時,因爲代碼原因拋出異常是需要從新發布版本才能解決的,那麼就不需要重試,重試也解決不了該問題的。存放到死信隊列或者是數據庫表記錄、後期人工實現補償。
詳細的策略解決冪等性和保證冪等性的方法
https://www.cnblogs.com/javalyy/p/8882144.html