本文以ActiveMQ最新的5.10版本爲準。
大家知道,JMS規範中,Message消息頭接口中有setJMSRedelivered(boolean redelivered)和getJMSRedelivered()方法,用於設置和獲取消息的重發標誌,當然set方法主要是MOM來調用的,我們客戶端使用的是get方法。
還記得當時阿里的電話面試曾問過我,你知道ActiveMQ中的消息重發時間間隔和重發次數嗎?我當時尷尬了,只知道會重發,還真沒去了解過其中的細節,所以最後被完美的“淘汰了”。
後來有時間了就去網上看了下官方的文檔,所以現在把ActiveMQ中的重發機制和大家一起分享一下。
首先,我們得大概瞭解下,在哪些情況下,ActiveMQ服務器會將消息重發給消費者,這裏爲簡單起見,假定採用的消息發送模式爲隊列(即消息發送者和消息接收者)。
1.如果消息接收者在處理完一條消息的處理過程後沒有對MOM進行應答,則該消息將由MOM重發。需要注意的是,如果採用非事務持久化消息加Session.CLIENT_ACKNOWLEDGE應答模式,當消費者在處理完消息後沒有主動調用Message#acknowledge()方法時,MOM不會主動重發,如果這時候MOM宕機了,當重啓MOM後,將消費者機器也重啓後MOM纔會重發消息,但此時的消息不會有重發標記,因爲MOM都不記得自己有宕機過,也不知道這些消息被髮送過。
2.如果我們隊某個隊列設置了預讀參數(consumer.prefetchSize),如果消息接收者在處理第一條消息時(沒向MOM發送消息接收確認)就宕機了,則預讀數量的所有消息都將被重發。
3.如果Session是事務的,則只要消息接收者有一條消息沒有確認,或發送消息期間MOM或客戶端某一方突然宕機了,則該事務範圍中的所有消息MOM都將重發。
說到這裏,大家可能會有疑問,ActiveMQ消息服務器怎麼知道消費者客戶端到底是消息正在處理中還沒來得急對消息進行應答還是已經處理完成了沒有應答或是宕機了根本沒機會應答呢?其實在所有的客戶端機器上,內存中都運行着一套客戶端的ActiveMQ環境,該環境負責緩存發來的消息,負責維持着和ActiveMQ服務器的消息通訊,負責失效轉移(fail-over)等,所有的判斷和處理都是由這套客戶端環境來完成的。
我們可以來對ActiveMQ的重發策略(Redelivery Policy)來進行自定義配置,其中的配置參數主要有以下幾個:
a) collisionAvoidanceFactor :碰撞躲避因數,默認值是0.15,這個參數是爲了躲避高併發的重發帶來的問題,我們查看org.apache.activemq.RedeliveryPolicy類的源代碼,
// +/-15% for a 30% spread -cgs
protected double collisionAvoidanceFactor = 0.15d;
protected long initialRedeliveryDelay = 1000L;
可以發現,該默認值帶來的變動範圍是正負百分之15,也就是有30%的範圍,也就是說,如果延遲發送時間(也就是initialRedeliveryDelay 默認值)是1000毫秒,則該條消息第一次有可能被拖延850毫秒到1150毫秒之間後被髮送,如果有第二次重發,基數就不是1000毫秒了,而是以上一次重發拖延時間爲基礎來算。源代碼如下:
public long getNextRedeliveryDelay(long previousDelay) {
long nextDelay = redeliveryDelay;
if (previousDelay > 0 && useExponentialBackOff && backOffMultiplier > 1) {
nextDelay = (long) (previousDelay * backOffMultiplier);
if(maximumRedeliveryDelay != -1 && nextDelay > maximumRedeliveryDelay) {
// in case the user made max redelivery delay less than redelivery delay for some reason.
nextDelay = Math.max(maximumRedeliveryDelay, redeliveryDelay);
}
}
if (useCollisionAvoidance) {
/*
* First random determines +/-, second random determines how far to
* go in that direction. -cgs
*/
Random random = getRandomNumberGenerator();
double variance = (random.nextBoolean() ? collisionAvoidanceFactor : -collisionAvoidanceFactor) * random.nextDouble();
nextDelay += nextDelay * variance;
}
return nextDelay;
}
b)maximumRedeliveries :最大重發次數,默認值是6,如果你想不限次數重發,可以設置成-1。同樣是org.apache.activemq.RedeliveryPolicy類中的代碼:
public static final int NO_MAXIMUM_REDELIVERIES = -1;
public static final int DEFAULT_MAXIMUM_REDELIVERIES = 6;
protected int maximumRedeliveries = DEFAULT_MAXIMUM_REDELIVERIES;
我們探究一下maximumRedeliveries 的get方法,可以發現有org.apache.activemq.ActiveMQSession和org.apache.activemq.ActiveMQMessageConsumer兩個類中有用到:
其中ActiveMQSession中的代碼如下:
// Figure out how long we should wait to resend this message.
long redeliveryDelay = redeliveryPolicy.getInitialRedeliveryDelay();
for (int i = 0; i < redeliveryCounter; i++) {
// 每次重發拖延時間都是以上一次重發拖延時間來算,所以這裏for循環來取得最新的拖延時間
redeliveryDelay = redeliveryPolicy.getNextRedeliveryDelay(redeliveryDelay);
}
// 交給定時任務重發
connection.getScheduler().executeAfterDelay(new Runnable() {
public void run() {
((ActiveMQDispatcher)md.getConsumer()).dispatch(md);
}
}, redeliveryDelay);
ActiveMQMessageConsumer中的代碼類似。
c)maximumRedeliveryDelay :重發最大拖延時間,默認爲-1,表示沒有最大拖延時間,此參數只有當useExponentialBackOff 爲true時起效。同樣是RedeliveryPolicy中的代碼:
protected long maximumRedeliveryDelay = -1;
public long getNextRedeliveryDelay(long previousDelay) {
long nextDelay = redeliveryDelay;
if (previousDelay > 0 && useExponentialBackOff && backOffMultiplier > 1) {
nextDelay = (long) (previousDelay * backOffMultiplier);
if(maximumRedeliveryDelay != -1 && nextDelay > maximumRedeliveryDelay) {
// in case the user made max redelivery delay less than redelivery delay for some reason.
nextDelay = Math.max(maximumRedeliveryDelay, redeliveryDelay);
}
}
。。。。。
}
看源代碼就顯而易見了。
d)initialRedeliveryDelay :第一次重發的拖延時間基礎,默認是1000,單位爲毫秒,前面講collisionAvoidanceFactor 屬性時已經提到過,這裏不再多說。
e)redeliveryDelay :如果initialRedeliveryDelay 爲0,則使用redeliveryDelay ,默認也是1000。RedeliveryPolicy中源代碼如下:
protected long initialRedeliveryDelay = 1000L;
protected long redeliveryDelay = initialRedeliveryDelay;
f)useCollisionAvoidance :消息重發時是否採用前面提到的碰撞避免collisionAvoidanceFactor 參數,默認是false,不採用。源代碼上面也給出了,這裏不再多說。
g)useCollisionAvoidance :是否使用成倍增加拖延,默認爲false,如果我們希望重發的拖延時間一次比一次大很多,則可以設置它爲true。上面已經給出過源代碼,這裏再次給出:
protected boolean useExponentialBackOff;
protected double backOffMultiplier = 5.0;
public long getNextRedeliveryDelay(long previousDelay) {
long nextDelay = redeliveryDelay;
if (previousDelay > 0 && useExponentialBackOff && backOffMultiplier > 1) {
nextDelay = (long) (previousDelay * backOffMultiplier);
if(maximumRedeliveryDelay != -1 && nextDelay > maximumRedeliveryDelay) {
// in case the user made max redelivery delay less than redelivery delay for some reason.
nextDelay = Math.max(maximumRedeliveryDelay, redeliveryDelay);
}
}
if (useCollisionAvoidance) {
/*
* First random determines +/-, second random determines how far to
* go in that direction. -cgs
*/
Random random = getRandomNumberGenerator();
double variance = (random.nextBoolean() ? collisionAvoidanceFactor : -collisionAvoidanceFactor) * random.nextDouble();
nextDelay += nextDelay * variance;
}
return nextDelay;
}
可以看出,成倍拖延是將上一次拖延時間乘以backOffMultiplier來實現的,而 backOffMultiplier默認爲5.
h)backOffMultiplier :成倍拖延時間的倍率,默認爲5,上面已經提到了,這裏不再多說。
那麼接下來我們討論下該如何配置上面所說的幾項,我們可以通過Java代碼,也就是JMS API來配置,也可以通過Spring來配置,當然也可以通過連接器的URL來配置:
如果直接使用JMS API來使用ActiveMQ,我們可以如下配置(代碼來自ActiveMQ的官方說明):
ActiveMQConnection connection ... // Create a connection
RedeliveryPolicy queuePolicy = new RedeliveryPolicy();
queuePolicy.setInitialRedeliveryDelay(0);
queuePolicy.setRedeliveryDelay(1000);
queuePolicy.setUseExponentialBackOff(false);
queuePolicy.setMaximumRedeliveries(2);
RedeliveryPolicy topicPolicy = new RedeliveryPolicy();
topicPolicy.setInitialRedeliveryDelay(0);
topicPolicy.setRedeliveryDelay(1000);
topicPolicy.setUseExponentialBackOff(false);
topicPolicy.setMaximumRedeliveries(3);
// Receive a message with the JMS API
RedeliveryPolicyMap map = connection.getRedeliveryPolicyMap();
map.put(new ActiveMQTopic("topic1"), topicPolicy);
map.put(new ActiveMQQueue("queue1"), queuePolicy);