JMS之activemq訊息持久化
JMS 即 java message service 是為java提供了一種建立、傳送、接收訊息的通用方法。可以將複雜的系統進行業務分離,變成靈活的高度解耦合的佈局。同時對我們的日常業務需求開發,提供了非常靈活的業務解決方案。比如繳費還款送積分,送積分的業務邏輯不能影響到繳費還款的業務邏輯,所以最好的,就是繳費/還款邏輯執行完成之後,通過一種方式告訴積分系統,給使用者傳送積分,傳送積分的結果不要影響到複雜的繳費還款的過程。這種情況下,採用jms進行非同步處理,便是一個很好的選擇。
要使用訊息的方式來進行系統互動,我們需要一個訊息中間平臺,來進行訊息的接受轉發,同時處理複雜的訊息持久化等問題。本文我們採用activemq來做實驗。這樣的架構下,我們的系統通常會變成如下架構:
訊息產生者 -> 訊息中心 -> 訊息消費者
1、訊息的兩種傳播方式
JMS支援兩種訊息傳播:PTP 和 PUB/SUB
PTP : 點對點發送。訊息的傳送方將訊息放入管道中,訊息的接收方從管道中取出訊息並處理。
PUB/SUB : 釋出/訂閱方式。訊息的釋出者將自己的主題放入訊息中心,同時進行訊息投遞,訊息訂閱者只獲取自己訂閱的訊息。
jms為了支援上述兩種模式,提供了兩套針對同樣介面的實現,對照關係如下:
ConnectionFacatory:被管理的物件,由客戶端(釋出者/接受者)使用,用來建立一個連結。
Connection:提供一個JMS訊息的活動連結。
Destination:封裝了訊息目的地,或者主題型別。
Session:一個用來發送和接受訊息的線上上下文。
MessageProducer:由session建立的,用來發送訊息的物件。
MessageConsumer:由session建立的用來接受訊息的物件。
2、jms訊息模型
Jms的訊息分為三部分:訊息頭、訊息屬性、訊息體
訊息頭:包含了訊息的客戶端和提供者用來路由和識別訊息的資料。
訊息頭包含的欄位:
JMSDestination:包含了訊息發往的目的地或者主題資訊。
JMSDeliveryMode:訊息傳送模式。spring提供額jms模板提供了2種模式(有預設模式):DEFAULT_DELEVERY_MODE 預設模式、DEFAULT_PRIORITY、DEFAULT_TIME_TO_LIVE
JMSMessageID:訊息標示,唯一性,每個訊息都不同,即便是承載者相同訊息體的訊息。
JMSTimestamp:傳送時間
JMSCorrelationID:與當前訊息關聯的其他訊息的標示
JMSReplyTo:回覆訊息的目的地。帶有這樣屬性的訊息通常是傳送方希望有一個響應,這個響應是可選的。
JMSRedelivered:帶有該欄位的訊息通常過去傳送過但是沒有被確認,如果要再次傳送,提供者必須設定該欄位。如果true,則訊息接受者必須進行訊息重複處理的邏輯。
JMSType:訊息型別標示。官方文件的解釋:
JMSType頭欄位包含了由客戶端在傳送訊息時提供的訊息型別標識。一些訊息提供者使用訊息庫來儲存由應用傳送的訊息定義。type頭欄位可以引用提供者庫中的訊息定義。JMS沒有定義一個標準的訊息定義庫,也沒有定義這個庫中所包含的各種定義的命名策略。一些訊息系統要求每個被建立的應用訊息都必須有一個訊息型別定義,並且每個訊息都指定它的型別。為了能夠使JMS工作於這些訊息系統提供者,無論應用是否使用,JMS客戶端最好賦值JMSType ,這樣可以保證為需要該頭欄位的提供者提供了正確的設定。為了保證移植性,JMS客戶端應使用安裝時在提供者訊息庫中定義的語義值作為JMSType的值。
JMSExpiration :過期時間。
JMSPriority:優先順序。
訊息屬性:包括了標準投資段之外的額外新增給訊息的可選的欄位。比如 應用指定的屬性。
訊息體:訊息攜帶的內容。
3、訊息傳輸程式設計步驟
1)使用jndi獲取一個ConnectionFacatory物件;
2)使用jndi獲取一個或者多個Destination物件;
3)使用ConnectionFactory建立一個JMS連線;
4)使用連線建立Jms session;
5)使用session和destination建立MessageProducers和MessageConsumers
6)使用Connection進行傳輸訊息;
上述是jms的基礎知識,簡單瞭解可以便於下面的應用。jms本身提供了jar可以下載並使用相關配置,結合訊息系統來完成訊息的傳送和接受等操作。但是一種便捷的方式,為加快開發,可以使用spring提供的jms模板,即JmsTemplate,這個類似於jdbcTemplate。
我們演示PTP和PUB/SUB兩種模式的配置。
先看下基礎公用的類:
我們定義:訊息傳送者、訊息接受者、訊息轉換器
[java] view plain copy print?- /**
- * message sender
- * @description <p></p>
- * @author quzishen
- * @project NormandyPositionII
- * @class MessageSender.java
- * @version 1.0
- * @time 2011-1-11
- */
- publicclass MessageSender {
- // ~~~ jmsTemplate
- public JmsTemplate jmsTemplate;
- /**
- * send message
- */
- publicvoid sendMessage(){
- jmsTemplate.convertAndSend("hello jms!");
- }
- publicvoid setJmsTemplate(JmsTemplate jmsTemplate) {
- this.jmsTemplate = jmsTemplate;
- }
- }
- /**
- * message receiver
- * @description <p></p>
- * @author quzishen
- * @project NormandyPositionII
- * @class MessageReceiver.java
- * @version 1.0
- * @time 2011-1-11
- */
- publicclass MessageReceiver implements MessageListener {
- /* (non-Javadoc)
- * @see javax.jms.MessageListener#onMessage(javax.jms.Message)
- */
- publicvoid onMessage(Message m) {
- System.out.println("[receive message]");
- ObjectMessage om = (ObjectMessage) m;
- try {
- String key1 = om.getStringProperty("key1");
- System.out.println(key1);
- System.out.println("model:"+om.getJMSDeliveryMode());
- System.out.println("destination:"+om.getJMSDestination());
- System.out.println("type:"+om.getJMSType());
- System.out.println("messageId:"+om.getJMSMessageID());
- System.out.println("time:"+om.getJMSTimestamp());
- System.out.println("expiredTime:"+om.getJMSExpiration());
- System.out.println("priority:"+om.getJMSPriority());
- } catch (JMSException e) {
- e.printStackTrace();
- }
- }
- }
- /**
- * message converter
- * @description <p></p>
- * @author quzishen
- * @project NormandyPositionII
- * @class MessageConvertForSys.java
- * @version 1.0
- * @time 2011-1-11
- */
- publicclass MessageConvertForSys implements MessageConverter {
- /* (non-Javadoc)
- * @see org.springframework.jms.support.converter.MessageConverter#toMessage(java.lang.Object, javax.jms.Session)
- */
- public Message toMessage(Object object, Session session)
- throws JMSException, MessageConversionException {
- System.out.println("[toMessage]");
- ObjectMessage objectMessage = session.createObjectMessage();
- objectMessage.setJMSExpiration(1000);
- objectMessage.setStringProperty("key1", object+"_add");
- return objectMessage;
- }
- /* (non-Javadoc)
- * @see org.springframework.jms.support.converter.MessageConverter#fromMessage(javax.jms.Message)
- */
- public Object fromMessage(Message message) throws JMSException,
- MessageConversionException {
- System.out.println("[fromMessage]");
- ObjectMessage objectMessage = (ObjectMessage) message;
- return objectMessage.getObjectProperty("key1");
- }
- }
第一種,PTP方式的配置:
[java] view plain copy print?- <?xml version="1.0" encoding="UTF-8"?>
- <beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
- xmlns:context="http://www.springframework.org/schema/context"
- xmlns:aop="http://www.springframework.org/schema/aop"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
- http://www.springframework.org/schema/context
- http://www.springframework.org/schema/context/spring-context-2.5.xsd "
- default-autowire="byName">
- <!-- JMS PTP MODEL -->
- <!-- PTP連結工廠 -->
- <bean id="queueConnectionFactory"class="org.apache.activemq.spring.ActiveMQConnectionFactory">
- <property name="brokerURL" value="tcp://127.0.0.1:61616" />
- <!-- <property name="brokerURL" value="vm://normandy.notify" /> -->
- <property name="useAsyncSend" value="true" />
- </bean>
- <!-- 定義訊息佇列 -->
- <bean id="dest"class="org.apache.activemq.command.ActiveMQQueue">
- <constructor-arg value="queueDest" />
- </bean>
- <!-- PTP jms模板 -->
- <bean id="jmsTemplate"class="org.springframework.jms.core.JmsTemplate">
- <property name="connectionFactory" ref="queueConnectionFactory"></property>
- <property name="defaultDestination" ref="dest" />
- <property name="messageConverter" ref="messageConvertForSys" />
- <property name="pubSubDomain" value="false" />
- </bean>
- <!-- 訊息轉換器 -->
- <bean id="messageConvertForSys"class="com.normandy.tech.test.MessageConvertForSys"></bean>
- <!-- 訊息傳送方 -->
- <bean id="messageSender"class="com.normandy.tech.test.MessageSender"></bean>
- <!-- 訊息接收方 -->
- <bean id="messageReceiver"class="com.normandy.tech.test.MessageReceiver"></bean>
- <!-- 訊息監聽容器 -->
- <bean id="listenerContainer"
- class="org.springframework.jms.listener.DefaultMessageListenerContainer">
- <property name="connectionFactory" ref="queueConnectionFactory" />
- <property name="destination" ref="dest" />
- <property name="messageListener" ref="messageReceiver" />
- </bean>
- </beans>
第二種:PUB/SUB方式的配置
我們配置兩個訊息訂閱者,分別訂閱不同的訊息,這樣用於判斷是否成功執行了訊息的釋出和訊息的訂閱
[java] view plain copy print?- <?xml version="1.0" encoding="UTF-8"?>
- <beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
- xmlns:context="http://www.springframework.org/schema/context"
- xmlns:aop="http://www.springframework.org/schema/aop"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
- http://www.springframework.org/schema/context
- http://www.springframework.org/schema/context/spring-context-2.5.xsd "
- default-autowire="byName">
- <!-- JMS TOPIC MODEL -->
- <!-- TOPIC連結工廠 -->
- <bean id="topicSendConnectionFactory"class="org.apache.activemq.spring.ActiveMQConnectionFactory">
- <property name="brokerURL" value="tcp://127.0.0.1:61616" />
- <property name="useAsyncSend" value="true" />
- </bean>
- <bean id="topicListenConnectionFactory"class="org.apache.activemq.spring.ActiveMQConnectionFactory">
- <property name="brokerURL" value="tcp://127.0.0.1:61616" />
- </bean>
- <!-- 定義主題 -->
- <bean id="myTopic"class="org.apache.activemq.command.ActiveMQTopic">
- <constructor-arg value="normandy.topic"/>
- </bean>
- <bean id="myTopic2"class="org.apache.activemq.command.ActiveMQTopic">
- <constructor-arg value="normandy.topic2"/>
- </bean>
- <!-- 訊息轉換器 -->
- <bean id="messageConvertForSys"class="com.normandy.tech.test.MessageConvertForSys"></bean>
- <!-- TOPIC send jms模板 -->
- <bean id="topicSendJmsTemplate"class="org.springframework.jms.core.JmsTemplate">
- <property name="connectionFactory" ref="topicSendConnectionFactory"></property>
- <property name="defaultDestination" ref="myTopic" />
- <property name="messageConverter" ref="messageConvertForSys" />
- <!-- 開啟訂閱模式 -->
- <property name="pubSubDomain" value="true"/>
- </bean>
- <!-- 訊息傳送方 -->
- <bean id="topicMessageSender"class="com.normandy.tech.test.MessageSender">
- <property name="jmsTemplate" ref="topicSendJmsTemplate"></property>
- </bean>
- <!-- 訊息接收方 -->
- <bean id="topicMessageReceiver"class="com.normandy.tech.test.MessageReceiver">
- </bean>
- <!-- 主題訊息監聽容器 -->
- <bean id="listenerContainer"
- class="org.springframework.jms.listener.DefaultMessageListenerContainer">
- <property name="connectionFactory" ref="topicListenConnectionFactory" />
- <property name="pubSubDomain" value="true"/><!-- default is false -->
- <property name="destination" ref="myTopic" /> <!-- listen topic: myTopic -->
- <property name="subscriptionDurable" value="true"/>
- <property name="clientId" value="clientId_001"/>
- <property name="messageListener" ref="topicMessageReceiver" />
- </bean>
- <!-- 主題訊息監聽容器2 -->
- <bean id="listenerContainer2"
- class="org.springframework.jms.listener.DefaultMessageListenerContainer">
- <property name="connectionFactory" ref="topicListenConnectionFactory" />
- <property name="pubSubDomain" value="true"/><!-- default is false -->
- <property name="destination" ref="myTopic2" /> <!-- listen topic: myTopic2 -->
- <property name="subscriptionDurable" value="true"/>
- <property name="clientId" value="clientId_002"/>
- <property name="messageListener" ref="topicMessageReceiver" />
- </bean>
- </beans>
單元測試程式碼:
[java] view plain copy print?- publicclass TechTest extends TestCase {
- ApplicationContext ptpApplicationContext;
- ApplicationContext topicApplicationContext;
- @Override
- protectedvoid setUp() throws Exception {
- super.setUp();
- ptpApplicationContext = new ClassPathXmlApplicationContext(
- "com/normandy/tech/test/ptpContext.xml");
- topicApplicationContext = new ClassPathXmlApplicationContext(
- "com/normandy/tech/test/topicContext.xml");
- }
- protected Object getPtpBean(String name) {
- return ptpApplicationContext.getBean(name);
- }
- protected Object getTopicBean(String name) {
- return topicApplicationContext.getBean(name);
- }
- }
- /**
- * 測試訊息傳送
- * @description <p></p>
- * @author quzishen
- * @project NormandyPositionII
- * @class JmsQueueTest.java
- * @version 1.0
- * @time 2011-1-11
- */
- publicclass JmsQueueTest extends TechTest {
- /**
- * 測試訊息傳送
- */
- publicvoid testQueueSend() {
- long beginTime = System.currentTimeMillis();
- // PTP
- // MessageSender messageSender = (MessageSender) getPtpBean("messageSender");
- // messageSender.sendMessage();
- // TOPIC
- MessageSender messageSender = (MessageSender) getTopicBean("topicMessageSender");
- messageSender.sendMessage();
- System.out.println("cost time:"+ (System.currentTimeMillis() - beginTime));
- }
- }
測試結果執行便可。
在這裡,訊息系統我們採用的是activemq,試想一個問題,如果訊息過多,這個時候發生了宕機,訊息是否會丟失?
這裡就涉及到了一個新問題,即訊息持久化。
activemq的訊息持久化分成兩種:檔案和資料庫(支援MySQL/oracle)。可以再其配置檔案中進行配置,activemq配置檔案採用的是spring的方式,所以配置起來非常的方便。
通常下載了activemq後,會有一系列的配置檔案demo,可以參照其中的樣例修改即可。
這裡我們使用mysql作為訊息持久化的資料庫伺服器。
將mysql的驅動包,拷貝到activemq的lib目錄,配置如下:
conf/activemq.xml
[xhtml] view plain copy print?- <persistenceAdapter>
- <!--<kahaDB directory="${activemq.base}/data/kahadb"/>-->
- <jdbcPersistenceAdapterdataDirectory="${activemq.base}/data"dataSource="#mysql-ds"/>
- </persistenceAdapter>
- <bean id="mysql-ds"class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
- <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
- <property name="url" value="jdbc:mysql://localhost/activemq?relaxAutoCommit=true"/>
- <property name="username" value="root"/>
- <property name="password" value="root"/>
- <property name="maxActive" value="200"/>
- <property name="poolPreparedStatements" value="true"/>
- </bean>
特別注意的是,這裡指定的資料庫名稱,需要事先在mysql中建立好schema。
執行activemq,可以發現自動建立了三張表:
activemq_acks
activemq_lock
activemq_msgs