網站面對高併發的情況下,除了增加硬體, 最佳化程式提高以響應速度外,還可以透過並行改序列的思路來解決。這種思想常見的實踐方式就是資料庫鎖和訊息佇列的方式。這種方式的缺點是需要排隊,響應速度慢,優點是節省成本。
演示一下現象
建立一個在售產品表
CREATE TABLE [dbo].[product](
[id] [int] NOT NULL,--唯一主鍵
[name] nvarchar NULL,--產品名稱
[status] [int] NULL ,--0未售出 1 售出 預設為0
[username] nvarchar 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
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,建立訂單處理者(消費者)
///
/// 訂單的處理者(消費者)
///
public class OrderConsumer
{
///
/// 訂票的訊息佇列
///
public static ConcurrentQueue
///
/// 訂單結果訊息佇列
///
public static List
///
/// 訂單處理
///
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,建立訂單業務的實際執行者
///
/// 訂單業務的實際處理者
///
public class TicketHelper
{
///
/// 實際庫存標識
///
private bool hasStock = true;
///
/// 執行一個訂單到資料庫
///
///
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;
}
}
}
}
///
/// 訂單處理結果實體
///
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
{
///
/// 待售商品佇列
///
public static ConcurrentQueue
///
/// 初始化待售商品佇列
///
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
{
///
/// 下訂單
///
/// 訂單提交者
///
public async Task
{
if (ProductForSaleManager.ProductsForSale.TryDequeue(out int pid))
{
await new TicketHelper2().PlaceOrderDataBase(userName, pid);
return Content($"下單成功,對應產品id為:{pid}");
}
else
{
await Task.CompletedTask;
return Content($"商品已經被搶光");
}
}
}
3,當然還需要一個業務的實際執行者
///
/// 訂單業務的實際處理者
///
public class TicketHelper2
{
///
/// 執行復雜的訂單操作(如資料庫)
///
/// 下單使用者
/// 產品id
///
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:8080/Order/PlaceOrder?userName=zhangsan
http://localhost:8080/Order/PlaceOrder?userName=lisi
http://localhost:8080/Order/PlaceOrder?userName=wangwu
這種處理方式的優點為:執行效率快,相比第二種方式不需要第二個介面來返回查詢結果。
缺點:暫時沒想到,歡迎大家補充
轉載至:https://www.cnblogs.com/chenxizhaolu/p/12543376.html