眾所周知,訊息佇列是應用系統中重要的元件,主要解決應用解耦,非同步訊息,流量削鋒等問題,實現高效能,高可用,可伸縮和最終一致性架構。目前使用較多的訊息佇列有 ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ.
但是如果你不想為你的系統引入一個重量級(相對 redis 來說)的 mq,但是想要享受解耦、非同步訊息等特性,通過本文你就 get 到了,通過 redis 實現一個簡單版的 mq。
為什麼是 redis
- redis 通常作為快取服務引入,因此大部分系統都會有 redis
- redis 本身的資源消耗是極小的,符合我們的輕量要求
- redis 速度很快,幾乎不會出現速度瓶頸
- redis 有持久化方案,調整配置項可以在資料安全和速度間進行取捨(參考這篇)[https://segmentfault.com/a/1190000002906345]
如何實現
利用 redis 的佇列結構來實現訊息佇列。redis 單個佇列最多支援 2*32-1 條資料,對於大部分應用是完全夠用的。
簡單來說就是:
- 每個 topic 對應一條佇列
- 從佇列一段寫入資料,從另一端讀取資料
- 消費失敗,重新將訊息放入佇列
注意:程式碼僅供個人嚐鮮使用,請勿用於真實生產環境
程式碼僅可在 springboot 環境中使用
首先定義註解和介面類
註解程式碼如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface MqConsumer {
/**
* 佇列主題
*/
String topic() default "default_es_topic";
}
被該註解修飾的類,將會接收 topic 下的訊息。
介面程式碼如下:
public interface RedisConsumer {
/**
* 功能描述: 消費方法,消費者類必須繼承此方法
*
* @param message 資料載體
* @author 123
* @date 2020/3/28 22:41
*/
void deal(String message);
}
本介面用於定於接受訊息的處理方法。
掃描註解修飾類
本部分為核心程式碼,首先需要獲取程式碼中被註解修飾的類,然後建立一個迴圈從 redis 佇列中取資料,最後呼叫類物件的 deal 方法消費訊息,如果 deal 方法丟擲錯誤,認為消費失敗,重新將該資料放入佇列中。
- 掃描部分程式碼如下:
/**
* MqConfiguration.java
*/
@Override
public void run(ApplicationArguments args) {
Map<String, Object> map = context.getBeansWithAnnotation(MqConsumer.class);
map.values().forEach(item -> {
if (!(item instanceof RedisConsumer)) {
log.warn("注意檢測到被@EsConsumer註解的類{}未實現RedisConsumer介面", item.getClass().getCanonicalName());
return;
}
MqConsumer[] annotations = item.getClass().getAnnotationsByType(MqConsumer.class);
MqConsumer annotation = annotations[0];
String topic = annotation.topic();
if (topicMap.containsKey(topic)) {
log.error("多個消費者{},消費同一個訊息:{},已忽略", item.getClass().getCanonicalName(), topic);
} else {
topicMap.put(topic, (RedisConsumer) item);
}
});
log.info("redis訂閱資訊彙總完畢!!!!!!");
//由一個執行緒始終迴圈獲取es佇列資料
threadPoolExecutor.execute(loop());
}
run 方法在 spring 掃描完畢後呼叫,通過實現ApplicationRunner
介面實現,通過 spring 的方法來獲取所有被MqConsumer
介面註解的類(否則需要自己寫類載入器)。資料彙總完畢後使用一個執行緒來進行無線迴圈從 redis 佇列中取資料。
- 執行執行緒部分程式碼如下:
private Runnable loop() {
return () -> {
while (true) {
AtomicInteger count = new AtomicInteger(0);
topicMap.forEach((k, v) -> {
try {
String message = mqUtil.getRedisTemplate().opsForList().rightPop(k);
if (message == null) {
count.getAndIncrement();
} else {
pushTask(v, message, k);
}
} catch (RedisConnectionFailureException connException) {
log.error("redis無法連線,10s後重試", connException);
sleep(10);
} catch (Exception e) {
log.error("redis訊息佇列異常", e);
}
});
if (count.get() == topicMap.keySet().size()) {
//當所有的佇列都為空時休眠1s
sleep(1);
}
}
};
}
private void pushTask(RedisConsumer item, String value, String key) {
threadPoolExecutor.execute(() -> {
try {
item.deal(value);
} catch (Exception e) {
log.error("執行消費任務出錯", e);
//非廣播訊息進行資料回補
mqUtil.getRedisTemplate().opsForList().rightPush(key, value);
}
});
}
loop 方法無限迴圈根據 topic 從 redis 中取資料,如果取到資料,呼叫 pushTask 方法執行,如果執行報錯將會進行資料回補。
完整程式碼見本文結尾
測試
執行專案後呼叫,MainController
中的介面即可測試。
完整程式碼:github
本文原創釋出於:手把手教你用 redis 實現一個簡單的 mq 訊息佇列