Redis分佈式緩存系列(六)- Redis中的List類型

本系列將和你們分享Redis分佈式緩存,本章主要簡單介紹下Redis中的List類型,以及如何使用Redis解決博客數據分頁、生產者消費者模型和發佈訂閱等問題。html

Redis List的實現爲一個雙向鏈表,便可以支持反向查找和遍歷,更方便操做,不過帶來了部分額外的內存開銷,Redis內部的不少實現,包括髮送緩衝隊列等也都是用這個數據結構。  redis

List類型主要用於隊列和棧,先進先出,後進先出等。數據庫

存儲形式:key--LinkList<value>編程

首先先給你們Show一波Redis中與List類型相關的API:設計模式

using System;
using System.Collections.Generic;
using ServiceStack.Redis;

namespace TianYa.Redis.Service
{
    /// <summary>
    /// Redis List的實現爲一個雙向鏈表,便可以支持反向查找和遍歷,更方便操做,不過帶來了部分額外的內存開銷,
    /// Redis內部的不少實現,包括髮送緩衝隊列等也都是用這個數據結構。  
    /// </summary>
    public class RedisListService : RedisBase
    {
        #region Queue隊列(先進先出)

        /// <summary>
        /// 入隊
        /// </summary>
        /// <param name="listId">集合Id</param>
        /// <param name="value">入隊的值</param>
        public void EnqueueItemOnList(string listId, string value)
        {
            base._redisClient.EnqueueItemOnList(listId, value);
        }

        /// <summary>
        /// 出隊
        /// </summary>
        /// <param name="listId">集合Id</param>
        /// <returns>出隊的值</returns>
        public string DequeueItemFromList(string listId)
        {
            return base._redisClient.DequeueItemFromList(listId);
        }

        /// <summary>
        /// 出隊(阻塞)
        /// </summary>
        /// <param name="listId">集合Id</param>
        /// <param name="timeOut">阻塞時間(超時時間)</param>
        /// <returns>出隊的值</returns>
        public string BlockingDequeueItemFromList(string listId, TimeSpan? timeOut)
        {
            return base._redisClient.BlockingDequeueItemFromList(listId, timeOut);
        }

        /// <summary>
        /// 從多個list中出隊(阻塞)
        /// </summary>
        /// <param name="listIds">集合Id</param>
        /// <param name="timeOut">阻塞時間(超時時間)</param>
        /// <returns>返回出隊的 listId & Item</returns>
        public ItemRef BlockingDequeueItemFromLists(string[] listIds, TimeSpan? timeOut)
        {
            return base._redisClient.BlockingDequeueItemFromLists(listIds, timeOut);
        }

        #endregion Queue隊列(先進先出)

        #region Stack棧(後進先出)

        /// <summary>
        /// 入棧
        /// </summary>
        /// <param name="listId">集合Id</param>
        /// <param name="value">入棧的值</param>
        public void PushItemToList(string listId, string value)
        {
            base._redisClient.PushItemToList(listId, value);
        }

        /// <summary>
        /// 入棧,並設置過時時間
        /// </summary>
        /// <param name="listId">集合Id</param>
        /// <param name="value">入棧的值</param>
        /// <param name="expireAt">過時時間</param>
        public void PushItemToList(string listId, string value, DateTime expireAt)
        {
            base._redisClient.PushItemToList(listId, value);
            base._redisClient.ExpireEntryAt(listId, expireAt);
        }

        /// <summary>
        /// 入棧,並設置過時時間
        /// </summary>
        /// <param name="listId">集合Id</param>
        /// <param name="value">入棧的值</param>
        /// <param name="expireIn">過時時間</param>
        public void PushItemToList(string listId, string value, TimeSpan expireIn)
        {
            base._redisClient.PushItemToList(listId, value);
            base._redisClient.ExpireEntryIn(listId, expireIn);
        }

