目前互聯網分佈式架構的系統基本上離不開消息中間件,也就是此篇博文要講的,那我們就一起來認識一下這位朋友吧。
講到MQ不得不提交異步解耦這個概念,就拿電商下訂單舉例(一般訂單都涉及到短信、物流、郵件等各個流程),下面以圖片爲例子說明。
應用介紹
應用解耦
- 不用與線程池綁定,不用寫短信線程任務、物流線程任務等
- 即使物流系統掛了,需要幾分鐘修復,在這幾分鐘消息物流系統待處理的消息都被緩存在MQ裏,並不會影響用戶下單,當系統恢復後,補充存儲在MQ裏的訂單信息即可,終端客戶察覺不到物流系統故障,提升體驗。
流量消峯
淘寶雙十一大部分流量系統會在期間猛增,這個時候如果沒有緩衝機制難以爲繼,可以通過MQ把請求暫存起來,分散到較長的一段時間內處理,這也就是我們雙十一爲什麼到了零點,下訂單不能一次到位的感覺,但事實上進行流量消峯並不是因爲能力不夠,而是出於經濟原則,比如有的業務系統流量最高峯也不會超過10000QPS,平時只有1000QPS,這種情況下就可以用個普通的服務器(支持1000QPS即可),然後加個消息隊列作爲高峯期緩衝,無需大筆資金購買服務器。
消息分發
在大數據時代,數據是不斷產生的,各個分析團隊、算法團隊都需要依賴這些數據進行工作,這個時候有個持久化的消息隊列就顯得尤爲重要。數據的產生方面只需要將各自的消息寫入到消息隊列即可,數據的使用方根據各自需求訂閱各自感興趣的數據,不同的團隊所訂閱的消息是可以重複的也可以不重複,互不干擾,也不必和數據生產方關聯。
各個子系統將日誌數據不停的寫入到消息隊列,不同的數據處理系統有各自的Offset,互不影響。甚至各個團隊處理完的數據也可以寫入到消息隊列,作爲數據的產生方,供給其它團隊使用,避免重複計算。
保證最終一致性
動態擴容
消息中間件概念
1、什麼是Message-Queue消息中間件
- 利用高效可靠地消息機制進行與平臺無關的 數據交流
- 基於數據通信來實現分佈式系統的集成
- 通過提供消息傳遞和消息排隊模型,它可以在分佈式環境下擴展進程間的通信
- 本質上就是接受請求-->保存數據-->發送數據
2、消息中間件的應用場景
- 跨系統數據傳遞、高併發流量消峯、數據異步處理、系統解耦等
3、常用的消息中間件
- ActiceMq、RabbitMq、Kafka、RocketMq
協議
協議是計算機通信共同遵從的一組約定,是對數據格式和計算機之間交換數據時必須遵守的規則。(http重協議,使用輕協議), 消息中間件協議:OpenWire、AMQP、MQTT、Kafka、OpenMessage
- AMQP(Advanced Message Queuing Protocol)協議:高級消息隊列協議。特性:事務支持、持久化支持出生在金融行業,在可靠消息處理上具備天然優勢。使用此協議的MQ:ActiceMq 、RabbitMq
- MQTT(Message Queue Telemetry Transport)協議:消息隊列遙測傳輸,是IBM開發的一個即時通訊的通訊協議,物聯網架構系統中中亞的組成部分。特性:輕量、結構簡單、傳輸快沒有事務支持、沒有持久化相關設計,應用場景:適用於計算能力有限、低帶寬、網路不穩定的場景。使用此協議的MQ:ActiceMq 、RabbitMq
- OpenMessage是最近一兩年有阿里發起的,與雅虎、滴滴出行、Streamlio等公司共同參與創立的分佈式消息中間件協議、流處理應用的開發標準,是國內首個在全球範圍內發起的分佈式消息領域國際標準。特性:結構簡單、解析快、有事務支持、有持久化設計。使用此協議的MQ:Apache RocketMq
- Kafka協議:是基於TCP二進制協議,消息內部是通過長度來分隔,由一些基本數據類型組成。特性:結構簡單,解析快無事務支持有持久化設計。使用此協議的MQ:Kafka
備註:持久化簡單來說就是將數據存入磁盤,而不是存在內存中隨着服務的生命週期一致同生同死,而是是數據永久保存稱之爲持久化。
ActiveMQ安裝(得先安裝JDK1.8以上版本)
- 官網:https://activemq.apache.org/ 文檔:http://activemq.apache.org/components/classic/documentation
- 下載:https://activemq.apache.org/components/classic/download/
- 官網下載太雞兒慢,我這裏給下好了可到我的百度雲盤自行提取(linux版本):https://pan.baidu.com/s/1ZJadXiPPTcYblGb8jq9zoQ 提取碼hf4i
1、下載後解壓文件
mkdir /usr/activemq 並將文件解壓至此處文件夾 tar -zxvf apache-activemq-5.15.9-bin.tar.gz -C /usr/activemq 解壓後目錄如下(可以發現基本上和tomcat一模一樣,其中data是存放日誌文件,webapps是存放管理控制檯程序目錄 ,jar包爲activemq全量包。)
2、創建軟鏈接並啓動
爲方便寫配置項,創建軟鏈接 ln -s /usr/activemq/apache-activemq-5.15.9 /usr/activemq/latest (和快捷方式一樣,操作同一個文件)
啓動:
- 進入bin目錄 cd /usr/activemq/latest/bin
- 後臺守護進程啓動 (稍等會會啓動進程) ./activemq start 備註:前臺啓動 ./activemq console
2、檢測是否啓動成功
方法一:訪問管理控制檯:http://ip:8161/admin ActiveMQ的管理頁面默認開啓了身份校驗: 賬號:admin 密碼:admin
方法二:在啓動Console 或 日誌文件(data/activemq.log)中看到日誌輸出
方法三:或用jps命令查看 6193爲啓動的Pid
[root@zhujiname bin]# jps
6193 activemq.jar
6350 Jps
備註:輸入網址,因爲我這裏是虛擬機所以以實際ip爲準(可能會被防火牆攔截,記得關閉防火牆:service iptables stop,查看防火牆狀態service iptables status)。
ActiveMQ使用
使用下載好的jar包 (在ActiveMQ目錄有activemq-all-5.15.9jar包)或者使用maven依賴下載
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
<version>5.15.9</version>
</dependency>
如果是springboot那麼請添加一下依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
普通web項目運行activemq
package activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.util.Date;
/**
* @author Heian
* @time 19/08/14 12:22
* @description: 消息生產者
*/
public class Producer {
public static void main(String[] args) {
//啓動生產者
ProducerThread producerThread = new ProducerThread ("tcp://192.168.32.130:61616","queue1");
producerThread.start ();
}
static class ProducerThread extends Thread{
String brokerUrl;
String destinationUrl;
public ProducerThread(String brokerUrl,String destinationUrl){
this.brokerUrl = brokerUrl;
this.destinationUrl = destinationUrl;
}
@Override
public void run() {
ActiveMQConnectionFactory connectionFactory;
Connection connection;
Session session;
try {
//1、創建連接工廠
connectionFactory = new ActiveMQConnectionFactory (brokerUrl);
//2、創建連接
connection = connectionFactory.createConnection ();
connection.start ();//一定要記得start
//3、創建會話(可以創建一個或者多個session,我這裏默認一個)
// 第一個參數:是否開啓事務。true:開啓事務,第二個參數忽略。第二個參數:當第一個參數爲false時,纔有意義。消息的應答模式。1、自動應答2、手動應答。一般是自動應答。
session = connection.createSession (false, Session.AUTO_ACKNOWLEDGE);
//4、創建消息發送目標 (Topic 或者 Queue 都實現了Destination)
Destination destination = session.createQueue (destinationUrl);
//5、用目的地創建消息生產者
MessageProducer producer = session.createProducer (destination);
producer.setDeliveryMode (DeliveryMode.PERSISTENT);//設置遞送模式(持久化/不持久化)
//6、創建一條文本消息
String str = "hello activemq:from" + Thread.currentThread ().getName () + ",時間:" + new Date (System.currentTimeMillis ());
TextMessage message = session.createTextMessage (str);
//7、通過producer推送消息
producer.send (message);
//8、清理 關閉連接
session.close ();
connection.close ();
}catch (JMSException e){
e.printStackTrace ();
}
super.run ();
}
}
}
package activemq;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* @author Heian
* @time 19/08/14 20:13
* @description: 消息消費者
*/
public class Consumer {
public static void main(String[] args) {
new ConsumerThread("tcp://192.168.32.130:61616","queue1").start ();
new ConsumerThread("tcp://192.168.32.130:61616","queue1").start ();
}
static class ConsumerThread extends Thread{
String brokerUrl;//ip+端口
String destinationUrl;//目標隊列
public ConsumerThread(String brokerUrl,String destinationUrl){
this.brokerUrl = brokerUrl;
this.destinationUrl = destinationUrl;
}
@Override
public void run() {
ActiveMQConnectionFactory connectionFactory;
Connection connection;
Session session;
MessageConsumer messageConsumer;//消息消費者
try {
//1、創建連接工廠
connectionFactory = new ActiveMQConnectionFactory (brokerUrl);
//2、創建連接
connection = connectionFactory.createConnection ();
connection.start ();//一定要啓動
//3、創建會話(可以創建一個或者多個session,我這裏默認一個)類似於selector 可以同時監聽多個連接
// 第一個參數:是否開啓事務。true:開啓事務,第二個參數忽略。第二個參數:當第一個參數爲false時,纔有意義。消息的應答模式。1、自動應答2、手動應答。一般是自動應答。
session = connection.createSession (false, Session.AUTO_ACKNOWLEDGE);
//4、創建消息消費目標 (Topic 或者 Queue 都實現了Destination)
Destination destination = session.createQueue (destinationUrl);
//5、創建消息消費者
messageConsumer = session.createConsumer (destination);
//session.createDurableSubscriber ("topic",destinationUrl);持久訂閱
//6、接收消息,沒有消息就持續等待 阻塞等待
Message receiveMsg = messageConsumer.receive ();
if (receiveMsg instanceof TextMessage ){
System.out.println ("收到消息:" + ((TextMessage) receiveMsg).getText ());
}else {
System.out.println (receiveMsg);
}
//8、清理 關閉連接
messageConsumer.close ();
connection.close ();
session.close ();
} catch (JMSException e) {
e.printStackTrace ();
}
}
}
}
可以看出其創建過程類似於我們的jdbc的創建,可參考下面的流程圖:
現在分別啓動一個生產和兩個消費者:此時消費者已經消費了一個消息,但還有一個消費者處於阻塞狀態,可查看前臺頁面
如果生產者創建的topic,消費者也訂閱了(發佈訂閱),僞代碼如下:那麼生生產者發一條並且兩個消費者訂閱了,那麼這兩個消費者都會收到消息。
引出問題?
如何是先運行生產者,在運行消費者,也就是生產者生產了1條數據,兩個消費者處於等待,那麼這兩個消費者會收到消息嗎?答案是並不會,就像你訂閱報紙一樣,你開始都沒訂閱人家報紙,人家爲什麼還給你送報紙,對吧。如果想要收到,就必須的持久訂閱。
備註:在發佈訂閱模式下,訂閱者只會收到發佈者的訂閱以後的消息;在持久訂閱模式下(在MQ登記一個有名的訂閱),當前消費者消費了一段時間後可能掛了,此時發佈者會將沒傳遞到你的消息存起來,當你復活起來後,繼續給你推送未收到的消息。
//生產者
public static void main(String[] args) {
ProducerThread producerThread = new ProducerThread("tcp://192.168.32.130:61616","topic1");
producerThread.start ();
}
//消費者
public static void main(String[] args) {
new ConsumerThread("tcp://192.168.32.130:61616","topic1").start ();
new ConsumerThread("tcp://192.168.32.130:61616","topic1").start ();
}
SpringBoot項目運行activemq
yaml配置:
spring:
activemq:
broker-url: tcp://192.168.32.130:61616
#user:admin
#password:admin
配置類:第一配置:連接mq實例配置Bean 第二個配置:對象序列化轉成json類型配置Bean
package com.example.customers.activemq.day1;
import javax.jms.ConnectionFactory;
import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.MessageType;
@Configuration
public class JmsConfiguration {
//定義 連接mq實例的工廠類 如果是多個mq實例,則要建立連接工廠,單機測試的話可有可無
@Bean
public DefaultJmsListenerContainerFactory myFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
System.out.println ("----------創建工廠連接監聽器--------------");
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
return factory;
}
@Bean // 序列化對象在將其轉成json形式
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setTypeIdPropertyName("_type");
return converter;
}
}
生產者:
package com.example.customers.activemq.day1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jms.core.JmsTemplate;
import javax.annotation.PostConstruct;
/**
* @author Heian
* @time 19/08/15 21:47
* @description: 生產者發送對象出去
*/
@SpringBootApplication
public class Producer {
@Autowired
private JmsTemplate jmsTemplate;
@PostConstruct
public void sendMessage() {
// 發送對象出去
System.out.println("-----發送短信中-----");
jmsTemplate.convertAndSend("destinationName", new Emails ("[email protected]", "hello 1"));
jmsTemplate.convertAndSend("destinationName", new Emails ("[email protected]", "hello 2"));
}
public static void main(String[] args) {
SpringApplication.run(Producer.class, args);
}
}
消費者:
package com.example.customers.activemq.day1;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
/**
* @author Heian
* @time 19/08/15 21:51
* @description: 消費者接受對象
*/
@Component
public class Consumer {
//會啓動線程池創建監聽線程不斷的輪詢指定目標地址是否有消息 必須指定連接工廠myFactory,否則無法收到消息
@JmsListener(destination = "destinationName",containerFactory = "myFactory")
public void receive(Emails email) {
System.out.println(Thread.currentThread().getName() + "接受到的消息" + email);
}
}
傳輸對象實體類
package com.example.customers.activemq.day1;
/**
* @author Heian
* @time 19/08/15 21:36
* @description: 測試activemq
*/
public class Emails {
private String emailsUrl;
private String emailsContent;
public Emails() {
}
public Emails(String emailsUrl, String emailsContent) {
this.emailsUrl = emailsUrl;
this.emailsContent = emailsContent;
}
public String getEmailsUrl() {
return emailsUrl;
}
public void setEmailsUrl(String emailsUrl) {
this.emailsUrl = emailsUrl;
}
public String getEmailsContent() {
return emailsContent;
}
public void setEmailsContent(String emailsContent) {
this.emailsContent = emailsContent;
}
@Override
public String toString() {
return "Emails{" +
"emailsUrl='" + emailsUrl + '\'' +
", emailsContent='" + emailsContent + '\'' +
'}';
}
}
此時啓動生產者,會掃描路徑同時消費者也會啓動,即生產者生產了兩條信息,消費者自然也會消費兩條,類比發佈訂閱。