第三节: List类型的介绍、生产者消费者模式、发布订阅模式

一. List类型基础

1.介绍

  它是一个双向链表,支持左进、左出、右进、右出,所以它即可以充当队列使用,也可以充当栈使用。

(1). 队列:先进先出, 可以利用List左进右出,或者右进左出(ListLeftPush和ListRightPop配合 、 ListRightPush和ListLeftPop配合)

(2). 栈:先进后出,可以利用List左进左出,或者右进右出

2. 常用指令Api

 

3.常用Api

(1). ListLeftPush:从左侧添加,返回集合总数

(2). ListRightPush:从右侧添加,返回集合总数

(3). ListLeftPop:从左侧取1个值,并删除

(4). ListRightPop:从右侧取1个值,并删除

(5). ListInsertBefore:指定的key指定value之前(左边)插入1个值

(6). ListInsertAfter:指定的key指定value之后(右边)插入1个值

(7). ListGetByIndex:获取key的指定索引对应的value值(从左往右算)

(8). ListRange:获取key的所有value,数据类型得一致 (也可以获取指定索引之间的value值,带扩展)

(9). ListLength:获取指定key的数据的个数

(10). ListRemove:删除指定key对应的指定value值,返回删除的个数

(11). ListRightPopLeftPush:从List1右侧取一个值加到List2左侧,返回的是右侧取出来的这个值

 代码分享:

 1             //1.从左侧添加
 2             //单个,返回集合总数
 3             db.ListLeftPush("group1", "你好1");
 4             db.ListLeftPush("group1", "你好2");
 5             db.ListLeftPush("group1", "你好3");
 6             //多个
 7             string[] dList1 = { "你好4", "你好5" };
 8             RedisValue[] redisValue = dList1.Select(u => (RedisValue)u).ToArray();
 9             var d1 = db.ListLeftPush("group1", redisValue);
10 
11             //2.从右侧添加
12             db.ListRightPush("group1", "你好6");
13 
14             //3.从左侧取1个值,并删除
15             //var v1=db.ListLeftPop("group1");
16 
17             //4.从右侧取1个值并删除
18             //var v2 = db.ListRightPop("group1");
19 
20             //5. 在List的指定的key指定value之前(左边)插入1个值
21             db.ListInsertBefore("group1", "你好3", "ypf001");
22 
23             //6. 在List的指定的key指定value之后(右边)插入1个值
24             db.ListInsertAfter("group1", "你好3", "ypf002");
25 
26             //7. 获取key指定索引的值(从左往右算)
27             var d2 = db.ListGetByIndex("group1", 0);
28             var d3 = db.ListGetByIndex("group1", 2);
29 
30             //8. 获取key的所有数据,数据类型得一致
31             var d4 = db.ListRange("group1").Select(u => (string)u).ToList();
32             //获取key的前4条数据(从左往右)
33             var d44 = db.ListRange("group1", 0, 3).Select(u => (string)u).ToList();
34 
35             //9.获取指定key的数据的个数
36             long d5 = db.ListLength("group1");
37 
38             //10. 删除指定key对应的指定value值,返回删除的个数
39             db.ListLeftPush("group1", "你好");
40             db.ListLeftPush("group1", "你好");
41             long d6 = db.ListRemove("group1", "你好");
42 
43             //11. 从List1右侧取一个值加到List2左侧,返回的是右侧取出来的这个值
44             db.ListLeftPush("group2", "你好1");
45             db.ListLeftPush("group2", "你好2");
46             db.ListLeftPush("group2", "你好3");
47             db.ListLeftPush("group3", "哈哈1");
48             db.ListLeftPush("group3", "哈哈2");
49             db.ListLeftPush("group3", "哈哈3");
50             //从队列group2右侧取出来一个值放到group3中
51             var d7 = db.ListRightPopLeftPush("group2", "group3");

 

二. 案例分析

(一). 消息队列 (生产者消费者模式)

PS:生产者消费模式:可以是多个生产者,多个消费者,但是生成的数据按顺序进入队列,但是每个数据只能被一个消费者消费。

1. 异步处理

  (1). 将同步业务:创建订单→增加积分→发送短信,改为创建订单后 存放到两个消息队列中(积分队列和短信队列),然后积分业务和短信业务分部去队列中读取,执行各自的业务

  (2). 注册成功发邮件通知:很多场景注册成功后要给用户发一封邮件提示,但这封邮件实时性要求并不是很高,而且发邮件一般是调用第三方接口进行发送,有时候可能会很慢或者故障了, 针对这种情况,借助队列采用生产者消费者模式非常适合。

2. 应用解耦

  将原先订单系统和库存系统的强依赖关系,改为中间引入消息队列,这样二者都依赖消息队列做中介.

3. 流量削锋

  秒杀服务,下单的用户加到队列中,然后开启另外一个线程从队列中读取进行下单,下单成功/失败 利用实时通讯技术通知客户端 或者 客户端主动刷新页面进行查看结果,这里要结合实际架构(单体or集群)分析秒杀情况,不能一概而论,详见后面秒杀章节。

