用redis實現訊息佇列(實時消費+ack機制)
訊息佇列
首先做簡單的引入。
MQ主要是用來:
解耦應用、
非同步化訊息
流量削峰填谷
目前使用的較多的有ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ等。
網上的資源對各種情況都有詳細的解釋,在此不做過多贅述。本文
僅介紹如何使用Redis實現輕量級MQ的過程。
為什麼要用Redis實現輕量級MQ?
在業務的實現過程中,就算沒有大量的流量,解耦和非同步化幾乎也是處處可用,此時MQ就顯得尤為重要。但與此同時MQ也是一個蠻重的元件,例如我們如果用RabbitMQ就必須為它搭建一個伺服器,同時如果要考慮可用性,就要為服務端建立一個叢集,而且在生產如果有問題也需要查詢功能。在中小型業務的開發過程中,可能業務的其他整個實現都沒這個重。過重的元件服務會成倍增加工作量。
所幸的是,Redis提供的list資料結構非常適合做訊息佇列。
但是如何實現即時消費?如何實現ack機制?這些是實現的關鍵所在。
如何實現即時消費?
網上所流傳的方法是使用Redis中list的操作BLPOP或BRPOP,即列表的阻塞式(blocking)彈出。
讓我們來看看阻塞式彈出的使用方式:
BRPOP key [key ...] timeout
此命令的說明是:
1、當給定列表內沒有任何元素可供彈出的時候,連線將被 BRPOP 命令阻塞,直到等待超時或發現可彈出元素為止。
2、當給定多個key引數時,按引數 key 的先後順序依次檢查各個列表,彈出第一個非空列表的尾部元素。
另外,BRPOP 除了彈出元素的位置和 BLPOP 不同之外,其他表現一致。
以此來看,列表的阻塞式彈出有兩個特點:
1、如果list中沒有任務的時候,該連線將會被阻塞
2、連線的阻塞有一個超時時間
此時問題已經很明顯了,這裡的超時時間該怎麼設定呢,能不能保證在佇列有訊息進入之前一隻保持阻塞狀態?顯然很難做到,因為兩者沒有相互約束。
好在Redis還支援Pub/Sub(釋出/訂閱)。在訊息A入隊list的同時釋出(PUBLISH)訊息B到頻道channel,此時已經訂閱channel的worker就接收到了訊息B,知道了list中有訊息A進入,即可迴圈lpop或rpop來消費list中的訊息。流程如下:
用redis實現訊息佇列(實時消費+ack機制)
其中的worker可以是單獨的執行緒,也可以是獨立的服務,其充當了Consumer和業務處理者角色。下面做例項說明。
即時消費例項
示例場景為:worker要做同步檔案功能,等到有檔案生成時立馬同步。
首先開啟一個執行緒代表worker,來訂閱頻道channel:
@Servicepublic class SubscribeService { @Resource private RedisService redisService; @Resource private SynListener synListener;//訂閱者 @PostConstruct public void subscribe() { new Thread(new Runnable() { @Override public void run() { LogCvt.info("服務已訂閱頻道:{}", channel); redisService.subscribe(synListener, channel); } }).start(); }}
程式碼中的SynListener即為所宣告的訂閱者,channel為訂閱的頻道名稱,具體的訂閱邏輯如下:
@Servicepublic class SynListener extends JedisPubSub { @Resource private DispatchMessageHandler dispatchMessageHandler; @Override public void onMessage(String channel, String message) { LogCvt.info("channel:{},receives message:{}",channel,message); try { //處理業務(同步檔案) dispatchMessageHandler.synFile(); } catch (Exception e) { LogCvt.error(e.getMessage(),e); } }}
處理業務的時候,就去list中去消費訊息:
@Servicepublic class DispatchMessageHandler { @Resource private RedisService redisService; @Resource private MessageHandler messageHandler; public void synFile(){ while(true){ try { String message = redisService.lpop(RedisKeyUtil.syn_file_queue_key()); if (null == message){ break; } Thread.currentThread().setName(Tools.uuid()); // 佇列資料處理 messageHandler.synfile(message); } catch (Exception e) { LogCvt.error(e.getMessage(),e); } } }}
這樣我們就達到了訊息的實時消費的目的。
如何實現ack機制?
ack,即訊息確認機制(Acknowledge)。
首先來看RabbitMQ的ack機制:
Publisher把訊息通知給Consumer,如果Consumer已處理完任務,那麼它將向Broker傳送ACK訊息,告知某條訊息已被成功處理,可以從佇列中移除。如果Consumer沒有傳送回ACK訊息,那麼Broker會認為訊息處理失敗,會將此訊息及後續訊息分發給其他Consumer進行處理(redeliver flag置為true)。
這種確認機制和TCP/IP協議確立連線類似。不同的是,TCP/IP確立連線需要經過三次握手,而RabbitMQ只需要一次ACK。
值的注意的是,RabbitMQ當且僅當檢測到ACK訊息未發出且Consumer的連線終止時才會將訊息重新分發給其他Consumer,因此不需要擔心訊息處理時間過長而被重新分發的情況。
那麼在我們用Redis實現訊息佇列的ack機制的時候該怎麼做呢?
需要注意兩點:
work處理失敗後,要回滾訊息到原始pending佇列
假如worker掛掉,也要回滾訊息到原始pending佇列
上面第一點可以在業務中完成,即失敗後執行回滾訊息。
實現方案
(該方案主要解決worker掛掉的情況)
維護兩個佇列:pending佇列和doing佇列。
worker定義為ThreadPool。
由pending佇列出隊後,worker分配一個執行緒去處理訊息——給目標訊息append一個當前時間戳和當前執行緒名稱,然後入隊doing佇列。
啟用一個定時任務,每隔一段時間去掃描doing佇列,檢查每隔元素的時間戳,如果超時,則由worker的ThreadPoolExecutor去檢查執行緒是否存在,如果存在則取消當前任務執行,並把事務rollback。最後把該任務從doing佇列中pop出,再重新push進pending佇列。
在worker的某執行緒中,如果處理業務失敗,則主動回滾,並把任務從doing佇列中移除,重新push進pending佇列。
總結
Redis作為訊息佇列是有很大侷限性的。因為其主要特性及用途決定它只能實現輕量級的訊息佇列。寫在最後:沒有絕對好的技術,只有對業務最友好的技術,謹此獻給所有developer。
文章中涉及到的技術點我都分享在群 ⑥⑨⑦⑤⑦⑨⑦⑤①裡,錄製成視訊供大家免費下載,希望可以幫助在這個行業發展的朋友和童鞋們,在論壇部落格等地方少花些時間找資料,把有限的時間,真正花在學習上,所以我把這些資料,分享出來。相信對於已經工作和遇到技術瓶頸或者寫部落格碼友,在這份資料中一定都有你需要的內容。
相關文章
- Redis實現訊息佇列Redis佇列
- redis應用系列二:非同步訊息佇列:生產/消費模式實現及優化Redis非同步佇列模式優化
- Redis 竟然能用 List 實現訊息佇列Redis佇列
- Go中使用Redis實現訊息佇列教程GoRedis佇列
- 一文講透訊息佇列RocketMQ實現消費冪等佇列MQ
- Redis 應用-非同步訊息佇列與延時佇列Redis非同步佇列
- 實時數倉之Flink消費kafka訊息佇列資料入hbaseKafka佇列
- 實戰Spring4+ActiveMQ整合實現訊息佇列(生產者+消費者)SpringMQ佇列
- Redis 學習筆記(六)Redis 如何實現訊息佇列Redis筆記佇列
- PHP基於Redis訊息佇列實現的訊息推送的方法PHPRedis佇列
- Redis 使用 List 實現訊息佇列能保證訊息可靠麼?Redis佇列
- 分散式訊息佇列:如何保證訊息不被重複消費?(訊息佇列消費的冪等性)分散式佇列
- Redis使用ZSET實現訊息佇列使用總結一Redis佇列
- Redis使用ZSET實現訊息佇列使用總結二Redis佇列
- 訊息佇列——數十萬級訊息的消費方案佇列
- [Redis]訊息佇列Redis佇列
- 別再用 Redis List 實現訊息佇列了,Stream 專為佇列而生Redis佇列
- Flink實戰:消費Wikipedia實時訊息
- 用訊息佇列和socket實現聊天系統佇列
- redis應用系列三:延遲訊息佇列正確實現姿勢Redis佇列
- RabbitMQ使用 prefetch_count優化佇列的消費,使用死信佇列和延遲佇列實現訊息的定時重試,golang版本MQ優化佇列Golang
- 訊息機制篇——初識訊息與訊息佇列佇列
- 使用Spring Boot實現訊息佇列Spring Boot佇列
- redis訊息佇列簡單應用Redis佇列
- 剖析 Redis List 訊息佇列的三種消費執行緒模型Redis佇列執行緒模型
- RocketMQ -- 訊息消費佇列與索引檔案MQ佇列索引
- 消費端如何保證訊息佇列MQ的有序消費佇列MQ
- 使用Node.js驅動Redis,實現一個訊息佇列!Node.jsRedis佇列
- Redis?使用?List?實現訊息佇列的優缺點猜陂Redis佇列
- 如何實現MQ佇列訊息監控MQ佇列
- 【MQ】java 從零開始實現訊息佇列 mq-02-如何實現生產者呼叫消費者?MQJava佇列
- 訊息佇列系列一:訊息佇列應用佇列
- 使用SpringCloud Stream結合rabbitMQ實現訊息消費失敗重發機制SpringGCCloudMQ
- 什麼鬼,面試官竟然讓我用Redis實現一個訊息佇列!!?面試Redis佇列
- RabbitMQ訊息佇列(九):Publisher的訊息確認機制MQ佇列
- Redis系列:使用Stream實現訊息佇列 (圖文總結+Go案例)Redis佇列Go
- Redis 中使用 list,streams,pub/sub 幾種方式實現訊息佇列Redis佇列
- 使用Redis做訊息佇列Redis佇列