c# redis系列三
List存储
链表:存储非紧密摆放 修改新增方便 查询性能稍慢
封装的List存储的redis类:
/// <summary> /// Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销, /// Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。 /// </summary> public class RedisListService : RedisBase { #region 赋值 /// <summary> /// 从左侧向list中添加值 /// </summary> public void LPush(string key, string value) { base.iClient.PushItemToList(key, value); } /// <summary> /// 从左侧向list中添加值,并设置过期时间 /// </summary> public void LPush(string key, string value, DateTime dt) { base.iClient.PushItemToList(key, value); base.iClient.ExpireEntryAt(key, dt); } /// <summary> /// 从左侧向list中添加值,设置过期时间 /// </summary> public void LPush(string key, string value, TimeSpan sp) { base.iClient.PushItemToList(key, value); base.iClient.ExpireEntryIn(key, sp); } /// <summary> /// 从右侧向list中添加值 /// </summary> public void RPush(string key, string value) { base.iClient.PrependItemToList(key, value); } /// <summary> /// 从右侧向list中添加值,并设置过期时间 /// </summary> public void RPush(string key, string value, DateTime dt) { base.iClient.PrependItemToList(key, value); base.iClient.ExpireEntryAt(key, dt); } /// <summary> /// 从右侧向list中添加值,并设置过期时间 /// </summary> public void RPush(string key, string value, TimeSpan sp) { base.iClient.PrependItemToList(key, value); base.iClient.ExpireEntryIn(key, sp); } /// <summary> /// 添加key/value /// </summary> public void Add(string key, string value) { base.iClient.AddItemToList(key, value); } /// <summary> /// 添加key/value ,并设置过期时间 /// </summary> public void Add(string key, string value, DateTime dt) { base.iClient.AddItemToList(key, value); base.iClient.ExpireEntryAt(key, dt); } /// <summary> /// 添加key/value。并添加过期时间 /// </summary> public void Add(string key, string value, TimeSpan sp) { base.iClient.AddItemToList(key, value); base.iClient.ExpireEntryIn(key, sp); } /// <summary> /// 为key添加多个值 /// </summary> public void Add(string key, List<string> values) { base.iClient.AddRangeToList(key, values); } /// <summary> /// 为key添加多个值,并设置过期时间 /// </summary> public void Add(string key, List<string> values, DateTime dt) { base.iClient.AddRangeToList(key, values); base.iClient.ExpireEntryAt(key, dt); } /// <summary> /// 为key添加多个值,并设置过期时间 /// </summary> public void Add(string key, List<string> values, TimeSpan sp) { base.iClient.AddRangeToList(key, values); base.iClient.ExpireEntryIn(key, sp); } #endregion #region 获取值 /// <summary> /// 获取list中key包含的数据数量 /// </summary> public long Count(string key) { return base.iClient.GetListCount(key); } /// <summary> /// 获取key包含的所有数据集合 /// </summary> public List<string> Get(string key) { return base.iClient.GetAllItemsFromList(key); } /// <summary> /// 获取key中下标为star到end的值集合 /// </summary> public List<string> Get(string key, int star, int end) { return base.iClient.GetRangeFromList(key, star, end); } #endregion #region 阻塞命令 /// <summary> /// 阻塞命令:从list为key的尾部移除一个值,并返回移除的值,阻塞时间为sp /// </summary> public string BlockingPopItemFromList(string key, TimeSpan? sp) { return base.iClient.BlockingPopItemFromList(key, sp); } /// <summary> /// 阻塞命令:从多个list中尾部移除一个值,并返回移除的值&key,阻塞时间为sp /// </summary> public ItemRef BlockingPopItemFromLists(string[] keys, TimeSpan? sp) { return base.iClient.BlockingPopItemFromLists(keys, sp); } /// <summary> /// 阻塞命令:从list中keys的尾部移除一个值,并返回移除的值,阻塞时间为sp /// </summary> public string BlockingDequeueItemFromList(string key, TimeSpan? sp) { return base.iClient.BlockingDequeueItemFromList(key, sp); } /// <summary> /// 阻塞命令:从多个list中尾部移除一个值,并返回移除的值&key,阻塞时间为sp /// </summary> public ItemRef BlockingDequeueItemFromLists(string[] keys, TimeSpan? sp) { return base.iClient.BlockingDequeueItemFromLists(keys, sp); } /// <summary> /// 阻塞命令:从list中一个fromkey的尾部移除一个值,添加到另外一个tokey的头部,并返回移除的值,阻塞时间为sp /// </summary> public string BlockingPopAndPushItemBetweenLists(string fromkey, string tokey, TimeSpan? sp) { return base.iClient.BlockingPopAndPushItemBetweenLists(fromkey, tokey, sp); } #endregion #region 删除 /// <summary> /// 从尾部移除数据,返回移除的数据 /// </summary> public string PopItemFromList(string key) { var sa = base.iClient.CreateSubscription(); return base.iClient.PopItemFromList(key); } /// <summary> /// 从尾部移除数据,返回移除的数据 /// </summary> public string DequeueItemFromList(string key) { return base.iClient.DequeueItemFromList(key); } /// <summary> /// 移除list中,key/value,与参数相同的值,并返回移除的数量 /// </summary> public long RemoveItemFromList(string key, string value) { return base.iClient.RemoveItemFromList(key, value); } /// <summary> /// 从list的尾部移除一个数据,返回移除的数据 /// </summary> public string RemoveEndFromList(string key) { return base.iClient.RemoveEndFromList(key); } /// <summary> /// 从list的头部移除一个数据,返回移除的值 /// </summary> public string RemoveStartFromList(string key) { return base.iClient.RemoveStartFromList(key); } #endregion #region 其它 /// <summary> /// 从一个list的尾部移除一个数据,添加到另外一个list的头部,并返回移动的值 /// </summary> public string PopAndPushItemBetweenLists(string fromKey, string toKey) { return base.iClient.PopAndPushItemBetweenLists(fromKey, toKey); } /// <summary> /// 清理数据,保持list长度 /// </summary> /// <param name="key"></param> /// <param name="start">起点</param> /// <param name="end">终结点</param> public void TrimList(string key, int start, int end) { base.iClient.TrimList(key, start, end); } #endregion #region 发布订阅 public void Publish(string channel, string message) { base.iClient.PublishMessage(channel, message);//取消订阅 } public void Subscribe(string channel, Action<string, string, IRedisSubscription> actionOnMessage) { var subscription = base.iClient.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 } public void UnSubscribeFromChannels(string channel) { var subscription = base.iClient.CreateSubscription(); subscription.UnSubscribeFromChannels(channel); } #endregion }
下面我们进行一些简单的api的调用展示:
using (RedisListService service = new RedisListService()) { service.FlushAll(); service.Add("article", "Richard1234"); service.Add("article", "kevin"); service.Add("article", "大叔"); service.Add("article", "C卡"); service.Add("article", "触不到的线"); service.Add("article", "程序错误"); service.FlushAll(); service.LPush("article", "Richard1234");//头部插入 service.LPush("article", "kevin"); service.LPush("article", "大叔"); service.LPush("article", "C卡"); service.LPush("article", "触不到的线"); service.LPush("article", "程序错误"); service.FlushAll(); service.RPush("article", "Richard1234");//尾部插入 service.RPush("article", "kevin"); service.RPush("article", "大叔"); service.RPush("article", "C卡"); service.RPush("article", "触不到的线"); service.RPush("article", "程序错误"); var result11 = service.Get("article"); //获取索引0到3的数据,包括0,3 注意list存储中没有索引,这里的索引是经过内部处理之后给外界显示的。 var result2 = service.Get("article", 0, 3); //限制article中数据最大有多少 service.TrimList("article", 0, 3); service.FlushAll(); ////队列:生产者消费者模型 service.Add("article", "Richard1234"); service.Add("article", "kevin"); service.Add("article", "大叔"); service.Add("article", "C卡"); service.Add("article", "触不到的线"); service.Add("article", "程序错误"); for (int i = 0; i < 5; i++) { //PopItemFromList 从尾部移除数据,每次移除一个 Console.WriteLine(service.PopItemFromList("article")); var result1 = service.Get("article"); } }
这是redis可视化工具看到的具体数据:
ServiceStack程序集中有一个GetRangeFromList(string listId, int startingFrom, int endingAt)方法,根据这个方法可以实现分页。比如说知乎,每天都有几万条问答的数据进入到平台,如果我每次都去数据里去查询;数据库肯定是扛不住,可以通过List 来存储。将key存储一个文章的id(可以是数据库中的主键),value存储Id_标题,首页获取最新的数据,我就可以通过前20条数据,至于详情就可以根据主键到数据库中查询,减少数据库压力。
消息队列
它的List存储天生支持消息队列。
一般来说,消息队列有两种场景,一种是发布者订阅者模式,一种是生产者消费者模式。利用redis这两种场景的消息队列都能够实现。
定义:
生产者消费者模式:生产者生产消息放到队列里,多个消费者同时监听队列,谁先抢到消息谁就会从队列中取走消息;即对于每个消息只能被最多一个消费者拥有。
发布者订阅者模式:发布者生产消息放到队列里,多个监听队列的消费者都会收到同一份消息;即正常情况下每个消费者收到的消息应该都是一样的。
生产者消费者模式
一个进程在向Redis写入数据,可以来多个进程在Redis 里面去获取数据;
生产者进程:
#region 生产者消费者 //一个进程在向Redis写入数据 //可以来多个进程在Redis 里面去获取数据; using (RedisListService service = new RedisListService()) { service.FlushAll(); List<string> stringList = new List<string>(); for (int i = 0; i < 10; i++) { stringList.Add(string.Format($"放入任务{i}")); } service.Add("test", "task1 这是一个学生Add1"); service.Add("test", "task1 这是一个学生Add2"); service.Add("test", "task1 这是一个学生Add3"); service.LPush("test", "task1 这是一个学生LPush1"); service.LPush("test", "task1 这是一个学生LPush2"); service.LPush("test", "task1 这是一个学生LPush3"); service.LPush("test", "task1 这是一个学生LPush4"); service.LPush("test", "task1 这是一个学生LPush5"); service.LPush("test", "task1 这是一个学生LPush6"); service.RPush("test", "task1 这是一个学生RPush1"); service.RPush("test", "task1 这是一个学生RPush2"); service.RPush("test", "task1 这是一个学生RPush3"); service.RPush("test", "task1 这是一个学生RPush4"); service.RPush("test", "task1 这是一个学生RPush5"); service.RPush("test", "task1 这是一个学生RPush6"); service.Add("test", "task2 这是一个学生Add1"); service.Add("test", "task2 这是一个学生Add2"); service.Add("test", "task2 这是一个学生Add3"); service.LPush("test", "task2 这是一个学生LPush1"); service.LPush("test", "task2 这是一个学生LPush2"); service.LPush("test", "task2 这是一个学生LPush3"); service.LPush("test", "task2 这是一个学生LPush4"); service.LPush("test", "task2 这是一个学生LPush5"); service.LPush("test", "task2 这是一个学生LPush6"); service.RPush("test", "task2 这是一个学生RPush1"); service.RPush("test", "task2 这是一个学生RPush2"); service.RPush("test", "task2 这是一个学生RPush3"); service.RPush("test", "task2 这是一个学生RPush4"); service.RPush("test", "task2 这是一个学生RPush5"); service.RPush("test", "task2 这是一个学生RPush6"); service.Add("test", "这是一个学生Add1"); service.Add("test", "这是一个学生Add2"); service.Add("test", "这是一个学生Add3"); service.LPush("test", "这是一个学生LPush1"); service.LPush("test", "这是一个学生LPush2"); service.LPush("test", "这是一个学生LPush3"); service.LPush("test", "这是一个学生LPush4"); service.LPush("test", "这是一个学生LPush5"); service.LPush("test", "这是一个学生LPush6"); service.RPush("test", "这是一个学生RPush1"); service.RPush("test", "这是一个学生RPush2"); service.RPush("test", "这是一个学生RPush3"); service.RPush("test", "这是一个学生RPush4"); service.RPush("test", "这是一个学生RPush5"); service.RPush("test", "这是一个学生RPush6"); service.Add("task", stringList); Console.WriteLine(service.Count("test")); Console.WriteLine(service.Count("task")); var list = service.Get("test"); list = service.Get("task", 2, 4); Action act = new Action(() => { while (true) { Console.WriteLine("************请输入数据**************"); string testTask = Console.ReadLine(); service.LPush("test", testTask); } }); //EndInvoke等待异步完成 act.EndInvoke(act.BeginInvoke(null, null)); } #endregion
消费者:
public class ServiceStackProcessor { public static void Show() { string path = AppDomain.CurrentDomain.BaseDirectory; string tag = path.Split('/', '\\').Last(s => !string.IsNullOrEmpty(s)); Console.WriteLine($"这里是 {tag} 启动了。。"); using (RedisListService service = new RedisListService()) { Action act = new Action(() => { while (true) { //阻塞命令:从多个list中尾部移除一个值,并返回移除的值&key,阻塞时间为sp var result = service.BlockingPopItemFromLists(new string[] { "test", "task" }, TimeSpan.FromHours(3)); Thread.Sleep(100); Console.WriteLine($"这里是 {tag} 队列获取的消息 {result.Id} {result.Item}"); } }); act.EndInvoke(act.BeginInvoke(null, null)); } } }
我们开启多个消费者进程,其中2个显示如下:
类似于12306买票系统或者美团买票,如果是高峰期,你买票是肯定不会立即就能买到,可能需要你等待一段时间,用户将买票信息申请传到服务器,服务器将信息存到队列中,然后会有多个专门的进程来处理队列中的数据。
通过这种方式可以实现下面的优点
1.可以控制并发数量
2.以前要求立马处理完,现在可以在一个时段完成
比如说等待一段时间,服务器处理完成之后会告诉用户。
3.失败还能重试
比如说消息队列中的这个任务失败了,我们可以重新将这个任务加入到消息队列中再次进行处理。
4.流量削峰,降低高峰期的压力
因为有多个服务器来处理消息队列中的任务,所以降低压力
5.高可用
比如说处理消息队列中服务器中存在某几个崩溃了,或者需要升级,对真正的服务器是没什么影响的,问题处理完成之后再接着处理消息队列中的任务就行,用户实际不会感受到问题。
6.可扩展
缺点:
不能立即处理、即时性不高;事务问题
发布者订阅者模式
比如微信公众号之类的关注系统
订阅了对应的频道之后可以接收到这个频道发布的所有信息
#region 发布订阅:观察者,一个数据源,多个接受者,只要订阅了就可以收到的,能被多个数据源共享 这里的是用多个线程代表多个进程订阅, Task.Run(() => { using (RedisListService service = new RedisListService()) {
//这个线程订阅Richard频道 service.Subscribe("Richard", (c, message, iRedisSubscription) => { Console.WriteLine($"注册{1}{c}:{message},Dosomething else"); //如果传的消息是exit,那么就取消订阅 if (message.Equals("exit")) iRedisSubscription.UnSubscribeFromChannels("Richard"); });//blocking } }); Task.Run(() => { using (RedisListService service = new RedisListService()) { service.Subscribe("Richard", (c, message, iRedisSubscription) => { Console.WriteLine($"注册{2}{c}:{message},Dosomething else"); if (message.Equals("exit")) iRedisSubscription.UnSubscribeFromChannels("Richard"); });//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("Richard", "Richard123"); service.Publish("Richard", "Richard234"); service.Publish("Richard", "Richard345"); service.Publish("Richard", "Richard456"); service.Publish("Twelve", "Twelve123"); service.Publish("Twelve", "Twelve234"); service.Publish("Twelve", "Twelve345"); service.Publish("Twelve", "Twelve456"); Console.WriteLine("**********************************************"); service.Publish("Richard", "exit"); service.Publish("Richard", "123Richard"); service.Publish("Richard", "234Richard"); service.Publish("Richard", "345Richard"); service.Publish("Richard", "456Richard"); service.Publish("Twelve", "exit"); service.Publish("Twelve", "123Twelve"); service.Publish("Twelve", "234Twelve"); service.Publish("Twelve", "345Twelve"); service.Publish("Twelve", "456Twelve"); } #endregion