c# 透過訊息佇列處理高併發請求實列

.Net菜鸟站發表於2024-04-23

網站面對高併發的情況下,除了增加硬體, 最佳化程式提高以響應速度外,還可以透過並行改序列的思路來解決。這種思想常見的實踐方式就是資料庫鎖和訊息佇列的方式。這種方式的缺點是需要排隊,響應速度慢,優點是節省成本。

演示一下現象
建立一個在售產品表
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(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,建立訂單處理者(消費者)

///


/// 訂單的處理者(消費者)
///

public class OrderConsumer
{
///
/// 訂票的訊息佇列
///

public static ConcurrentQueue TicketOrders = new ConcurrentQueue();
///
/// 訂單結果訊息佇列
///

public static List OrderResults = new 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 ProductsForSale = new 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 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,當然還需要一個業務的實際執行者

///


/// 訂單業務的實際處理者
///

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

相關文章