使用Redis做訊息佇列

江北、發表於2020-10-02

基於記憶體的單執行緒資料庫,使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();

如有不足,請見諒!今天是十月二號,昨天是中秋佳節又是國慶,在這裡祝大家雙節快樂。本來昨天晚上寫完這篇的,但是八九點的時候太困了,就?... ...

昨晚坐在窗邊,偶然間向外面瞄了一眼,月亮的光芒太耀眼了,也太漂亮了,趕緊拍了一張?

相關文章