        /// <summary>
        /// 出棧
        /// </summary>
        /// <param name="listId">集合Id</param>
        /// <returns>出棧的值</returns>
        public string PopItemFromList(string listId)
        {
            return base._redisClient.PopItemFromList(listId);
        }

        /// <summary>
        /// 出棧(阻塞)
        /// </summary>
        /// <param name="listId">集合Id</param>
        /// <param name="timeOut">阻塞時間(超時時間)</param>
        /// <returns>出棧的值</returns>
        public string BlockingPopItemFromList(string listId, TimeSpan? timeOut)
        {
            return base._redisClient.BlockingPopItemFromList(listId, timeOut);
        }

        /// <summary>
        /// 從多個list中出棧一個值(阻塞)
        /// </summary>
        /// <param name="listIds">集合Id</param>
        /// <param name="timeOut">阻塞時間(超時時間)</param>
        /// <returns>返回出棧的 listId & Item</returns>
        public ItemRef BlockingPopItemFromLists(string[] listIds, TimeSpan? timeOut)
        {
            return base._redisClient.BlockingPopItemFromLists(listIds, timeOut);
        }

        /// <summary>
        /// 從fromListId集合出棧併入棧到toListId集合
        /// </summary>
        /// <param name="fromListId">出棧集合Id</param>
        /// <param name="toListId">入棧集合Id</param>
        /// <returns>返回移動的值</returns>
        public string PopAndPushItemBetweenLists(string fromListId, string toListId)
        {
            return base._redisClient.PopAndPushItemBetweenLists(fromListId, toListId);
        }

        /// <summary>
        /// 從fromListId集合出棧併入棧到toListId集合(阻塞)
        /// </summary>
        /// <param name="fromListId">出棧集合Id</param>
        /// <param name="toListId">入棧集合Id</param>
        /// <param name="timeOut">阻塞時間(超時時間)</param>
        /// <returns>返回移動的值</returns>
        public string BlockingPopAndPushItemBetweenLists(string fromListId, string toListId, TimeSpan? timeOut)
        {
            return base._redisClient.BlockingPopAndPushItemBetweenLists(fromListId, toListId, timeOut);
        }

        #endregion Stack棧(後進先出)

        #region 賦值

        /// <summary>
        /// 向list頭部添加value值
        /// </summary>
        public void PrependItemToList(string listId, string value)
        {
            base._redisClient.PrependItemToList(listId, value);
        }

        /// <summary>
        /// 向list頭部添加value值,並設置過時時間
        /// </summary>    
        public void PrependItemToList(string listId, string value, DateTime expireAt)
        {
            base._redisClient.PrependItemToList(listId, value);
            base._redisClient.ExpireEntryAt(listId, expireAt);
        }

        /// <summary>
        /// 向list頭部添加value值,並設置過時時間
        /// </summary>        
        public void PrependItemToList(string listId, string value, TimeSpan expireIn)
        {
            base._redisClient.PrependItemToList(listId, value);
            base._redisClient.ExpireEntryIn(listId, expireIn);
        }

        /// <summary>
        /// 向list中添加value值
        /// </summary>     
        public void AddItemToList(string listId, string value)
        {
            base._redisClient.AddItemToList(listId, value);
        }

        /// <summary>
        /// 向list中添加value值,並設置過時時間
        /// </summary>  
        public void AddItemToList(string listId, string value, DateTime expireAt)
        {
            base._redisClient.AddItemToList(listId, value);
            base._redisClient.ExpireEntryAt(listId, expireAt);
        }

        /// <summary>
        /// 向list中添加value值,並設置過時時間
        /// </summary>  
        public void AddItemToList(string listId, string value, TimeSpan expireIn)
        {
            base._redisClient.AddItemToList(listId, value);
            base._redisClient.ExpireEntryIn(listId, expireIn);
        }

        /// <summary>
        /// 向list中添加多個value值
        /// </summary>  
        public void AddRangeToList(string listId, List<string> values)
        {
            base._redisClient.AddRangeToList(listId, values);
        }

