當我們在使用RocketMQ發送消息時,爲了實現百分百消息可靠投遞,那麼重複消息就不可避免。
發送消息(同步方式)一般經過三個步驟:
首先:將消息從生產端發送到broker,生產端繼續等待這條消息的處理結果(broker是否能夠正常接收);
然後:broker做一些處理(比如同步到從節點以及持久化等);
最後:將應答返還給客戶端,客戶端收到該消息的應答後,做下一步處理。
我們考慮一下這樣的情況:
a. 客戶端將一條消息發送(同步方式)到broker,broker成功接收,當broker將成功接收的結果返回給客戶端時,這個確認消息在網絡傳輸的過程中,由於某些原因丟失了;
b. 那麼對於客戶端來說,爲了實現高可用機制,在超時後默認有2次重試機會,將這條消息會再次發送給broker;
c. 當broker處理完這條消息後,將成功接收的結果返回給客戶端,這次客戶端成功的接收到了來自broker發來的確認消息,然後進行下一步的處理;
此時,broker就有兩條重複的消息。
RocketMQ爲了簡化功能上的設計,允許重複消息的存在,將消息去重的任務交給我們自己去處理。在我公司,我的做法如下:
a. 在消息處理之前,先把這條消息的訂單號放入redis中(有過期時間),即代表這條消息正在處理中,不允許在同一時間處理兩條相同的消息;
b. 根據訂單號,在消息消費記錄表中查詢是否存在(兜底方案,當redis失效時);
b1. 若存在且處理狀態爲已成功處理,則返回處理成功並把redis中訂單號刪除;
b2. 若存在但處理狀態爲處理失敗,則消費這條消息;
b2.1 若這條消息消費成功:
b2.1.1 在消息消費記錄表中的狀態置爲已成功處理;
b2.1.2 將redis中的訂單號刪除;
b2.1.3 返回處理成功;
b2.2 若這條消息處理失敗:
b2.2.1 在消息消費記錄表中的狀態置爲處理失敗;
b2.2.2 將redis中的訂單號刪除;
b2.2.3 返回稍後重試;
b3. 若不存在,則:
b3.1 將這條消息插入消費記錄表中,處理狀態置爲處理中;
b3.2 消費這條消息,若消費成功,則走 b2.1,若消費失敗,則走 b2.2
僞代碼如下:
public class PkgConcurrentlyListener implements MessageListenerConcurrently {
protected static Logger logger = LoggerFactory.getLogger(PkgConcurrentlyListener.class);
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
MessageExt me = list.get(0);
String topic = me.getTopic();
String tags = me.getTags();
String keys = me.getKeys();
boolean result = getLock30Second(keys,keys);
if(!result){
//正在處理中,請稍後再試
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
try {
String msgBody = new String(me.getBody(), RemotingHelper.DEFAULT_CHARSET);
logger.info("消費者消費消息 topic = 【" + topic + "】keys = 【" + keys + "】,msgBody = 【" + msgBody + "】");
//根據keys在消費記錄表中查詢數據
//業務的處理邏輯
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;//消息處理失敗,過會再試
} finally {
//無論消費成功與否,都刪除
RedisPoolUtil.del(keys);
}
}
/**
* 鎖住某個key值30S,需要解鎖時刪除即可
* 注意:不要使用這種方式,因爲這種做法不能防死鎖,使用這篇文章
* https://blog.csdn.net/zhaoming19870124/article/details/91041855介紹的優化版本
*/
public static boolean getLock30Second(final String key, final String value) {
Jedis jedis = RedisPoolConfig.getJedis();
try {
Long flag = jedis.setnx(key, value);
System.out.println("將key = 【" + key + "】,放入redis中的結果 = 【" + flag + "】");
if (1 == flag) {
//如果在redis中不存在,則設置有效期,防止永久存在;
//當這個keys永久存在時,若一條消息在第一次消費時,消費失敗了,則永遠不會被消費了。
// 因爲接下來的每次消費,都被redis攔下來了
flag = jedis.expire(key, 30);
}
return flag == 1L ? true : false;
} catch (Exception e) {
System.out.println(e);
return false;
}
}
}