Redis分布式缓存系列(六)- Redis中的List类型

本系列将和大家分享Redis分布式缓存,本章主要简单介绍下Redis中的List类型,以及如何使用Redis解决博客数据分页、生产者消费者模型和发布订阅等问题。

Redis List的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,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来解决一些具体的问题:

一、博客数据分页

应用场景:

  博客网站每天新增的随笔和文章可能都是几千几万的,表里面是几千万数据。首页要展示最新的随笔,还有前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); //第三页
            }
        }
    }
}

二、生产者消费者模型

分布式缓存,多服务器都可以访问到,多个生产者,多个消费者,任何产品只被消费一次。(使用队列实现)

其中一个(或多个)程序写入,另外一个(或多个)程序读取消费。按照时间顺序,数据失败了还可以放回去下次重试。

下面我们来看个例子:

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:此处事务是一个很大问题,真实项目中需根据实际情况决定是否采用异步队列。

三、发布订阅

发布订阅:

  发布一个数据,全部的订阅者都能收到。

  观察者,一个数据源,多个接收者,只要订阅了就可以收到的,能被多个数据源共享。

  观察者模式:微信订阅号---群聊天---数据同步。。。

下面我们来看个小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

版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!

posted @ 2020-11-25 22:05  谢友海  阅读(1328)  评论(0编辑  收藏  举报