        /// <summary>
        /// 向list中添加多個value值,並設置過時時間
        /// </summary>  
        public void AddRangeToList(string listId, List<string> values, DateTime expireAt)
        {
            base._redisClient.AddRangeToList(listId, values);
            base._redisClient.ExpireEntryAt(listId, expireAt);
        }

        /// <summary>
        /// 向list中添加多個value值,並設置過時時間
        /// </summary>  
        public void AddRangeToList(string listId, List<string> values, TimeSpan expireIn)
        {
            base._redisClient.AddRangeToList(listId, values);
            base._redisClient.ExpireEntryIn(listId, expireIn);
        }

        #endregion 賦值

        #region 獲取值

        /// <summary>
        /// 獲取指定list中包含的數據數量
        /// </summary>  
        public long GetListCount(string listId)
        {
            return base._redisClient.GetListCount(listId);
        }

        /// <summary>
        /// 獲取指定list中包含的全部數據集合
        /// </summary>  
        public List<string> GetAllItemsFromList(string listId)
        {
            return base._redisClient.GetAllItemsFromList(listId);
        }

        /// <summary>
        /// 獲取指定list中下標從startingFrom到endingAt的值集合
        /// </summary>  
        public List<string> GetRangeFromList(string listId, int startingFrom, int endingAt)
        {
            return base._redisClient.GetRangeFromList(listId, startingFrom, endingAt);
        }

        #endregion 獲取值

        #region 刪除

        /// <summary>
        /// 移除指定list中,listId/value,與參數相同的值,並返回移除的數量
        /// </summary>  
        public long RemoveItemFromList(string listId, string value)
        {
            return base._redisClient.RemoveItemFromList(listId, value);
        }

        /// <summary>
        /// 從指定list的尾部移除一個數據,並返回移除的數據
        /// </summary>  
        public string RemoveEndFromList(string listId)
        {
            return base._redisClient.RemoveEndFromList(listId);
        }

        /// <summary>
        /// 從指定list的頭部移除一個數據,並返回移除的數據
        /// </summary>  
        public string RemoveStartFromList(string listId)
        {
            return base._redisClient.RemoveStartFromList(listId);
        }

        #endregion 刪除

        #region 其它

        /// <summary>
        /// 清理數據,保持list長度
        /// </summary>
        /// <param name="listId">集合Id</param>
        /// <param name="keepStartingFrom">保留起點</param>
        /// <param name="keepEndingAt">保留終點</param>
        public void TrimList(string listId, int keepStartingFrom, int keepEndingAt)
        {
            base._redisClient.TrimList(listId, keepStartingFrom, keepEndingAt);
        }

        #endregion 其它

        #region 發佈訂閱

        /// <summary>
        /// 發佈
        /// </summary>
        /// <param name="channel">頻道</param>
        /// <param name="message">消息</param>
        public void Publish(string channel, string message)
        {
            base._redisClient.PublishMessage(channel, message);
        }

        /// <summary>
        /// 訂閱
        /// </summary>
        /// <param name="channel">頻道</param>
        /// <param name="actionOnMessage"></param>
        public void Subscribe(string channel, Action<string, string, IRedisSubscription> actionOnMessage)
        {
            var subscription = base._redisClient.CreateSubscription();
            subscription.OnSubscribe = c =>
            {
                Console.WriteLine($"訂閱頻道{c}");
                Console.WriteLine();
            };
            //取消訂閱
            subscription.OnUnSubscribe = c =>
            {
                Console.WriteLine($"取消訂閱 {c}");
                Console.WriteLine();
            };
            subscription.OnMessage += (c, s) =>
            {
                actionOnMessage(c, s, subscription);
            };
            Console.WriteLine($"開始啓動監聽 {channel}");
            subscription.SubscribeToChannels(channel); //blocking
        }

