温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

怎么在Redis中利用List实现一个双向链表

发布时间:2020-11-27 14:52:09 来源:亿速云 阅读:200 作者:Leah 栏目:开发技术

本篇文章为大家展示了怎么在Redis中利用List实现一个双向链表,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

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

Redis List的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用这个数据结构。

List类型主要用于队列和栈,先进先出,后进先出等。

存储形式:key--LinkList<value>

怎么在Redis中利用List实现一个双向链表

首先先给大家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")); //出队   }   //分布式缓存,多服务器都可以访问到,多个生产者,多个消费者,任何产品只被消费一次  } }

运行结果如下所示:

怎么在Redis中利用List实现一个双向链表

下面我们就来看下如何使用上面的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); //第三页    }   }  } }

二、生产者消费者模型

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

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

下面我们来看个例子:

怎么在Redis中利用List实现一个双向链表

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个生产者实例,运行结果如下所示:

怎么在Redis中利用List实现一个双向链表

像这种异步队列在项目中有什么价值呢?

怎么在Redis中利用List实现一个双向链表

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("结束");  } }

运行结果如下所示:

怎么在Redis中利用List实现一个双向链表

上述内容就是怎么在Redis中利用List实现一个双向链表,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注亿速云行业资讯频道。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI