網站面對高并發的情況下,除了增加硬件, 優化程序提高以響應速度外,還可以通過并行改串行的思路來解決。這種思想常見的實踐方式就是數據庫鎖和消息隊列的方式。這種方式的缺點是需要排隊,響應速度慢,優點是節省成本。
演示一下現象
創建一個在售產品表
1
2
3
4
5
6
|
CREATE TABLE [dbo].[product]( [id] [ int ] NOT NULL , --唯一主鍵 [ name ] [nvarchar](50) NULL , --產品名稱 [status] [ int ] NULL , --0未售出 1 售出 默認為0 [username] [nvarchar](50) NULL --下單用戶 ) |
添加一條記錄
1
|
insert into product(id, name ,status,username) values (1, '小米手機' ,0, null ) |
創建一個搶票程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public ContentResult PlaceOrder( string userName) { using (RuanMou2020Entities db = new RuanMou2020Entities()) { var product = db.product.Where<product>(p => p.status== 0).FirstOrDefault(); if (product.status == 1) { return Content( "失敗,產品已經被賣光" ); } else { //模擬數據庫慢造成并發問題 Thread.Sleep(5000); product.status = 1; product.username= userName; db.SaveChanges(); return Content( "成功購買" ); } } } |
如果我們在5秒內一次訪問以下兩個地址,那么返回的結果都是成功購買且數據表中的username是lisi。
/controller/PlaceOrder?username=zhangsan
/controller/PlaceOrder?username=lisi
這就是并發帶來的問題。
第一階段,利用線程鎖簡單粗暴
Web程序是多線程的,那我們把他在容易出現并發的地方加一把鎖就可以了,如下圖處理方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
private static object _lock = new object (); public ContentResult PlaceOrder( string userName) { using (RuanMou2020Entities db = new RuanMou2020Entities()) { lock (_lock) { var product = db.product.Where<product>(p => p.status == 0).FirstOrDefault(); if (product.status == 1) { return Content( "失敗,產品已經被賣光" ); } else { //模擬數據庫慢造成并發問題 Thread.Sleep(5000); product.status = 1; product.username = userName; db.SaveChanges(); return Content( "成功購買" ); } } } } |
這樣每一個請求都是依次執行,不會出現并發問題了。
優點:解決了并發的問題。
缺點:效率太慢,用戶體驗性太差,不適合大數據量場景。
第二階段,拉消息隊列,通過生產者,消費者的模式
1,創建訂單提交入口(生產者)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public class HomeController : Controller { /// <summary> /// 接受訂單提交(生產者) /// </summary> /// <returns></returns> public ContentResult PlaceOrderQueen( string userName) { //直接將請求寫入到訂單隊列 OrderConsumer.TicketOrders.Enqueue(userName); return Content( "wait" ); } /// <summary> /// 查詢訂單結果 /// </summary> /// <returns></returns> public ContentResult PlaceOrderQueenResult( string userName) { var rel = OrderConsumer.OrderResults.Where(p => p.userName == userName).FirstOrDefault(); if (rel == null ) { return Content( "還在排隊中" ); } else { return Content(rel.Result.ToString()); } } } |
2,創建訂單處理者(消費者)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
/// <summary> /// 訂單的處理者(消費者) /// </summary> public class OrderConsumer { /// <summary> /// 訂票的消息隊列 /// </summary> public static ConcurrentQueue< string > TicketOrders = new ConcurrentQueue< string >(); /// <summary> /// 訂單結果消息隊列 /// </summary> public static List<OrderResult> OrderResults = new List<OrderResult>(); /// <summary> /// 訂單處理 /// </summary> public static void StartTicketTask() { string userName = null ; while ( true ) { //如果沒有訂單任務就休息1秒鐘 if (!TicketOrders.TryDequeue( out userName)) { Thread.Sleep(1000); continue ; } //執行真實的業務邏輯(如插入數據庫) bool rel = new TicketHelper().PlaceOrderDataBase(userName); //將執行結果寫入結果集合 OrderResults.Add( new OrderResult() { Result = rel, userName = userName }); } } } |
3,創建訂單業務的實際執行者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
/// <summary> /// 訂單業務的實際處理者 /// </summary> public class TicketHelper { /// <summary> /// 實際庫存標識 /// </summary> private bool hasStock = true ; /// <summary> /// 執行一個訂單到數據庫 /// </summary> /// <returns></returns> public bool PlaceOrderDataBase( string userName) { //如果沒有了庫存,則直接返回false,防止頻繁讀庫 if (!hasStock) { return hasStock; } using (RuanMou2020Entities db = new RuanMou2020Entities()) { var product = db.product.Where(p => p.status == 0).FirstOrDefault(); if (product == null ) { hasStock = false ; return false ; } else { Thread.Sleep(10000); //模擬數據庫的效率比較慢,執行插入時間比較久 product.status = 1; product.username = userName; db.SaveChanges(); return true ; } } } } /// <summary> /// 訂單處理結果實體 /// </summary> public class OrderResult { public string userName { get ; set ; } public bool Result { get ; set ; } } |
4,在程序啟動前,啟動消費者線程
1
2
3
4
5
6
7
8
9
10
11
|
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); GlobalConfiguration.Configure(WebApiConfig.Register); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); //在Global的Application_Start事件里單獨開啟一個消費者線程 Task.Run(OrderConsumer.StartTicketTask); } |
這樣程序的運行模式是:用戶提交的需求里都會添加到消息隊列里去排隊處理,程序會依次處理該隊列里的內容(當然可以一次取出多條來進行處理,提高效率)。
優點:比上一步快了。
缺點:不夠快,而且下單后需要輪詢另外一個接口判斷是否成功。
第三階段 反轉生產者消費者的角色,把可售產品提前放到隊列里,然后讓提交的訂單來消費隊列里的內容
1,創建生產者并且在程序啟動前調用其初始化程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public class ProductForSaleManager { /// <summary> /// 待售商品隊列 /// </summary> public static ConcurrentQueue< int > ProductsForSale = new ConcurrentQueue< int >(); /// <summary> /// 初始化待售商品隊列 /// </summary> public static void Init() { using (RuanMou2020Entities db = new RuanMou2020Entities()) { db.product.Where(p => p.status == 0).Select(p => p.id).ToList().ForEach(p => { ProductsForSale.Enqueue(p); }); } } } public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); GlobalConfiguration.Configure(WebApiConfig.Register); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); //程序啟動前,先初始化待售產品消息隊列 ProductForSaleManager.Init(); } } |
2,創建消費者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class OrderController : Controller { /// <summary> /// 下訂單 /// </summary> /// <param name="userName">訂單提交者</param> /// <returns></returns> public async Task<ContentResult> PlaceOrder( string userName) { if (ProductForSaleManager.ProductsForSale.TryDequeue( out int pid)) { await new TicketHelper2().PlaceOrderDataBase(userName, pid); return Content($ "下單成功,對應產品id為:{pid}" ); } else { await Task.CompletedTask; return Content($ "商品已經被搶光" ); } } } |
3,當然還需要一個業務的實際執行者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/// <summary> /// 訂單業務的實際處理者 /// </summary> public class TicketHelper2 { /// <summary> /// 執行復雜的訂單操作(如數據庫) /// </summary> /// <param name="userName">下單用戶</param> /// <param name="pid">產品id</param> /// <returns></returns> public async Task PlaceOrderDataBase( string userName, int pid) { using (RuanMou2020Entities db = new RuanMou2020Entities()) { var product = db.product.Where(p => p.id == pid).FirstOrDefault(); if (product != null ) { product.status = 1; product.username = userName; await db.SaveChangesAsync(); } } } } |
這樣我們同時訪問下面三個地址,如果數據庫里只有兩個商品的話,會有一個請求結果為:商品已經被搶光。
http://localhost:88/Order/PlaceOrder?userName=zhangsan
http://localhost:88/Order/PlaceOrder?userName=lisi
http://localhost:88/Order/PlaceOrder?userName=wangwu
這種處理方式的優點為:執行效率快,相比第二種方式不需要第二個接口來返回查詢結果。
缺點:暫時沒想到,歡迎大家補充。
說明:該方式只是個人猜想,并非實際項目經驗,大家只能作為參考,慎重用于項目。歡迎大家批評指正。
到此這篇關于asp.net通過消息隊列處理高并發請求(以搶小米手機為例)的文章就介紹到這了,更多相關asp.net 消息隊列處理高并發 內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.cnblogs.com/chenxizhaolu/p/12543376.html