        /// <summary>
        /// 取消訂閱
        /// </summary>
        /// <param name="channel">頻道</param>
        public void UnSubscribeFromChannels(string channel)
        {
            var subscription = base._redisClient.CreateSubscription();
            subscription.UnSubscribeFromChannels(channel);
        }

        #endregion 發佈訂閱
    }
}

使用以下:緩存

/// <summary>
/// Redis List的實現爲一個雙向鏈表,便可以支持反向查找和遍歷,更方便操做,不過帶來了部分額外的內存開銷,
/// Redis內部的不少實現,包括髮送緩衝隊列等也都是用這個數據結構。  
/// 隊列/棧/生產者消費者模型/發佈訂閱
/// </summary>
public static void ShowList()
{
    using (RedisListService service = new RedisListService())
    {
        service.FlushAll();
        service.AddItemToList("article", "張三");
        service.AddItemToList("article", "李四");
        service.AddItemToList("article", "王五");
        service.PrependItemToList("article", "趙六");
        service.PrependItemToList("article", "錢七");

        var result1 = service.GetAllItemsFromList("article"); //一次性獲取全部的數據
        var result2 = service.GetRangeFromList("article", 0, 3); //能夠按照添加順序自動排序,並且能夠分頁獲取
        Console.WriteLine($"result1={JsonConvert.SerializeObject(result1)}");
        Console.WriteLine($"result2={JsonConvert.SerializeObject(result2)}");

        Console.WriteLine("=====================================================");

        //棧:後進先出
        service.FlushAll();
        service.PushItemToList("article", "張三"); //入棧
        service.PushItemToList("article", "李四");
        service.PushItemToList("article", "王五");
        service.PushItemToList("article", "趙六");
        service.PushItemToList("article", "錢七");

        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine(service.PopItemFromList("article")); //出棧
        }

        Console.WriteLine("=====================================================");

        //隊列:先進先出,生產者消費者模型   
        //MSMQ---RabbitMQ---ZeroMQ---RedisList 學習成本、技術成本
        service.FlushAll();
        service.EnqueueItemOnList("article", "張三"); //入隊
        service.EnqueueItemOnList("article", "李四");
        service.EnqueueItemOnList("article", "王五");
        service.EnqueueItemOnList("article", "趙六");
        service.EnqueueItemOnList("article", "錢七");

        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine(service.DequeueItemFromList("article")); //出隊
        }
        //分佈式緩存,多服務器均可以訪問到,多個生產者,多個消費者,任何產品只被消費一次
    }
}

運行結果以下所示:服務器

下面咱們就來看下如何使用上面的API來解決一些具體的問題:微信

1、博客數據分頁

應用場景:數據結構

  博客網站天天新增的隨筆和文章可能都是幾千幾萬的,表裏面是幾千萬數據。首頁要展現最新的隨筆,還有前20頁是不少人訪問的。異步

  這種狀況下若是首頁分頁數據每次都去查詢數據庫,那麼就會有很大的性能問題。

解決方案:

  每次寫入數據庫的時候,把 ID_標題 寫入到Redis的List中(後面搞個TrimList,只要最近的200個)。

  這樣的話用戶每次刷頁面就不須要去訪問數據庫了,直接讀取Redis中的數據。

  第一頁(固然也能夠是前幾頁)的時候能夠不體現總記錄數,只拿最新數據展現,這樣就能避免訪問數據庫了。

 

還有一種就是水平分表了,數據存到Redis的時候能夠保存 ID_表名稱_標題

使用List主要是解決數據量大,變化快的數據分頁問題

二八原則:80%的訪問集中在20%的數據,List裏面只用保存大概的量就夠用了。

using TianYa.Redis.Service;

