一. 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 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。