消息中間件
MQ概念Message Queue
消息隊列,通常在分佈式集羣中充當消息中間件,負責在多個工程和應用之間傳遞消息
MQ的產品
RabbitMq,ActiveMq,Kafka,RocketMq,Redis(消息訂閱和發佈-MQ:小型)
爲什麼需要MQ?
在原來的項目中,我們使用過HTTPClient進行系統間的通信;但是使用HTTPClient和WebService都是同步請求,被調用方沒結束,那麼調用方就會處於一個持續堵塞的狀態,不能進行後續的工作,使用MQ作爲消息中間件之後,就可以消除這種依賴,起到異步解耦的作用,調用方只需要將消息放入MQ中,消費方什麼時候調用,調用多長時間都和調用方無關了;
什麼是RabbitMQ?
RabbitMQ是一種實現了MQ設計理念的產品。像ActiveMQ(JMS),Kafka也是類似的產品。
RabbitMQ是一個基於ErLang語言和AMQP(Advanced Message
Queuing Protocol)傳輸協議開發的高併發的消息隊列服務程序
RabbitMq的安裝
1、創建目錄
創建目錄來存放rabbitmq的相關文件
cd /usr/local/
mkdir rabbitmq
cd rabbitmq
2、安裝erlang
上傳erlang 安裝包,通過命令安裝
rpm -ivh erlang-20.1.7-1.el6.x86_64.rpm
3、安裝rabbitMQ,rabbitmq-server
上傳我們準備好安裝包 rpm -ivh
4、啓動rabbitMQ服務
啓動停止命令:
service rabbitmq-server start
service rabbitmq-server stop
service rabbitmq-server restart
5、拷貝配置文件,並設置用戶信息
cp /usr/share/doc/rabbitmq-server-3.4.1/rabbitmq.config.example /etc/rabbitmq/
cd /etc/rabbitmq
mv rabbitmq.config.example rabbitmq.config
vim/etc/rabbitmq/rabbitmq.config
將{loopback_users, []}前面的註解“%%”去掉
6、開啓web界面管理工具,並重啓rabbitMQ
在當前目錄執行即可
rabbitmq-plugins enable rabbitmq_management
7、修改防火牆配置文件:開放15672 和 5672端口
15672:網頁訪問mq管理系統需要的端口
5672:Java程序連接mq時用到的端口
8、通過瀏覽器訪問管理界面
http://ip:15672
用戶名密碼爲guest
#上傳erlang 安裝包,通過命令安裝
rpm -ivh erlang-20.1.7-1.el6.x86_64.rpm
RabbitMq的工作模型
1.HelloWorld模型
P->消息隊列->C
2.工作模型
P->消息隊列->C1,C2…
多個消費者是輪詢消費
3.發佈訂閱模式(重點)
X(路由/交換機)::起到一個消息分發的作用(默認:type=fanout)
->消息隊列1->C1
P->X-> {
->消息隊列2->C2
...
是開發中最基礎的模型
4.Routing
X:路由器的類型不同,type=direct,有路由鍵的概念,發佈的消息會根據某些路由的路由鍵有選擇性的發佈到指定的消息隊列
-路由鍵->消息隊列1->C1
P->X->{
-路由鍵->消息隊列2->C2
...
5.Topic
X:路由器的類型不同,type=topic支持通配符*/#
-路由鍵->消息隊列1->C1
P->X->{
-路由鍵->消息隊列2->C2
...
6.RPC(瞭解)
RabbitMq的概念
Channel:管道,在RabbitMq中,通過連接可以獲得管道,管道對象是操作一切的核心對象(隊列,路由,綁定,消費,發送…)
Queue:隊列,存放消息的對象,Queue可以進行消息的擠壓
Exchange:交換機,消息分發的組件,交換機沒有存儲消息的能力
Channel中的常用方法
1.queueDeclare:聲明一個隊列(重要)
參數一:String queue,隊列名稱
參數二:Boolean durable,是否持久化
非持久化的隊列在rabbitmq重啓後會丟失,消息肯定也丟失
參數三:Boolean exclusive,是否爲排他隊列
排他隊列只對創建這個隊列的連接可見,一旦連接斷開,排他隊列自動刪除(不管是否設置持久化),注意:排他隊列是對當前連接可見,一個連接創建的多個隊列對其中的排他隊列是可見的;
參數四:Boolean autoDelete,是否自動刪除
一旦有消費者綁定到隊列上,如果消費者全部解綁,隊列會自動刪除
參數五:Map<Object,String> arguments,一些額外的屬性配置
2.exchangeDeclare:聲明一個路由(交換機)
常用參數
參數一:String exchange,路由名稱
參數二:String type,路由的類型(fanout|direct|topic|headers)
其他不常用參數:
參數三:Boolean durable,是否持久化
非持久化的路由在rabbitmq重啓後會丟失
參數四:Boolean autoDelete,是否自動刪除
當一個路由如果綁定了一個或者多個隊列或者路由,如果後面又將所有的綁定全部解除,則該路由就會自動刪除
參數五:Boolean internel,是否爲內置路由
一個內置路由,提供者無法直接向內置路由發送消息,內置路由只能接受其他路由發送的消息
參數六:Map<Object,String> arguments,一些額外的屬性配置
3.queueBind:隊列綁定到交換機
參數一:String queue,需要綁定的隊列名稱
參數二:String exechange,綁定到的交換機名稱
參數三:String routingKey,路由鍵(fanout交換機不支持路由鍵)
參數四:Map<Object,String> arguments,一些額外的屬性配置
RabbitMq進階
在rabbitmq可以給消息和隊列設置過期時間
#####1.給消息設置過期時間(重要)
a)通過隊列的屬性給隊列中的消息設置過期時間(某一隊列中所有消息)
Map<String,Object> map = new HashMap<>();
//該隊列中的消息發佈後五秒後會過期
map.put("x-message-ttl",5000);
queueDeclare("queueName1",false,false,false,map);
b)通過消息本身的屬性設置過期時間(某一條消息)
AMQP.BasicProperties prop = new AMQP.BasicProperties()
AMQP.BasicProperties.Builder builder = prop.bulider();
//設置過期時間
builder.expiration("5000");
chanle.publishProvider("queueName1",builder.bulid(),"hello ttl".getBytes("utf-8"));
注意:
1.通過隊列屬性設置消息的過期時間,對隊列中所有消息有效
2.通過消息本身的屬性設置過期時間只對該消息本身有效
3.如果通過隊列的屬性設置消息的過期時間,則過期的消息會立刻被隊列移除,因爲這個時候過期的消息一定在隊頭,因此rabbitmq只需要定期掃描隊頭就可以了
4.如果通過消息本身的屬性設置過期時間,當消息過期時,恰好在隊頭的話,就會立刻移除,如果在隊中的話,不會被立刻移除,當它在隊頭的時候纔會移除
注:RabbitMq出於性能考慮肯定不會掃描全部消息判斷全部過期時間
5.當給隊列的屬性設置過期時間時,如果x-message-ttl設置爲0,表示一個消息如果投遞到隊列中,如果該隊列有消費者綁定,則會立刻消費該消息,如果沒有消費者綁定,則消息會立刻丟失
2.給隊列設置過期時間(意義不大)
Map<String,Object> map = new HashMap<>();
//設置隊列的過期時間
map.put("x-expires",5000);
queueDeclare("queueName1",false,false,false,map);
當給一個隊列設置過期時間後,如果在有效期內,沒有消費者綁定該隊列,同時也沒有提供將消息發送到該隊列,則該隊列自動刪除;
如果該隊列中有積壓的消息,則隊列到期後也不會被刪除;
隊列的類型
1.死信隊列:專用於接受死信消息
概念:
當某個消息變成了死信消息時.會發送給綁定在這個隊列上的死信路由,如果還有一個隊列綁定在死信路由上,則這個隊列是死信隊列
其實隊列本身並沒有分類,而是出於業務邏輯我們對於不同隊列的作用不同進行了不同的稱呼
死信隊列可以被當做處理一些本來會被丟失的消息,有點類似於一個垃圾回收站,但是在某些應用場景下又不全是,比如訂單回庫的時候這些信息又是必不可少的
什麼時候一個消息會變成死信消息?
1.消息過期,同時該消息是隊頭
2.隊列已滿,添加消息時,新消息並不會被拒絕,而是隊頭的消息就變成了死信消息出隊
Map<String,Object> map = new HashMap<>();
//設置隊列最大有多少個消息
map.put("x-max-length",2000);
//設置隊列最大字節數
map.put("x-max-length-bytes",1024 * 1024);
channel.queueDeclare("queueName1",false,false,false,map);
3.消息被消費者拒絕,同時requeue設置爲false
簡單的死信路由
//1.創建一個死信路由
channel.exchangeDeclare("dx_exchange","fanout");
//2.創建一個死信隊列
channel.queueDeclare("dx_queue",false,false,false,map);
//3.將死信隊列綁定到死信路由
channel.queueBind("dx_queue","dx_exchange","");
//4.將死信路由設置給普通隊列
Map<String,Object> map = new HashMap<>();
map.put("x-dead-letter-exchange","dx_exchange");
//普通隊列channel.queueDeclare("queue",false,false,false,map);
//普通路由
channel.exchangeDeclare("exchange","fanout");
//普通隊列和普通路由綁定
channel.queueBind("queue","exchange","");
2.延遲隊列
概念:
過了一個規定時間之後,消費者才能消費隊列中的消息
rabbitmq本身沒有提供延遲隊列,我們通過死信隊列+ttl失效時間實現延遲隊列
可以把一個設置了失效時間的普通隊列和其死信路由+死信隊列看做一個整體,消費者消費這個死信隊列實現了定時消費死信消息
綁定死信路由的普通隊列的必須沒有消費者,消費者在死信隊列消費消息
延遲消息運用場景:
下單:通常用戶下單後一定的付款時間,當付款時間到了之後,如果已經付款直接關閉訂單,如果沒有付款,訂單關閉,交易失敗,商品庫存加回數據庫;
消息持久化
消息持久化是消息安全性的重要保證
rabbitmq分爲三部分:
1.路由持久化druable=true
2.隊列持久化druable=true,隊列持久化不意味着消息持久化
3.消息持久化:
方式一:
chanle.publishProvider("queueName1",MessageProperties.PERSIST_TEXT_PLAIN,"hello ttl".getBytes("utf-8"));
方式二:
AMQP.BasicProperties prop = new AMQP.BasicProperties()
AMQP.BasicProperties.Builder builder = prop.bulider().deliverMode(2).build();
chanle.publishProvider("queueName1",builder.bulid(),"hello".getBytes("utf-8"));
注意:
1.如果設置了消息持久化,沒有設置隊列持久化,是沒有任何意義的
2.持久化消息,rabbit會寫入硬盤,操作硬盤比操作內存會慢很多,用持久化消息會消耗rabbitmq的性能,因此如果對消息的可靠性沒絕對的要求,最好還是不要設置持久化消息,用以提高服務器整體的吞吐量
思考:
如果一個rabbitmq的服務設置了路由持久化,隊列持久化和消息持久化,能否保證數據的絕對可靠?
不能保證,有兩個原因:
1.提供者提供消息的時候
提供者提供消息後可能rabbitmq來不及進行持久化就發生了宕機,此時提供者認爲消息已經發送成功,所以消息丟失了
2.消費者消費消息的時候
當消費者來不及消費時就發生了宕機,仍然沒有消費消息,但是rabbitmq磁盤中已經沒有了該消息
綜上,RabbitMq就有消息確認機制保證數據的強一致性
消息確認機制
1.生產者消息確認機制
概念:
當生產者將消息發送出去後,如何確保消息正常到MQ服務器?默認情況下,發送消息的操作是不會返回任何消息給生產者,確保消息的正確送達。如果消息在發送到MQ服務器途中就丟失了,則持久化也保證不了消息的安全性。爲了解決這個問題,Rabbitmq引入了生產者確認機制
生產者消息確認機制的實現兩種方法:
a)事務機制(重量級):
- channel.txSelect();將當前管道設置成事務模式
- channel.txCommit();提交事務,提交事務成功,可以確保消息一定到達MQ服務器
- channel.txRollback();回滾事務
channel.txSelect();
try {
channel.basicPublish("tx_exchange", "", null, "message".getBytes());
channel.txCommit();
} catch (Exception e){
e.printStackTrace();
channel.txRollback();
//消息的重試機制
}
注意:事務機制雖然可以確保消息的正確到達,但是對服務器性能耗損比較大,一般都不會使用這種方案
b)發送方確認機制(輕量級):
發送方確認機制(publisher confirm):
前面提到的事務機制,雖然可以保證消息的正確發送,但是嚴重影響rabbitmq的性能,因此引入了一種輕量級的消息確認機制 – publisher confirm
publisher confirm機制:
首先將管道設置爲confirm模式,然後在該管道上發送的所有消息,都會被指派一個唯一ID(從1開始遞增),當消息投遞到隊列上後,服務器會發送一個確認信息(攜帶消息的ID)給生產者,生產者就知道消息已經正確達到服務,如果因爲服務器的問題導致消息丟失,服務器也會發送一個失敗的消息給生產者,生產者就可以嘗試進行重試邏輯。 這一整個過程可以是一個同步過程也可以是一個異步過程。
不推薦使用:同步模式
try {
//將管道設置爲confirm模式
channel.confirmSelect();
channel.basicPublish("tx_exchange", "", null, ("message").getBytes());
if(channel.waitForConfirms()){
System.out.println("消息正常發送!");
} else {
System.out.println("消息發送異常,進行重試邏輯!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
推薦使用:異步模式
//設置confirm模式
channel.confirmSelect();
//添加異步回調監聽
channel.addConfirmListener(new ConfirmListener() {
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//消息確認
System.out.println("消息確認id:" + deliveryTag);
//從緩存中剔除消息
//單條消息確認失敗
}
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//消息異常
System.out.println("消息失敗id:" + deliveryTag);
//引入重試邏輯
//批處理消息確認失敗
}
});
for(int i = 0; i < 100000; i++){
long id = channel.getNextPublishSeqNo();
System.out.println("獲得消息id:" + id) ;
channel.basicPublish("tx_exchange", "", null, ("message" + i).getBytes());
//將消息添加進緩存中
}
2.消費者消息確認和拒絕機制
//第一個參數表示消息標識
//第二個參數表示是否批量確認消息
channel.basicAsk(long id,Boolean multipt)
//第一個參數表示消息標識
//第二個參數表示拒絕消息後,消息是否重新放回隊列
channel.basicReject(long id,Boolean requeue)
注意:如果requeue設爲false,則該消息會變成死信消息,如果requeue設置爲true則會重新投遞到消費端,如果只有一個消費端就會變成(拒絕消費-重新投遞)的一個死循環,通常如果需要將requeue設置爲true切記保證至少有兩個以上的消費方
消費者端設置消息的限制:
//當前消費者最多從rabbitmq服務器領取10條十條未確認的消息
//只有消息確認消費了rabbitmq才能再推一條給消費者
channel.basicQos(10);
channel.basicQos會限制當前管道上的消費者所能保持的最大未確認消息數量,如果有一個消息被確認,則隊列會立刻再推送下一個消息到消費者端,依次類推
實際開發中根據服務器的性能進行適當的設置
消息確認簡單Demo
Provider.java
public class Provider {
private static TreeMap<Long, Object> treeMap = new TreeMap<>();
public static void main(String[] args) throws IOException {
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//創建隊列
channel.queueDeclare("queue1", false, false, false, null);
//創建路由
channel.exchangeDeclare("exchange1", "fanout");
//路由和隊列綁定
channel.queueBind("queue1", "exchange1", "");
//將管道設置爲事務模式
// channel.txSelect();
// try {
// //發送消息 - 路由
// channel.basicPublish("exchange1", "",
// null, "Hello!!".getBytes("utf-8"));
// channel.txCommit();
// System.out.println("消息正常發送!");
// } catch (IOException e) {
// channel.txRollback();
// //引入消息的重新發送機制
// System.out.println("消息發送失敗,嘗試重發!");
// }
//publish confirm
// ----- 同步
// channel.confirmSelect();//將管道設置爲confirm模式
// //發送消息 - 路由
// channel.basicPublish("exchange1", "",
// null, "Hello!!".getBytes("utf-8"));
// //同步等待消息確認
// //channel.waitForConfirms() 方法返回true表示消息發送成功 false表示消息發送失敗
// try {
// if(channel.waitForConfirms()){
// //發送成功
// } else {
// //發送失敗
// //引入重試機制
// }
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// ----- 異步
//準備一個消息緩存集合
channel.confirmSelect();//將管道設置爲confirm模式
//添加異步的回調監聽
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//確定消息已經正常發送
//deliveryTag - 確認消息發送成功的消息id
//multiple - 是否爲批量操作,如果爲false,表示當前這條deliveryTag的消息發送成功,
// 如果爲true,表示當前這條deliveryTag的消息之前的所有消息發送成功
if(!multiple){
//單挑確認
treeMap.remove(deliveryTag);
} else {
//批量確認 - deliveryTag 5 1 2 3 4 5
treeMap = (TreeMap<Long, Object>) treeMap.tailMap(deliveryTag + 1);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//消息發送異常
//deliveryTag - 確認消息發送異常的消息id
//multiple - 是否爲批量操作,如果爲false,表示當前這條deliveryTag的消息發送失敗,
// 如果爲true,表示當前這條deliveryTag的消息之前的所有消息發送失敗
if(!multiple){
//單條失敗
//重試 - msg
Object msg = treeMap.get(deliveryTag);
//重試機制
} else {
//批量失敗 5 1 2 3 4 5
TreeMap<Long, Object> map = (TreeMap<Long, Object>) treeMap.headMap(deliveryTag);
//重試機制 - map
}
}
});
//本條消息發送的id
long number = channel.getNextPublishSeqNo();
//發送消息 - 路由
channel.basicPublish("exchange1", "",
null, ("Hello World").getBytes("utf-8"));
//發送完一個消息就將該消息緩存到treemap中
treeMap.put(number, ("Hello World"));
connection.close();
}
}
Consumer.java
public class Consumer {
public static void main(String[] args) throws IOException {
Connection connection = ConnectionUtil.getConnection();
final Channel channel = connection.createChannel();
//創建隊列
// channel.queueDeclare("queue1", false, false, true, null);
//限制消費的條數
channel.basicQos(300);
//當前消費者最多從rabbitmq服務器領取10條未確認的消息,一般有一條消息確認,rabbitmq再推一條給消費者
channel.basicConsume("queue1", false, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("接收到消息:" + new String(body, "utf-8"));
//xxxxxx
//手動確認
if(!new String(body, "utf-8").equals("Hello World")){
//確認消息
channel.basicAck(envelope.getDeliveryTag(), false);
} else {
System.out.println("拒絕了這個消息~!!!");
//拒絕消息
channel.basicReject(envelope.getDeliveryTag(), true);
}
}
});
}
}
RabbitMq的應用場景
1服務間的異步通信
HttpClient
2.保持消息的消費順序
3.定時任務處理
訂單關閉時間
4.請求削峯
在訪問量劇增的情況下,應用仍然需要繼續發揮作用,但是這樣的突發流量並不常見。如果以能處理這類峯值爲標準而投入資源,無疑是巨大的浪費。使用消息中間件能夠使關鍵組件支撐突發訪問壓力,不會因爲突發的超負荷請求而完全癱瘓。
SpringBoot整合RabbitMq
引入依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置:
spring:
rabbitmq:
host: 192.168.226.144
port: 5672
username: admin
password: admin
virtual-host: /admin_host
設置不同模式:
1.簡單的消息模式
提供者
@Bean
public Queue getQueue(){
return new Queue("SimpleQueue");
}
編寫消息處理器
@Component
@RabbitListener(queues = "SimpleQueue")
public class MyRabbitHandler {
@RabbitHandler
public void handler(String message){
System.out.println(message);
}
}
2.Fanout交換機模式
提供者:
- 配置交換機與隊列綁定
@Configuration
public class Config {
@Bean
public Queue getQueueOne(){
return new Queue("one");
}
@Bean
public Queue getQueueTow(){
return new Queue("tow");
}
@Bean
public FanoutExchange getFanoutExchange(){
return new FanoutExchange("fanout_exchange");
}
@Bean
public Binding bindingExchangeOne(Queue getQueueOne, FanoutExchange getFanoutExchange){
return BindingBuilder.bind(getQueueOne).to(getFanoutExchange);
}
@Bean
public Binding bindingExchangeTow(Queue getQueueTow,FanoutExchange getFanoutExchange){
return BindingBuilder.bind(getQueueTow).to(getFanoutExchange);
}
}
- 注入RabbitMQ模板對象
@Autowired
private RabbitTemplate rabbitTemplate;
rabbitTemplate.convertAndSend("fanout_exchange","","Hello RabbitMQ");
消費者
- 配置交換機與隊列綁定
與提供者想相同,解耦,不需要先提供提供者
- 配置監聽者
@Component
public class MyRabbitHandler {
@RabbitHandler
@RabbitListener(queues = "one")
public void processOne(String str){
System.out.println("隊列one接收到對象:"+ str);
}
@RabbitHandler
@RabbitListener(queues = "tow")
public void processTow(String str){
System.out.println("隊列tow接收到對象:" + str);
}
}
3.Topic交換機
提供者
- 配置交換機與隊列綁定
@Configuration
public class TopicRabbitConfig {
@Bean
public TopicExchange getTopicExchange(){
return new TopicExchange("topic_exchange");
}
@Bean
public Queue getQueue(){
return new Queue("topic_queue");
}
@Bean
public Binding bindingExchange(Queue getQueue,TopicExchange getTopicExchange){
return BindingBuilder.bind(getQueue).to(getTopicExchange).with("a.*");
}
}
- 注入RabbitMQ模板對象
@Autowired
private RabbitTemplate rabbitTemplate;
rabbitTemplate.convertAndSend("topic_exchange","a.nba","Hello RabbitMQ1");
rabbitTemplate.convertAndSend("topic_exchange","a.nba.kobe","Hello RabbitMQ2");
消費者
- 配置監聽者
@RabbitHandler
@RabbitListener(queues = "topic_queue")
public void processTopic(String str){
System.out.println("str:" + str);
}