4. 即时通讯

  考虑到同时很多人发送,前端页面的渲染会有点吃不消,这里可以采用 群id 当做队列的key,发送的消息当做value,存入队列中,然后开启一个新的线程从里面读取, 可以一下获取20条,获取的同时并删除,如果队列为空,则休息几秒中,再次获取。

针对群聊的代码分享

  以群聊为例,利用ListLeftPush方法,以“群id”当做key,以发送人id、发送内容、时间组合当做value,从左侧存储到队列;然后利用Core中的BackService类开启后台线程利用ListRightPop 进行读取,同时要在ConfigureService中进行注册。

代码分享:

 1        /// <summary>
 2         /// 测试群聊页面
 3         /// (PS:不断刷新即可)
 4         /// </summary>
 5         /// <returns></returns>
 6         public IActionResult Index()
 7         {
 8             string userId = Guid.NewGuid().ToString("N");
 9             string msg = "哈哈" + new Random().Next(1, 10000);
10             SendMessage(userId, msg);
11             return View();
12         }
13 
14         /// <summary>
15         ///群聊发送消息接口 
16         /// </summary>
17         /// <param name="userId">用户id</param>
18         /// <param name="msg">发送的内容</param>
19         /// <returns></returns>
20         public string SendMessage(string userId, string msg)
21         {
22             try
23             {            
24                 string groupName = "classParty";  //群名
25                 string sendContent = $"{userId}_{msg}_{DateTime.Now}"; //内容
26                 //存入队列
27                 _redis.ListLeftPush(groupName, sendContent);
28                 return "ok";
29             }
30             catch (Exception ex)
31             {
32                 return "error";
33             }
34         }

后台服务及注册:

 1  public class SendService : BackgroundService
 2     {
 3         private readonly IDatabase _redis;
 4         public SendService(RedisHelp redisHelp)
 5         {
 6             _redis = redisHelp.GetDatabase();
 7         }
 8         protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 9         {
10             while (!stoppingToken.IsCancellationRequested)
11             {
12                 try
13                 {
14                     //实际情况,这里有几个群,开几个线程执行     
15                     List<string> msgList = new List<string>();
16                     Stopwatch stopwatch = new Stopwatch();
17                     stopwatch.Start();
18                     //要么没有更多待发消息立即发给客户端,要么累积满1秒钟待发消息后发送给客户端
19                     while (true)
20                     {                        
21                         string msg = _redis.ListRightPop("classParty");
22                         if (!string.IsNullOrEmpty(msg)) 
23                         {
24                             msgList.Add(msg);
25                         }
26                         else
27                         {
28                             await Task.Delay(500);
29                         }
30                         //满一段时间的消息向客户端发送
31                         if (stopwatch.Elapsed>TimeSpan.FromSeconds(1))
32                         {
33                             stopwatch.Stop();
34                             if (msgList.Any())
35                             {
36                                 //将这一批消息发送给客户端                             
37                                 //需要重新滞空msgList
38                             }
39                         }
40                     }                 
41                 }
42                 catch (Exception)
43                 {
44                     throw;
45                 }
46             }
47         }
48     }
1  public void ConfigureServices(IServiceCollection services)
2   {
3             //注册后台服务
4             services.AddHostedService<SendService>();
5    }

(二). 解决查询缓慢问题

  比如发帖网站,会有非常多的帖子,而且数量每日俱增,首页显示的是最新发布的10条帖子,显示的是:发帖人名称 和 发帖标题,如果从数据库中查询可能会非常慢,这时候可以把发帖人名称和

发帖标题(包括帖子id),存到Redis队列中,这个时候利用 栈 的特性,ListGetByIndex:获取key指定索引的值(从左往右算), 获取前10条数据,用于显示,查看详情的时候,再根据帖子的id到数据库中查。

 

三. 发布订阅模式

 1. 说明

  发布者发布一条消息,所有的订阅者都能收到。

2. 案例背景

  以微博为例(或者微信的订阅号),博主A,博主B都关注了博主C、博主D,在博主A(或B)的版面,应该显示的是博主C和博主D发布的最新博文(最新发布的在最上面),换句话说博主C或者博主D每发一篇博文,都要推送给关注他们的博主A和博主B。

3. 技术分析

(1). 数据结构的设计:一个博主对应一个List链表,用来存储该博主应该显示的博文消息。 以博主的用户id作为key,博文消息的id作为value。

(2). 博主C每发一条博文,就需要向关注他的粉丝(A和B)对应的链表中分别存储一下 该博文消息的id。

(3). 博主D每发一条博文,就需要向关注他的粉丝(A和B)对应的链表中分别存储一下 该博文消息的id。

(4).  博主A就可以到自己对应的链表中利用的特性,获取最新的n条博文消息id。

PS: 拿到博文消息id了,剩下的就容易了,根据id去关系型数据中查内容就很快了,或者也可以将标题或者内容的前100字也存储到Redis中,便于页面显示(这样value的格式就是:博文id-博文标题-博文内容前100字)。

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2019-11-27 10:44  Yaopengfei  阅读(1348)  评论(2编辑  收藏  举报