namespace MyRedis.Scene
{
    /// <summary>
    /// 博客數據分頁
    /// 
    /// 應用場景:
    ///     博客網站天天新增的隨筆和文章可能都是幾千幾萬的,表裏面是幾千萬數據。首頁要展現最新的隨筆,還有前20頁是不少人訪問的。
    ///     這種狀況下若是首頁分頁數據每次都去查詢數據庫,那麼就會有很大的性能問題。
    /// 
    /// 解決方案:
    ///     每次寫入數據庫的時候,把 ID_標題 寫入到Redis的List中(後面搞個TrimList,只要最近的200個)。
    ///     這樣的話用戶每次刷頁面就不須要去訪問數據庫了,直接讀取Redis中的數據。
    ///     第一頁(固然也能夠是前幾頁)的時候能夠不體現總記錄數,只拿最新數據展現,這樣就能避免訪問數據庫了。
    /// 
    /// 還有一種就是水平分表了,數據存到Redis的時候能夠保存 ID_表名稱_標題
    /// 
    /// 使用List主要是解決數據量大,變化快的數據分頁問題。
    /// 二八原則:80%的訪問集中在20%的數據,List裏面只用保存大概的量就夠用了。
    /// </summary>
    public class BlogPageList
    {
        public static void Show()
        {
            using (RedisListService service = new RedisListService())
            {
                service.AddItemToList("newBlog", "10001_IOC容器的實現原理");
                service.AddItemToList("newBlog", "10002_AOP面向切面編程");
                service.AddItemToList("newBlog", "10003_行爲型設計模式");
                service.AddItemToList("newBlog", "10004_結構型設計模式");
                service.AddItemToList("newBlog", "10005_建立型設計模式");
                service.AddItemToList("newBlog", "10006_GC垃圾回收");

                service.TrimList("newBlog", 0, 200); //保留最新的201個(一個List最多隻能存放2的32次方-1個)
                var result1 = service.GetRangeFromList("newBlog", 0, 9); //第一頁
                var result2 = service.GetRangeFromList("newBlog", 10, 19); //第二頁
                var result3 = service.GetRangeFromList("newBlog", 20, 29); //第三頁
            }
        }
    }
}

2、生產者消費者模型

分佈式緩存,多服務器均可以訪問到,多個生產者,多個消費者,任何產品只被消費一次。(使用隊列實現)

其中一個(或多個)程序寫入,另一個(或多個)程序讀取消費。按照時間順序,數據失敗了還能夠放回去下次重試。

下面咱們來看個例子:

Demo中添加了2個控制檯應用程序,分別模擬生產者和消費者:

using System;
using TianYa.Redis.Service;

namespace TianYa.Producer
{
    /// <summary>
    /// 模擬生產者
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("生產者程序啓動了。。。");
            using (RedisListService service = new RedisListService())
            {
                Console.WriteLine("開始生產test產品");
                for (int i = 1; i <= 20; i++)
                {
                    service.EnqueueItemOnList("test", $"產品test{i}");
                }

                Console.WriteLine("開始生產task產品");
                for (int i = 1; i <= 20; i++)
                {
                    service.EnqueueItemOnList("task", $"產品task{i}");
                }
                Console.WriteLine("模擬生產結束");

                while (true)
                {
                    Console.WriteLine("************請輸入數據************");
                    string testTask = Console.ReadLine();
                    service.EnqueueItemOnList("test", testTask);
                }
            }
        }
    }
}
using System;
using System.Threading;
using TianYa.Redis.Service;

namespace TianYa.Consumer
{
    /// <summary>
    /// 模擬消費者
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("消費者程序啓動了。。。");
            using (RedisListService service = new RedisListService())
            {
                while (true)
                {
                    var result = service.BlockingDequeueItemFromLists(new string[] { "test", "task" }, TimeSpan.FromHours(1));
                    Thread.Sleep(100);
                    Console.WriteLine($"消費者消費了 {result.Id} {result.Item}");
                }
            }
        }
    }
}

接下來咱們使用.NET Core CLI來啓動2個消費者實例和1個生產者實例,運行結果以下所示:

