基於內存的單線程數據庫,使Redis的線程安全性與性能極高。而Redis的雙向鏈表數據類型(List)天生就可作爲消息隊列存儲消息.
在這裏就不說消息隊列的等等一些優點。但是補充一下Redis的List類型的幾個命令,你可以指定將一個元素投送到列表的頭部(左邊)或者尾部(右邊),當然也可以指定從列表的頭部或尾部取出數據.
LPush:添加元素至列表的頭部
RPush:添加元素至列表的尾部
LPop:移除並獲取列表的頭部的第一個元素
RPop:移除並獲取列表的尾部的第一個元素
BLpop:移出並獲取列表頭部的第一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素爲止。命令格式:blpop key timeout,當timeout=0時,表示一直阻塞等待,直到有其他客戶端執行rpush或者lpush命令,插入數據後,阻塞才解除.
BRpop:與BLpop相同,不同的是它是移除列表尾部的第一個元素.
如下,開啓兩個客戶端,一個客戶端先使用BLpop阻塞讀取數據,另一個客戶端寫入數據.
OK,到此我想你已經明白了,List的作用已經顯而易見。生產者投入消息,消費者拿到消息。而且雙向鏈表的數據類型,投入和拿取數據都特別靈活。是不是感覺很不錯?接着往下看😄
下面在代碼中實現消息隊列的數據投遞與拾取.
引入NuGet包:StackExchange.Redis
生產者:
static void Main(string[] args) { var redis = new RedisContent(); for (int i = 0; i < 10; i++) { redis.db.ListRightPush("datalist", $"data{i}");//向列表的尾部投遞消息 Console.WriteLine($"{DateTime.Now} 已投遞消息data{i}!"); Thread.Sleep(3000); } Console.ReadKey(); }
消費者:
static void Main(string[] args) { var redis = new RedisContent(); while (true) { string result = redis.db.ListRightPop("datalist");//從列表的尾部拾取消息 if (string.IsNullOrEmpty(result)) { } else { Console.WriteLine($"{DateTime.Now} 已接收消息,Message={result.ToString()}"); Thread.Sleep(1000); } } }
爲了讓大家看到效果,我故意讓線程等待了幾秒鐘.
先進先出和先進後出的實現方式都比較靈活,如果要想實現先進先出的規則的話,要將上面的消費者代碼改爲redis.db.ListLeftPop("datalist")=>從頭部開始讀取消息
但是上面的代碼有一個很大的弊端,雖然消息已經消費完了,但是仍然在不停的lpop,所以造成很大的浪費.就算是這裏使用了Sleep,一定程度上減少了CPU的佔用率,但是消息處理的時效性就削弱了.
不用擔心,對此肯定有解決的方法😀,我們上面提到了Redis有兩個阻塞命令BRpop與BLpop,這兩個命令可以解決上述問題.有消息的話它就會幫你拿出來,而且不用while(true)的方式也會減少CPU的開銷.因爲列表沒有消息的話,它就會一直阻塞,可以理解爲保持了一個長連接(就相當於你問你女朋友爲什麼生氣,然後她就說因爲什麼什麼...,晚上你問她想喫點什麼,然後她說想喫點什麼什麼...,你每次都要去問她,時間久了她就覺得很煩,會覺得你不懂她。所以你就住進她心裏面,她心裏面想什麼你就能第一時間知道,用這個做比喻我相信你們都能懂😂)。
但是StackExchange.Redis並沒有提供BLpop與BRpop的API,我們可以使用使用pub/sub的方式.代碼如下:
生產者:
static void Main(string[] args) { var redis = new RedisContent(); var sub = RedisContent.redis.GetSubscriber(); for (int i = 0; i < 10; i++) { sub.PublishAsync("datalist", $"data{i}").GetAwaiter(); Console.WriteLine($"{DateTime.Now} 已投遞消息data{i}!"); Thread.Sleep(3000); } Console.ReadKey(); }
消費者:
static void Main(string[] args) { var redis = new RedisContent(); var sub = RedisContent.redis.GetSubscriber(); sub.Subscribe("datalist", (channel, message) => { Console.WriteLine($"{DateTime.Now} 已接收消息,Message={message}"); Thread.Sleep(1000); }); Console.WriteLine("消費者0已啓動成功!"); Console.ReadKey(); }
分別啓動兩個消費者客戶端
這種爲廣播模式,每一個訂閱者都會收到消息。但是該消息不保證是否被接收,生產者投遞完消息如果沒有消費者接收的話,消息會丟失.
還有一種方式消息不會丟失,將消息存在列表裏面。首先生產者向列表投入數據,緊接着去通知訂閱者,讓訂閱者從列表中取出數據。但是有一個弊端,如果有多個消費者訂閱時,只有一個消費者能取到數據。代碼如下:
生產者:
var redis = new RedisContent(); var sub = RedisContent.redis.GetSubscriber(); for (int i = 0; i < 10; i++) { redis.db.ListLeftPush("datalist", $"data{i}", flags: CommandFlags.FireAndForget); sub.PublishAsync("channel1", "").GetAwaiter(); Console.WriteLine($"{DateTime.Now} 已投遞消息data{i}!"); Thread.Sleep(3000); } Console.ReadKey();
消費者:
static void Main(string[] args) { var redis = new RedisContent(); var sub = RedisContent.redis.GetSubscriber();
//如果消費者後啓動,或者宕機重啓,要先查詢列表中是否有數據,如果有數據要消費掉 var len = redis.db.ListRange("datalist").Length; if (len > 0) { Task.Run(() => { for (int i = 0; i < len; i++) { string result = redis.db.ListRightPop("datalist"); //業務操作... } }); } sub.Subscribe("channel1", (channel, message) => { string result = redis.db.ListRightPop("datalist"); Console.WriteLine($"{DateTime.Now} 已接收消息,Message={result}"); Thread.Sleep(1000); }); Console.WriteLine("消費者0已啓動成功!"); Console.ReadKey(); }
如有不足,請見諒!今天是十月二號,昨天是中秋佳節又是國慶,在這裏祝大家雙節快樂。本來昨天晚上寫完這篇的,但是八九點的時候太困了,就😴... ...
昨晚坐在窗邊,偶然間向外面瞄了一眼,月亮的光芒太耀眼了,也太漂亮了,趕緊拍了一張🌕