html
git
github
商品的原始數據保存在數據庫中,增刪改查都在數據庫中完成。數據庫
搜索服務數據來源是索引庫,若是數據庫商品發生變化,索引庫數據不能及時更新。windows
商品詳情作了頁面靜態化,靜態頁面數據也不會隨着數據庫商品發生變化。瀏覽器
若是咱們在後臺修改了商品的價格,搜索頁面和商品詳情頁顯示的依然是舊的價格,這樣顯然不對。該如何解決?併發
這裏有兩種解決方案:異步
方案1:每當後臺對商品作增刪改操做,同時要修改索引庫數據及靜態頁面分佈式
方案2:搜索服務和商品頁面服務對外提供操做接口,後臺在商品增刪改後,調用接口ide
以上兩種方式都有同一個嚴重問題:就是代碼耦合,後臺服務中須要嵌入搜索和商品頁面服務,違背了微服務的獨立
原則。
因此,咱們會經過另一種方式來解決這個問題:消息隊列
(2)消息隊列
<1>
結合前面所說的問題:
商品服務對商品增刪改之後,無需去操做索引庫或靜態頁面,只是發送一條消息,也不關心消息被誰接收。
搜索服務和靜態頁面服務接收消息,分別去處理索引庫和靜態頁面。
JMS是定義了統一的接口,來對消息操做進行統一;AMQP是經過規定協議來統一數據交互的格式
JMS限定了必須使用Java語言;AMQP只是協議,不規定實現方式,所以是跨語言的。
RabbitMQ:基於AMQP協議,erlang語言開發,穩定性好
RocketMQ:基於JMS,阿里巴巴產品,目前交由Apache基金會
官方教程:
(3)下載與安裝(windows環境)
<1>下載安裝
RabbitMQ由Erlang語言開發,Erlang語言用於併發及分佈式系統的開發,在電信領域應用普遍,OTP(Open Telecom Platform)做爲Erlang語言的一部分,包含了不少基於Erlang開發的中間件及工具庫,安裝RabbitMQ須要安裝Erlang/OTP,並保持版本匹配,以下圖:
RabbitMQ的下載地址:http://www.rabbitmq.com/download.html
本項目使用Erlang/OTP 20.3版本和RabbitMQ3.7.3版本。
1)下載erlang
地址以下:
http://erlang.org/download/otp_win64_20.3.exe,以管理員方式運行此文件,安裝。
erlang安裝完成須要配置erlang環境變量: ERLANG_HOME=D:\Program Files\erl9.3 在path中添加%ERLANG_HOME%\bin;
此電腦-->鼠標右鍵「屬性」-->高級系統設置-->環境變量-->「新建」系統環境變量
變量名:ERLANG_HOME
變量值就是剛纔erlang的安裝地址,點擊肯定。
而後雙擊系統變量path
點擊「新建」,將%ERLANG_HOME%\bin加入到path中。
最後windows鍵+R鍵,輸入cmd,再輸入erl,看到版本號就說明erlang安裝成功了。
2)安裝RabbitMQ
https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.3,以管理員方式運行此文件,安裝。
<2>啓動
安裝成功後會自動建立RabbitMQ服務而且啓動。
1)從開始菜單啓動RabbitMQ
完成在開始菜單找到RabbitMQ的菜單:
RabbitMQ Service-install :安裝服務
RabbitMQ Service-remove 刪除服務
RabbitMQ Service-start 啓動
RabbitMQ Service-stop 中止
2)若是沒有開始菜單則進入安裝目錄下sbin目錄手動啓動:
1)安裝並運行服務
rabbitmq-service.bat install 安裝服務 rabbitmq-service.bat stop 中止服務 rabbitmq-service.bat start 啓動服務
2)安裝管理插件
安裝rabbitMQ的管理插件,方便在瀏覽器端管理RabbitMQ
管理員身份運行 rabbitmq-plugins.bat enable rabbitmq_management
安裝完插件後,須要重啓rabbitmq,此電腦-->鼠標右鍵「管理」-->服務與應用程序-->服務
打開sbin目錄,雙擊rabbitmq-server.bat
等幾秒鐘看到這個界面後,訪問http://localhost:15672
初始帳號和密碼:guest/guest
<3>注意事項
2.
導入成功後,模塊的結構:
package cn.itcast.rabbitmq.simple; import cn.itcast.rabbitmq.util.ConnectionUtil; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.MessageProperties; /** * 生產者 */ public class Send { private final static String QUEUE_NAME = "simple_queue"; public static void main(String[] argv) throws Exception { // 獲取到鏈接 Connection connection = ConnectionUtil.getConnection(); // 從鏈接中建立通道,使用通道才能完成消息相關的操做 Channel channel = connection.createChannel(); // 聲明(建立)隊列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 消息內容 String message = "Hello World!"; // 向指定的隊列中發送消息 channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); System.out.println(" [x] Sent '" + message + "'"); //關閉通道和鏈接 channel.close(); connection.close(); } }
(1)
C(consumer):消費者,消費和接收有相似的意思,消費者是一個主要用來等待接收消息的用戶應用程序
隊列(紅色區域):rabbitmq內部相似於郵箱的一個概念。雖然消息流經rabbitmq和你的應用程序,可是它們只能存儲在隊列中。隊列只受主機的內存和磁盤限制,實質上是一個大的消息緩衝區。許多生產者能夠發送消息到一個隊列,許多消費者能夠嘗試從一個隊列接收數據。
總之:
public class Send { private final static String QUEUE_NAME = "simple_queue"; public static void main(String[] argv) throws Exception { // 獲取到鏈接以及mq通道 Connection connection = ConnectionUtil.getConnection(); // 從鏈接中建立通道,這是完成大部分API的地方。 Channel channel = connection.createChannel(); // 聲明(建立)隊列,必須聲明隊列纔可以發送消息,咱們能夠把消息發送到隊列中。 // 聲明一個隊列是冪等的 - 只有當它不存在時纔會被建立 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 消息內容 String message = "Hello World!"; channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); System.out.println(" [x] Sent '" + message + "'"); //關閉通道和鏈接 channel.close(); connection.close(); } }
控制檯:
<2>
點擊隊列名稱,進入詳情頁,能夠查看消息:
在控制檯查看消息並不會將消息消費,因此消息還在。
<3>
public class Recv { private final static String QUEUE_NAME = "simple_queue"; public static void main(String[] argv) throws Exception { // 獲取到鏈接 Connection connection = ConnectionUtil.getConnection(); // 建立通道 Channel channel = connection.createChannel(); // 聲明隊列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 定義隊列的消費者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 獲取消息,而且處理,這個方法相似事件監聽,若是有消息的時候,會被自動調用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息體 String msg = new String(body); System.out.println(" [x] received : " + msg + "!"); } }; // 監聽隊列,第二個參數:是否自動進行消息確認。 channel.basicConsume(QUEUE_NAME, true, consumer); } }
控制檯:
這個時候,隊列中的消息就沒了:
咱們發現,消費者已經獲取了消息,可是程序沒有中止,一直在監聽隊列中是否有新的消息。一旦有新的消息進入隊列,就會當即打印.
<4>
那麼問題來了:RabbitMQ怎麼知道消息被接收了呢?
若是消費者領取消息後,還沒執行操做就掛掉了呢?或者拋出了異常?消息消費失敗,可是RabbitMQ無從得知,這樣消息就丟失了!
所以,RabbitMQ有一個ACK機制。當消費者獲取消息後,會向RabbitMQ發送回執ACK,告知消息已經被接收。不過這種回執ACK分兩種狀況:
自動ACK:消息一旦被接收,消費者自動發送ACK
手動ACK:消息接收後,不會發送ACK,須要手動調用
你們以爲哪一種更好呢?
這須要看消息的重要性:
若是消息不過重要,丟失也沒有影響,那麼自動ACK會比較方便
若是消息很是重要,不容丟失。那麼最好在消費完成後手動ACK,不然接收消息後就自動ACK,RabbitMQ就會把消息從隊列中刪除。若是此時消費者宕機,那麼消息就丟失了。
public class Recv2 { private final static String QUEUE_NAME = "simple_queue"; public static void main(String[] argv) throws Exception { // 獲取到鏈接 Connection connection = ConnectionUtil.getConnection(); // 建立通道 final Channel channel = connection.createChannel(); // 聲明隊列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 定義隊列的消費者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 獲取消息,而且處理,這個方法相似事件監聽,若是有消息的時候,會被自動調用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息體 String msg = new String(body); System.out.println(" [x] received : " + msg + "!"); // 手動進行ACK channel.basicAck(envelope.getDeliveryTag(), false); } }; // 監聽隊列,第二個參數false,手動進行ACK channel.basicConsume(QUEUE_NAME, false, consumer); } }
注意到最後一行代碼:
channel.basicConsume(QUEUE_NAME, false, consumer);
若是第二個參數爲true,則會自動進行ACK;若是爲false,則須要手動ACK。
這個概念在Web應用程序中特別有用,由於在短的HTTP請求窗口中沒法處理複雜的任務。
P:生產者:任務的發佈者
C1:消費者,領取任務而且完成任務,假設完成速度較快
C2:消費者2:領取任務並完成任務,假設完成速度慢
<1>生產者
生產者與案例1中的幾乎同樣:
public class Send { private final static String QUEUE_NAME = "test_work_queue"; public static void main(String[] argv) throws Exception { // 獲取到鏈接 Connection connection = ConnectionUtil.getConnection(); // 獲取通道 Channel channel = connection.createChannel(); // 聲明隊列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 循環發佈任務 for (int i = 0; i < 50; i++) { // 消息內容 String message = "task .. " + i; channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); System.out.println(" [x] Sent '" + message + "'"); Thread.sleep(i * 2); } // 關閉通道和鏈接 channel.close(); connection.close(); } }
<2>
一、1個生產者,多個消費者
三、生產者沒有將消息直接發送到隊列,而是發送到了交換機
四、每一個隊列都要綁定到交換機
五、生產者發送的消息,通過交換機到達隊列,實現一個消息被多個消費者獲取的目的
X(Exchanges):交換機一方面:接收生產者發送的消息。另外一方面:知道如何處理消息,例如遞交給某個特別隊列、遞交給全部隊列、或是將消息丟棄。到底如何操做,取決於Exchange的類型。
Fanout:廣播,將消息交給全部綁定到交換機的隊列
Direct:定向,把消息交給符合指定routing key 的隊列
Topic:通配符,把消息交給符合routing pattern(路由模式) 的隊列
1) 能夠有多個消費者
2) 每一個消費者有本身的queue(隊列)
3) 每一個隊列都要綁定到Exchange(交換機)
4) 生產者發送的消息,只能發送到交換機,交換機來決定要發給哪一個隊列,生產者沒法決定。
5) 交換機把消息發送給綁定過的全部隊列
6) 隊列的消費者都能拿到消息。實現一條消息被多個消費者消費
<1>生產者
1) 聲明Exchange,再也不聲明Queue
public class Send { private final static String EXCHANGE_NAME = "fanout_exchange_test"; public static void main(String[] argv) throws Exception { // 獲取到鏈接 Connection connection = ConnectionUtil.getConnection(); // 獲取通道 Channel channel = connection.createChannel(); // 聲明exchange,指定類型爲fanout channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); // 消息內容 String message = "Hello everyone"; // 發佈消息到Exchange channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes()); System.out.println(" [生產者] Sent '" + message + "'"); channel.close(); connection.close(); } }
<2>
public class Recv { private final static String QUEUE_NAME = "fanout_exchange_queue_1"; private final static String EXCHANGE_NAME = "fanout_exchange_test"; public static void main(String[] argv) throws Exception { // 獲取到鏈接 Connection connection = ConnectionUtil.getConnection(); // 獲取通道 Channel channel = connection.createChannel(); // 聲明隊列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 綁定隊列到交換機 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); // 定義隊列的消費者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 獲取消息,而且處理,這個方法相似事件監聽,若是有消息的時候,會被自動調用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息體 String msg = new String(body); System.out.println(" [消費者1] received : " + msg + "!"); } }; // 監聽隊列,自動返回完成 channel.basicConsume(QUEUE_NAME, true, consumer); } }
public class Recv2 { private final static String QUEUE_NAME = "fanout_exchange_queue_2"; private final static String EXCHANGE_NAME = "fanout_exchange_test"; public static void main(String[] argv) throws Exception { // 獲取到鏈接 Connection connection = ConnectionUtil.getConnection(); // 獲取通道 Channel channel = connection.createChannel(); // 聲明隊列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 綁定隊列到交換機 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ""); // 定義隊列的消費者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 獲取消息,而且處理,這個方法相似事件監聽,若是有消息的時候,會被自動調用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息體 String msg = new String(body); System.out.println(" [消費者2] received : " + msg + "!"); } }; // 監聽隊列,手動返回完成 channel.basicConsume(QUEUE_NAME, true, consumer); } }
<4>測試
咱們運行兩個消費者,而後發送1條消息:
(5)
在訂閱模式中,生產者發佈消息,全部消費者均可以獲取全部消息。
在路由模式中,咱們將添加一個功能 - 咱們將只能訂閱一部分消息。 例如,咱們只能將重要的錯誤消息引導到日誌文件(以節省磁盤空間),同時仍然可以在控制檯上打印全部日誌消息。
可是,在某些場景下,咱們但願不一樣的消息被不一樣的隊列消費。這時就要用到Direct類型的Exchange。
在Direct模型下,隊列與交換機的綁定,不能是任意綁定了,而是要指定一個RoutingKey(路由key)
X:Exchange(交換機),接收生產者的消息,而後把消息遞交給 與routing key徹底匹配的隊列
C1:消費者,其所在隊列指定了須要routing key 爲 error 的消息
public class Send { private final static String EXCHANGE_NAME = "direct_exchange_test"; public static void main(String[] argv) throws Exception { // 獲取到鏈接 Connection connection = ConnectionUtil.getConnection(); // 獲取通道 Channel channel = connection.createChannel(); // 聲明exchange,指定類型爲direct channel.exchangeDeclare(EXCHANGE_NAME, "direct"); // 消息內容 String message = "商品新增了, id = 1001"; // 發送消息,而且指定routing key 爲:insert ,表明新增商品 channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes()); System.out.println(" [商品服務:] Sent '" + message + "'"); channel.close(); connection.close(); } }
<2>消費者1
咱們此處假設消費者1只接收兩種類型的消息:更新商品和刪除商品。
public class Recv { private final static String QUEUE_NAME = "direct_exchange_queue_1"; private final static String EXCHANGE_NAME = "direct_exchange_test"; public static void main(String[] argv) throws Exception { // 獲取到鏈接 Connection connection = ConnectionUtil.getConnection(); // 獲取通道 Channel channel = connection.createChannel(); // 聲明隊列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 綁定隊列到交換機,同時指定須要訂閱的routing key。假設此處須要update和delete消息 channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update"); channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete"); // 定義隊列的消費者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 獲取消息,而且處理,這個方法相似事件監聽,若是有消息的時候,會被自動調用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息體 String msg = new String(body); System.out.println(" [消費者1] received : " + msg + "!"); } }; // 監聽隊列,自動ACK channel.basicConsume(QUEUE_NAME, true, consumer); } }
<2>消費者2
咱們此處假設消費者2接收全部類型的消息:新增商品,更新商品和刪除商品
public class Recv2 { private final static String QUEUE_NAME = "direct_exchange_queue_2"; private final static String EXCHANGE_NAME = "direct_exchange_test"; public static void main(String[] argv) throws Exception { // 獲取到鏈接 Connection connection = ConnectionUtil.getConnection(); // 獲取通道 Channel channel = connection.createChannel(); // 聲明隊列 channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 綁定隊列到交換機,同時指定須要訂閱的routing key。訂閱 insert、update、delete channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert"); channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update"); channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete"); // 定義隊列的消費者 DefaultConsumer consumer = new DefaultConsumer(channel) { // 獲取消息,而且處理,這個方法相似事件監聽,若是有消息的時候,會被自動調用 @Override public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) throws IOException { // body 即消息體 String msg = new String(body); System.out.println(" [消費者2] received : " + msg + "!"); } }; // 監聽隊列,自動ACK channel.basicConsume(QUEUE_NAME, true, consumer); } }
<4>測試
咱們分別發送增、刪、改的RoutingKey,發現結果:
(6)
Routingkey
通常都是有一個或多個單詞組成,多個單詞之間以」.」分割,例如: item.insert
`#`:匹配一個或多個詞
`*`:匹配很少很多剛好1個詞
舉例:
`audit.#`:可以匹配`audit.irs.corporate` 或者 `audit.irs`
`audit.*`:只能匹配`audit.irs`
咱們建立了三個綁定:Q1綁定了綁定鍵「* .orange.」,Q2綁定了「.*.rabbit」和「lazy.#」。
Q1匹配全部的橙色動物。
Q2匹配關於兔子以及懶惰動物的消息。
(7)
1) 消費者的ACK機制。能夠防止消費者丟失消息。
2) 可是,若是在消費者消費以前,MQ就宕機了,消息就沒了。
是能夠將消息進行持久化呢?
<2>
<3>