asp.net c# 通過消息隊列處理高並發請求(以搶小米手機為例)
網站面對高並發的情況下,除了增加硬件, 優化程序提高以響應速度外,還可以通過並行改串行的思路來解決。這種思想常見的實踐方式就是數據庫鎖和消息隊列的方式。這種方式的缺點是需要排隊,響應速度慢,優點是節省成本。
演示一下現象
創建一個在售產品表
CREATE TABLE [dbo].[product](
[id] [int] NOT NULL,--唯一主鍵
[name] [nvarchar](50) NULL,--產品名稱
[status] [int] NULL ,--0未售出 1 售出 默認為0
[username] [nvarchar](50) NULL--下單用戶
)
添加一條記錄
insert into product(id,name,status,username) values(1,'小米手機',0,null)
創建一個搶票程序
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程序是多線程的,那我們把他在容易出現並發的地方加一把鎖就可以了,如下圖處理方式。
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,創建訂單提交入口(生產者)
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,創建訂單處理者(消費者)
/// <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,創建訂單業務的實際執行者
/// <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,在程序啟動前,啟動消費者線程
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,創建生產者並且在程序啟動前調用其初始化程序
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,創建消費者
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,當然還需要一個業務的實際執行者
/// <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
這種處理方式的優點為:執行效率快,相比第二種方式不需要第二個接口來返回查詢結果。
缺點:暫時沒想到,歡迎大家補充。
說明:該方式只是個人猜想,並非實際項目經驗,大家只能作為參考,慎重用於項目。歡迎大家批評指正。
https://www.cnblogs.com/chenxizhaolu/p/12543376.html 轉載自此處覺得有用的請支持正版

