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)

建立一個搶票程序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

這種處理方式的優勢爲:執行效率快,相比第二種方式不須要第二個接口來返回查詢結果。

缺點:暫時沒想到,歡迎你們補充。

說明:該方式只是我的猜測,並不是實際項目經驗,你們只能做爲參考,慎重用於項目。歡迎你們批評指正。

相關文章
相關標籤/搜索