上一講,我們簡單的寫了一個ActiveMQ的入門案例。這一節,我們主要學習一下什麼是JMS。
1.JMS簡介
什麼是JMS?JMS(java message service)就是Java消息服務,指兩個應用程序之間進行異步通信的API,它爲標準協議和消息服務提供了一組通用接口,包括創建、發送、讀取消息等,用於支持Java應用程序開發。在JavaEE中,當兩個應用程序使用JMS進行通信時,它們之間不是直接相連的,而是通過一個共同的消息收發服務組件關聯起來以達到解耦/異步削峯的效果。
2.JMS組成結構和特點
JMS Provider
實現JMS藉口和規範的消息中間件,也就是我們說的MQ的服務器。
JMS Producer
消息生產者/發佈者,創建和發送消息的客戶端應用;
JMS Consumer
消息消費者/訂閱者,接收和處理JMS消息的客戶端應用;
JMS Message
消息。消息又由消息頭、消息頭和消息屬性組成。
消息頭:
-JMSDestination:消息發送的目的地;
-JMSDeliveryMode:消息的發送模式。一種是持久模式,指消息在被髮送時JMS服務器奔潰,則該條消息不會被丟棄,當服務器恢復之後,會再次傳遞。另一種是非持久模式,JMS服務器奔潰時,消息會被丟失。
-JMSExpiration:消息的過期機制;默認是永不過期;消息過期時間等於Destination的send方法中的timeToLive值加上發送時刻的GMT時間值。如果timeToLive值等於0,則JMSExpiration被設爲0,表示該消息永不過期。如果發送後,在消息過期時間之後還沒有被髮送到目的地,則該消息被清除。
-JMSPriority:消息優先級;從0-9十個級別,0-4是普通消息5-9是加急消息。JMS不要求MQ嚴格按照這十個優先級發送消息但必須保證加急消息要先於普通消息到達。默認是4級。
-JMSMessageID:唯一標識每個消息,由MQ服務器產生。
消息體:
1)封裝消息的具體內容。
2)有五種消息格式:
-TxtMessage:普通字符串消息,包含一個String;
-MapMessage:一個Map類型的消息,key爲String類型,而值爲java基本類型。
-BytesMessage:二進制數組類型,包含一個byte[];
-SteamMessage:java數據流消息,用標準六操作,來順序填充和讀取。
-ObjectMessage:對象消息,包含一個可序列化的java對象。
3)發送和接收的消息體類型必須保持一致。
消息屬性:
如果需要除消息字段以外的值,那麼可以使用消息屬性
3.JMS的可靠性:
1)持久化persistent
消息的持久化是指當MQ服務器宕機後,消息也不會丟失。當服務器恢復後,消息依然可以被消費到。
messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT)
消息的非持久化是指當MQ服務器宕機後,消息會丟失。
messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT)
代碼演示:
生產者
package com.jzt.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
/**
* 演示隊列的持久化和非持久化
* @author sj
*/
public class QueueProducer1 {
public static String MQ_NAME = "admin";
public static String MQ_PASSWORD = "admin";
public static String MQ_BROKETURL = "tcp://192.168.106.131:61616";
public static void main(String[] args) throws JMSException {
ConnectionFactory factory = new ActiveMQConnectionFactory(MQ_NAME, MQ_PASSWORD, MQ_BROKETURL);
Connection connection = factory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("queue1");
MessageProducer producer = session.createProducer(destination);
Message message = session.createTextMessage("這是一個持久化消息");
producer.send(message);
producer.close();
session.close();
connection.close();
}
}
我們先啓動生產者:
現在有一條消息等待消費,然後我們手動停止MQ服務,通過命令:
[root@CentOS122 bin]# ./activemq stop
發現服務器已停止,然後重啓:
[root@CentOS122 bin]# ./activemq restart
INFO: Using default configuration
(you can configure options in one of these file: /etc/default/activemq /root/.activemqrc)
INFO: Invoke the following command to create a configuration file
./activemq setup [ /etc/default/activemq | /root/.activemqrc ]
INFO: Using java '/usr/bin/java'
INFO: Using default configuration
(you can configure options in one of these file: /etc/default/activemq /root/.activemqrc)
INFO: Invoke the following command to create a configuration file
./activemq setup [ /etc/default/activemq | /root/.activemqrc ]
INFO: Using java '/usr/bin/java'
ActiveMQ not running
INFO: Using default configuration
(you can configure options in one of these file: /etc/default/activemq /root/.activemqrc)
INFO: Invoke the following command to create a configuration file
./activemq setup [ /etc/default/activemq | /root/.activemqrc ]
INFO: Using java '/usr/bin/java'
INFO: Starting - inspect logfiles specified in logging.properties and log4j.properties to get details
INFO: pidfile created : '/usr/local/activemq/data/activemq-CentOS122.pid' (pid '13455')
INFO: Using default configuration
(you can configure options in one of these file: /etc/default/activemq /root/.activemqrc)
INFO: Invoke the following command to create a configuration file
./activemq setup [ /etc/default/activemq | /root/.activemqrc ]
INFO: Using java '/usr/bin/java'
ActiveMQ is running (pid '13455')
再去控制檯看消息情況:
發現消息依然存在,未被消費,也沒有丟棄。
消費者:
package com.jzt.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.io.IOException;
/**
* @author sj
*/
public class QueueConsumer1 {
public static String MQ_NAME = "admin";
public static String MQ_PASSWORD = "admin";
public static String MQ_BROKETURL = "tcp://192.168.106.131:61616";
public static void main(String[] args) throws JMSException, IOException {
ConnectionFactory factory = new ActiveMQConnectionFactory(MQ_NAME, MQ_PASSWORD,MQ_BROKETURL);
Connection connection = factory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("queue1");
MessageConsumer consumer = session.createConsumer(destination);
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if(message != null && message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
System.out.println(textMessage);
}
}
});
System.in.read();
consumer.close();
session.close();
connection.close();
}
}
啓動消費者,發現可以獲取到消息。
注意:
持久化和非持久化狀態下,只要MQ服務器沒有宕機,即使沒有消費者消費消息,消息也不會丟失。Queue默認的採用持久化。
2)事務
消費者和生產者的事務,完全沒有關聯,各自是各自的事務。
(1)生產者開啓事務後,執行commit方法,這批消息才真正的被提交。不執行commit方法,這批消息不會提交。執行rollback方法,之前的消息會回滾掉。
生產者的事務機制,要高於簽收機制,當生產者開啓事務,簽收機制不再重要。
代碼演示:
消費者:
package com.jzt.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class QueueProducer2 {
public static String MQ_NAME = "admin";
public static String MQ_PASSWORD = "admin";
public static String MQ_BROKETURL = "tcp://192.168.106.131:61616";
public static void main(String[] args) {
ConnectionFactory factory = new ActiveMQConnectionFactory(MQ_NAME, MQ_PASSWORD, MQ_BROKETURL);
Connection connection = null;
Session session = null;
Destination destination;
MessageProducer producer = null;
try {
connection = factory.createConnection();
connection.start();
session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
destination = session.createQueue("queue2");
producer = session.createProducer(destination);
Message message = session.createTextMessage("這是一個事務消息");
producer.send(message);
// for (int i = 0; i < 3; i++){
// if(i == 1){
// throw new RuntimeException("這裏異常了!");
// }
// }
session.commit();
} catch (JMSException e) {
try {
session.rollback();
} catch (JMSException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}finally {
try {
producer.close();
session.close();
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
當我們沒有開啓異常時,啓動結果如下:
當我們把異常代碼放開,在執行時,得到結果如下:
還是隻有一條消息。說明事務回滾了,這條消息沒有發送成功。
3)簽收
(1)簽收的幾種方式
自動簽收:Session.AUTO_ACKNOWLEDGE。這種方式是默認的。這種方式無線消費者做任何操作,框架都會幫我們自動簽收消息。
手動簽收:Session.CLIENT_ACKNOWLEDGE。這種方式需要消費者客戶端手動調用Message.acknowledge()方法,來簽收消息。如果不簽收消息,消息會被消費者反覆消費,知道被簽收。
允許重複消息:Session.DUPS_OK_ACKNOWLEDGE。多線程或者多個消費者同事消費一個消息時,因爲線程不安全,可能會重複消費。這種方式比較少用到。
事務下簽收:Session.SESSION_TRANSACTED。開始事務的情況下,可以使用這種方式。也比較少用到。
代碼演示:
生產者:
package com.jzt.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
public class QueueProducer3 {
public static String MQ_NAME = "admin";
public static String MQ_PASSWORD = "admin";
public static String MQ_BROKETURL = "tcp://192.168.106.131:61616";
public static void main(String[] args) {
ConnectionFactory factory = new ActiveMQConnectionFactory(MQ_NAME, MQ_PASSWORD, MQ_BROKETURL);
Connection connection = null;
Session session = null;
Destination destination;
MessageProducer producer = null;
try {
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
destination = session.createQueue("queue3");
producer = session.createProducer(destination);
Message message = session.createTextMessage("這是一個需要手動簽收的消息");
producer.send(message);
} catch (JMSException e) {
try {
session.rollback();
} catch (JMSException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}finally {
try {
producer.close();
session.close();
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
消費者:
首先不簽收消息:
package com.jzt.queue;
import org.apache.activemq.ActiveMQConnectionFactory;
import javax.jms.*;
import java.io.IOException;
public class QueueConsumer3 {
public static String MQ_NAME = "admin";
public static String MQ_PASSWORD = "admin";
public static String MQ_BROKETURL = "tcp://192.168.106.131:61616";
public static void main(String[] args) {
ConnectionFactory factory = new ActiveMQConnectionFactory(MQ_NAME, MQ_PASSWORD, MQ_BROKETURL);
Connection connection = null;
Session session = null;
Destination destination;
MessageConsumer consumer = null;
try {
connection = factory.createConnection();
connection.start();
session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
destination = session.createQueue("queue3");
consumer = session.createConsumer(destination);
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if(message != null && message instanceof TextMessage){
TextMessage textMessage = (TextMessage) message;
System.out.println(textMessage);
// try {
// textMessage.acknowledge();
// } catch (JMSException e) {
// e.printStackTrace();
// }
}
}
});
System.in.read();
} catch (JMSException | IOException e) {
e.printStackTrace();
}finally {
try {
consumer.close();
session.close();
connection.close();
} catch (JMSException e) {
e.printStackTrace();
}
}
}
}
當消費者接受到消息未手動簽收時,查看控制檯,發現消息顯示被消費:
而MQ控制檯顯示消息卻沒有出隊,
當我們再次啓動消費者,發現消息依舊可以被消費。所以當我們開啓手動簽收的方式,消費者卻沒有簽收時,消息被重複消費了。
現在我們開啓消費者簽收。調用textMessage.acknowledge()方法。
發現消息就可以正常消費,並且沒有重複消費的情況。
(2)事務和簽收的關係
事務性會話中,當一個事務被成功提交則消息被自動簽收(針對於消費者);如果事務回滾,則消息會被再次傳送。事務優先於簽收,開始事務後,簽收機制不在起作用;
非事務性會話中,消息何時被確認取決於創建回話時的應答模式;
生產事務開啓,只有commit後才能將全部消息變爲已消費。
事務偏向於生產者,簽收偏向於消費者;也就是說,生產者使用事務更好一點,消費者使用簽收機制更好一些。
4.點對點模式總結
點對點模型是基於隊列的,生產者發送消息到隊列,消費者從隊列接收消息,隊列的存在使得消息的異步傳輸成爲可能。和我們平時給朋友發送短信類似。
1)如果在Session關閉時有部分消息被收到但還沒有被簽收(acknowledge),那當消費者下次連接到相同的隊列時,這些消息還會被再次接收
2)隊列可以長久的保存消息直到消費者收到消息。消費者不需要因爲擔心消息會丟失而時刻和隊列保持激活的鏈接狀態,充分體現了異步傳輸模式的優勢
5.發佈訂閱模式總結
JMS Pub/Sub 模型定義瞭如何向一個內容節點發布和訂閱消息,這些節點被稱作topic
主題可以被認爲是消息的傳輸中介,發佈者(publisher)發佈消息到主題,訂閱者(subscribe)從主題訂閱消息。
主題使得消息訂閱者和消息發佈者保持互相獨立不需要解除即可保證消息的傳送。
1)持久訂閱
客戶端首先向MQ註冊一個自己的身份ID識別號,當這個客戶端處於離線時,生產者會爲這個ID保存所有發送到主題的消息,當客戶再次連接到MQ的時候,會根據消費者的ID得到所有當自己處於離線時發送到主題的消息
當持久訂閱狀態下,不能恢復或重新派送一個未簽收的消息。
持久訂閱才能恢復或重新派送一個未簽收的消息。
2)非持久訂閱
非持久訂閱只有當客戶端處於激活狀態,也就是和MQ保持連接狀態才能收發到某個主題的消息。
如果消費者處於離線狀態,生產者發送的主題消息將會丟失作廢,消費者永遠不會收到。
通俗來說:先訂閱註冊才能接受到發佈,只給訂閱者發佈消息。
如何選擇:
當所有的消息必須被接收,則用持久訂閱。當消息丟失能夠被容忍,則用非持久訂閱
-