ActiveMQ第五彈:增加ReDelivery功能

黃博文發表於2014-03-09

在使用Message Queue的過程中,總會由於種種原因而導致訊息失敗。一個經典的場景是一個生成者向Queue中發訊息,裡面包含了一組郵件地址和郵件內容。而消費者從Queue中將訊息一條條讀出來,向指定郵件地址傳送郵件。消費者在傳送訊息的過程中由於種種原因會導致失敗,比如網路超時、當前郵件伺服器不可用等。這樣我們就希望建立一種機制,對於未傳送成功的郵件再重新傳送,也就是重新處理。重新處理超過一定次數還不成功,就放棄對該訊息的處理,記錄下來,繼續對剩餘訊息進行處理。

ActiveMQ為我們實現了這一功能,叫做ReDelivery(重新投遞)。當消費者在處理訊息時有異常發生,會將訊息重新放回Queue裡,進行下一次處理。當超過重試次數時,訊息會被放置到一個特殊的Queue中,即Dead Letter Queue,簡稱DLQ,用於進行後續分析。

廢話不多說,一起來實現吧。(該示例中的全部程式碼已放置到GitHub上,請自行下載。)

還是接著本系列中的示例程式碼來進行。要實現ReDelivery功能,要給LinsterContainer加上事務處理。設定SimpleMessageListenerContainer的sessionTransacted屬性為true。

activeMQConnection.xml
1
2
3
4
5
6
7
8
9
    <!-- Message Receiver Definition -->
    <bean id="messageReceiver" class="huangbowen.net.jms.retry.MessageReceiver">
    </bean>
    <bean class="org.springframework.jms.listener.SimpleMessageListenerContainer">
        <property name="connectionFactory" ref="connectionFactory"/>
        <property name="destinationName" value="${jms.queue.name}"/>
        <property name="messageListener" ref="messageReceiver"/>
        <property name="sessionTransacted" value="true" />
    </bean>

然後建立一個ReDeliveryPolicy,來定義ReDelivery的機制。

activeMQConnection.xml
1
    <amq:redeliveryPolicy id="activeMQRedeliveryPolicy" destination="#defaultDestination" redeliveryDelay="100" maximumRedeliveries="4" />

這裡設定ReDelivery的時間間隔是100毫秒,最大重發次數是4次。

在ActiveMQ的Connection Factory中應用這個Policy。就是給Connection Factory設定屬性redeliveryPolicy為我們剛剛建立的Policy。

activeMQConnection.xml
1
2
3
4
5
6
    <!-- Activemq connection factory -->
    <bean id="amqConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL" value="${jms.broker.url}?"/>
        <property name="useAsyncSend" value="true"/>
        <property name="redeliveryPolicy" ref="activeMQRedeliveryPolicy" />
    </bean>

這樣ReDelivery機制就設定好了。那麼怎麼能證明我不是在忽悠你們那?當然最好的辦法是寫自動化測試來測試這個功能了。

首先修改下broker的配置,將其對訊息的持久化設定為false,這樣每次執行測試時Queue中訊息都為0,用於還原現場。然後設定一個Destination Policy,當訊息超過重試次數仍未被正確處理時,就把它放入到以DLQ.為字首的Queue中。由於ActiveMQ預設對非持久化的Message不放入DLQ中的,所以手動設定processNonPersistent為true。

activeMQConnection.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    <amq:broker id="activeMQBroker" persistent="false">
        <amq:transportConnectors>
            <amq:transportConnector uri="${jms.broker.url}"/>
        </amq:transportConnectors>
        <amq:destinationPolicy>
            <amq:policyMap>
                <amq:policyEntries>
                    <amq:policyEntry queue=">">
                        <amq:deadLetterStrategy>
                            <amq:individualDeadLetterStrategy
                                    queuePrefix="DLQ." useQueueForQueueMessages="true" processExpired="true"
                                    processNonPersistent="true" />
                        </amq:deadLetterStrategy>
                    </amq:policyEntry>
                </amq:policyEntries>
            </amq:policyMap>
        </amq:destinationPolicy>
    </amq:broker>

然後新建一個MessageListener,當接收到訊息就丟擲一個異常,這樣用以啟動ReDelivery機制。

retry/MessageReceiver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package huangbowen.net.jms.retry;

import org.springframework.jms.support.JmsUtils;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

public class MessageReceiver implements MessageListener {

    public void onMessage(Message message) {
        if(message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            try {
                String text = textMessage.getText();
                System.out.println(String.format("Received: %s",text));
                throw new JMSException("process failed");
            } catch (JMSException e) {
                System.out.println("there is JMS exception: " + e.getMessage() );
                throw JmsUtils.convertJmsAccessException(e);
            }
        }
    }
}

最後新建一個整合測試類。

ReDeliveryFunctionIntegrationTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package huangbowen.net;

import huangbowen.net.jms.MessageSender;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.BrowserCallback;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;

import javax.jms.JMSException;
import javax.jms.QueueBrowser;
import javax.jms.Session;
import java.util.Enumeration;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

@ContextConfiguration(locations = {"/retry/activeMQConnection.xml"})
@DirtiesContext
public class ReDeliveryFunctionIntegrationTest extends AbstractJUnit4SpringContextTests {


    private final static String DLQ = "DLQ.bar";
    @Autowired
    public JmsTemplate jmsTemplate;

    @Autowired
    public MessageSender messageSender;


    private int getMessagesInDLQ() {
        return jmsTemplate.browse(DLQ, new BrowserCallback<Integer>() {
            @Override
            public Integer doInJms(Session session, QueueBrowser browser) throws JMSException {
                Enumeration messages = browser.getEnumeration();
                int total = 0;
                while(messages.hasMoreElements()) {
                    messages.nextElement();
                    total++;
                }

                return  total;
            }
        });
    }

    @Test
    public void shouldRetryIfExceptionHappened() throws Exception {

        assertThat(getMessagesInDLQ(), is(0));

        messageSender.send("this is a message");
        Thread.sleep(5000);

        assertThat(getMessagesInDLQ(), is(1));
    }
}

我們通過Spring的Autowired功能拿到配置中的JmsTemplate和MessageSender。使用JmsTemplate的brower方法來讀取當前DLQ.bar Queue中有多少剩餘的訊息。用MessageSender來傳送一條訊息,這樣即使我們有Listener來處理這條訊息,但是由於每次都會丟擲異常,超過限定次數後,被放置到了DLQ.bar中。我們檢測DLQ.bar中的訊息數量就可以知道ReDelivery功能是否正確。

執行測試,成功通過。這是日誌資訊:

1
2
3
4
5
6
7
8
9
10
11
12
13
send: this is a message
Received: this is a message
there is JMS exception: process failed
Received: this is a message
there is JMS exception: process failed
Received: this is a message
there is JMS exception: process failed
Received: this is a message
there is JMS exception: process failed
Received: this is a message
there is JMS exception: process failed

Process finished with exit code 0

本系列的全部示例程式碼請在https://github.com/huangbowen521/SpringJMSSample下載。

相關文章