像這種異步隊列在項目中有什麼價值呢?

PS:此處事務是一個很大問題,真實項目中需根據實際狀況決定是否採用異步隊列。

3、發佈訂閱

發佈訂閱:

  發佈一個數據,所有的訂閱者都能收到。

  觀察者,一個數據源,多個接收者,只要訂閱了就能夠收到的,能被多個數據源共享。

  觀察者模式:微信訂閱號---羣聊天---數據同步。。。

下面咱們來看個小Demo:

/// <summary>
/// 發佈訂閱
///     發佈一個數據,所有的訂閱者都能收到。
///     觀察者,一個數據源,多個接收者,只要訂閱了就能夠收到的,能被多個數據源共享。
///     觀察者模式:微信訂閱號---羣聊天---數據同步。。。
/// </summary>
public static void ShowPublishAndSubscribe()
{
    Task.Run(() =>
    {
        using (RedisListService service = new RedisListService())
        {
            service.Subscribe("TianYa", (c, message, iRedisSubscription) =>
            {
                Console.WriteLine($"註冊{1}{c}:{message},Dosomething else");
                if (message.Equals("exit"))
                    iRedisSubscription.UnSubscribeFromChannels("TianYa");
            });//blocking
        }
    });
    Task.Run(() =>
    {
        using (RedisListService service = new RedisListService())
        {
            service.Subscribe("TianYa", (c, message, iRedisSubscription) =>
            {
                Console.WriteLine($"註冊{2}{c}:{message},Dosomething else");
                if (message.Equals("exit"))
                    iRedisSubscription.UnSubscribeFromChannels("TianYa");
            });//blocking
        }
    });
    Task.Run(() =>
    {
        using (RedisListService service = new RedisListService())
        {
            service.Subscribe("Twelve", (c, message, iRedisSubscription) =>
            {
                Console.WriteLine($"註冊{3}{c}:{message},Dosomething else");
                if (message.Equals("exit"))
                    iRedisSubscription.UnSubscribeFromChannels("Twelve");
            });//blocking
        }
    });
    using (RedisListService service = new RedisListService())
    {
        Thread.Sleep(1000);
        service.Publish("TianYa", "TianYa1");
        Thread.Sleep(1000);
        service.Publish("TianYa", "TianYa2");
        Thread.Sleep(1000);
        service.Publish("TianYa", "TianYa3");

        Thread.Sleep(1000);
        service.Publish("Twelve", "Twelve1");
        Thread.Sleep(1000);
        service.Publish("Twelve", "Twelve2");
        Thread.Sleep(1000);
        service.Publish("Twelve", "Twelve3");

        Thread.Sleep(1000);
        Console.WriteLine("**********************************************");

        Thread.Sleep(1000);
        service.Publish("TianYa", "exit");
        Thread.Sleep(1000);
        service.Publish("TianYa", "TianYa6");
        Thread.Sleep(1000);
        service.Publish("TianYa", "TianYa7");
        Thread.Sleep(1000);
        service.Publish("TianYa", "TianYa8");

        Thread.Sleep(1000);
        service.Publish("Twelve", "exit");
        Thread.Sleep(1000);
        service.Publish("Twelve", "Twelve6");
        Thread.Sleep(1000);
        service.Publish("Twelve", "Twelve7");
        Thread.Sleep(1000);
        service.Publish("Twelve", "Twelve8");

        Thread.Sleep(1000);
        Console.WriteLine("結束");
    }
}

運行結果以下所示:

至此本文就所有介紹完了,若是以爲對您有所啓發請記得點個贊哦!!!

 

Demo源碼:

連接:https://pan.baidu.com/s/1B_XUM4Eqc81CJdjufOWS9A 
提取碼:a78n

此文由博主精心撰寫轉載請保留此原文連接:https://www.cnblogs.com/xyh9039/p/14022264.html

相關文章
相關標籤/搜索