網站面對高併發的狀況下,除了增長硬件, 優化程序提升以響應速度外,還能夠經過並行改串行的思路來解決。這種思想常見的實踐方式就是數據庫鎖和消息隊列的方式。這種方式的缺點是須要排隊,響應速度慢,優勢是節省成本。數據庫
建立一個在售產品表多線程
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)
建立一個搶票程序async
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程序是多線程的,那咱們把他在容易出現併發的地方加一把鎖就能夠了,以下圖處理方式。spa
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
這種處理方式的優勢爲:執行效率快,相比第二種方式不須要第二個接口來返回查詢結果。
缺點:暫時沒想到,歡迎你們補充。
說明:該方式只是我的猜測,並不是實際項目經驗,你們只能做爲參考,慎重用於項目。歡迎你們批評指正。