详解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/1_kEMCtbf2iT5pLV7irxR5Q 提取码: v4sr
此文由博主精心撰写转载请保留此原文链接:https://www.cnblogs.com/xyh9039/p/14022264.html
到此这篇关于详解Redis中的List类型的文章就介绍到这了,更多相关Redis List类型内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!