第二節:搶單流程優化1(小白寫法→lock寫法→服務器緩存+隊列(含lock)→Redis緩存+原子性+隊列【干掉lock】)


一. 小白寫法

1.設計思路

 純DB操作

 DB查庫存→判斷庫存→(DB扣減庫存+DB創建訂單)

2.分析

 A.響應非常慢,導致大量請求拿不到結果而報錯

 B.存在超賣現象

 C.扣減庫存錯誤

3.壓測結果

前提:原庫存為10000,這里統計2s內可處理的並發數,以90%百分位為例,要求錯誤率為0。

代碼分享:

       /// <summary>
        /// 原始版本-純DB操作
        /// </summary>
        /// <param name="userId">用戶編號</param>
        /// <param name="arcId">商品編號</param>
        /// <param name="totalPrice">訂單總額</param>
        /// <returns></returns>
        public string POrder1(string userId, string arcId, string totalPrice)
        {
            try
            {
                //1. 查詢庫存
                var sArctile = _baseService.Entities<T_SeckillArticle>().Where(u => u.articleId == arcId).FirstOrDefault();
                if (sArctile.articleStockNum - 1 > 0)
                {
                    //2. 扣減庫存
                    sArctile.articleStockNum--;

                    //3. 進行下單
                    T_Order tOrder = new T_Order();
                    tOrder.id = Guid.NewGuid().ToString("N");
                    tOrder.userId = userId;
                    tOrder.orderNum = Guid.NewGuid().ToString("N");
                    tOrder.articleId = arcId;
                    tOrder.orderTotalPrice = Convert.ToDecimal(totalPrice);
                    tOrder.addTime = DateTime.Now;
                    tOrder.orderStatus = 0;
                    _baseService.Add<T_Order>(tOrder);
                    _baseService.SaveChange();

                    return "下單成功";
                }
                else
                {
                    //賣完了
                    return "賣完了";
                }
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }

        }
View Code

測試結果:

(1). 100並發,需要1788ms,訂單數量插入正確,但庫存扣減錯誤。

 

 

(2). 200並發,需要4453ms,訂單數量插入正確,但庫存扣減錯誤。

 

 

 

二. lock寫法

1.設計思路

 純DB操作的基礎上Lock鎖

 Lock { DB查庫存→判斷庫存→(DB扣減庫存+DB創建訂單) }

2.分析

 A. 解決超賣現象

 B. 響應依舊非常慢,導致大量請求拿到結果而報錯

3.壓測結果

前提:原庫存為10000,這里統計2s內可處理的並發數,以90%百分位為例,要求錯誤率為0。

代碼分享:

        /// <summary>
        /// 02-純DB操作+Lock鎖
        /// </summary>
        /// <param name="userId">用戶編號</param>
        /// <param name="arcId">商品編號</param>
        /// <param name="totalPrice">訂單總額</param>
        /// <returns></returns>
        public string POrder2(string userId, string arcId, string totalPrice)
        {
            try
            {
                lock (_lock)
                {
                    //1. 查詢庫存
                    var sArctile = _baseService.Entities<T_SeckillArticle>().Where(u => u.articleId == arcId).FirstOrDefault();
                    if (sArctile.articleStockNum - 1 > 0)
                    {
                        //2. 扣減庫存
                        sArctile.articleStockNum--;

                        //3. 進行下單
                        T_Order tOrder = new T_Order();
                        tOrder.id = Guid.NewGuid().ToString("N");
                        tOrder.userId = userId;
                        tOrder.orderNum = Guid.NewGuid().ToString("N");
                        tOrder.articleId = arcId;
                        tOrder.orderTotalPrice = Convert.ToDecimal(totalPrice);
                        tOrder.addTime = DateTime.Now;
                        tOrder.orderStatus = 0;
                        _baseService.Add<T_Order>(tOrder);
                        _baseService.SaveChange();

                        return "下單成功";
                    }
                    else
                    {
                        //賣完了
                        return "賣完了";
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
View Code

(1). 30並發,需要2132ms,訂單數量插入正確,庫存扣減正確。

 

 

(2). 100並發,需要9186ms,訂單數量插入正確,庫存扣減正確。

 

 

 

三. 服務器緩存+隊列

1.設計思路

 生產者和消費者模式→流量削峰(異步的模式平滑處理請求)

 A. Lock{ 事先同步DB庫存到緩存→緩存查庫存→判斷庫存→訂單相關信息服務端隊列中 }

 B. 消費者從隊列中取數據批量提交信息,依次進行(DB扣減庫存+DB創建訂單)

2.分析

 A. 接口中徹底干掉了DB操作, 並發數提升非常大

 B. 服務宕機,原隊列中的下單信息全部丟失

 C. 但是生產者和消費者必須在一個項目及一個進程內

3.壓測結果

前提:原庫存為10000,這里統計2s內可處理的並發數,以90%百分位為例,要求錯誤率為0。

代碼分享:

初始化庫存到內存緩存中

    /// <summary>
    /// 后台任務-初始化庫存到緩存中
    /// </summary>
    public class CacheBackService : BackgroundService
    {
        private IMemoryCache _cache;
        private StackExchange.Redis.IDatabase _redisDb;
        private IConfiguration _Configuration;

        public CacheBackService(IMemoryCache cache,RedisHelp redisHelp, IConfiguration Configuration)
        {
            _cache = cache;
            _redisDb = redisHelp.GetDatabase();
            _Configuration = Configuration;
        }

        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            // EFCore的上下文默認注入的請求內單例的,而CacheBackService要注冊成全局單例的
            // 由於二者的生命周期不同,所以不能相互注入調用,這里手動new一個EF上下文
            var optionsBuilder = new DbContextOptionsBuilder<ESHOPContext>();
            optionsBuilder.UseSqlServer(_Configuration.GetConnectionString("EFStr"));
            ESHOPContext context = new ESHOPContext(optionsBuilder.Options);
            IBaseService _baseService = new BaseService(context);

            //初始化庫存信息,連臨時寫在這個位置,充當服務器啟動的時候初始化
            var data = await _baseService.Entities<T_SeckillArticle>().Where(u => u.id == "300001").FirstOrDefaultAsync();
   //服務器緩存
                _cache.Set<int>($"{data.articleId}-sCount", data.articleStockNum);      
        }
    }
View Code

隊列定義和下單接口

    /// <summary>
    /// 基於內存的隊列
    /// </summary>
    public static class MyQueue
    {
      private static  ConcurrentQueue<string> _queue = new ConcurrentQueue<string>();
        public static ConcurrentQueue<string> GetQueue()
        {
            return _queue;
        }
    }
        /// <summary>
        /// 03-服務端緩存+隊列版本+Lock
        /// </summary>
        /// <param name="userId">用戶編號</param>
        /// <param name="arcId">商品編號</param>
        /// <param name="totalPrice">訂單總額</param>
        /// <returns></returns>
        public string POrder3(string userId, string arcId, string totalPrice)
        {
            try
            {
                lock (_lock)
                {
                    //1. 查詢庫存
                    int count = _cache.Get<int>($"{arcId}-sCount");
                    if (count - 1 >= 0)
                    {
                        //2. 扣減庫存
                        count = count - 1;
                        _cache.Set<int>($"{arcId}-sCount", count);

                        //3. 將下單信息存到消息隊列中
                        var orderNum = Guid.NewGuid().ToString("N");
                        MyQueue.GetQueue().Enqueue($"{userId}-{arcId}-{totalPrice}-{orderNum}");

                        //4. 把部分訂單信息返回給前端
                        return $"下單成功,訂單信息為:userId={userId},arcId={arcId},orderNum={orderNum}";
                    }
                    else
                    {
                        //賣完了
                        return "賣完了";
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
View Code

基於內存的消費者

     /// <summary>
    /// 后台任務--基於內存隊列的消費者(已經測試)
    /// </summary>
    public class CustomerService : BackgroundService
    {
        private IConfiguration _Configuration;
        public CustomerService(IConfiguration Configuration)
        {
            _Configuration = Configuration;
        }
        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
        {

            // EFCore的上下文默認注入的請求內單例的,而CacheBackService要注冊成全局單例的
            // 由於二者的生命周期不同,所以不能相互注入調用,這里手動new一個EF上下文
            var optionsBuilder = new DbContextOptionsBuilder<ESHOPContext>();
            optionsBuilder.UseSqlServer(_Configuration.GetConnectionString("EFStr"));
            ESHOPContext context = new ESHOPContext(optionsBuilder.Options);
            IBaseService _baseService = new BaseService(context);

            Console.WriteLine("下面開始執行消費業務");
            while (true)
            {
                try
                {
                    string data = "";
                    MyQueue.GetQueue().TryDequeue(out data);
                    if (!string.IsNullOrEmpty(data))
                    {
                        List<string> tempData = data.Split('-').ToList();
                        //1.扣減庫存---禁止狀態追蹤
                        var sArctile = context.Set<T_SeckillArticle>().AsNoTracking().Where(u => u.id == "300001").FirstOrDefault();
                        sArctile.articleStockNum = sArctile.articleStockNum - 1;
                        context.Update(sArctile);

                        //2. 插入訂單信息
                        T_Order tOrder = new T_Order();
                        tOrder.id = Guid.NewGuid().ToString("N");
                        tOrder.userId = tempData[0];
                        tOrder.orderNum = tempData[3];
                        tOrder.articleId = tempData[1];
                        tOrder.orderTotalPrice = Convert.ToDecimal(tempData[2]);
                        tOrder.addTime = DateTime.Now;
                        tOrder.orderStatus = 0;
                        context.Add<T_Order>(tOrder);
                        int count = await context.SaveChangesAsync();

                        //釋放一下
                        context.Entry<T_SeckillArticle>(sArctile).State = EntityState.Detached;
                        Console.WriteLine($"執行成功,條數為:{count},當前庫存為:{ sArctile.articleStockNum}");
                    }
                    else
                    {
                        Console.WriteLine("暫時沒有訂單信息,休息一下");
                        await Task.Delay(TimeSpan.FromSeconds(1));
                    }
                }
                catch (Exception ex)
                {

                    Console.WriteLine($"執行失敗:{ex.Message}");
                }
            }
        }
    }
View Code

(1). 1000並發,需要600ms,訂單數量插入正確,庫存扣減正確。

 

(2). 2000並發,需要1500ms,訂單數量插入正確,庫存扣減正確。

 

 

 

四. Redis緩存+原子性+隊列【干掉lock】

1.設計思路

 生產者和消費者模式→流量削峰(異步的模式平滑處理請求)

 思路同上,緩存和隊列改成基於Redis的。

2. 分析

 A. 引入Redis緩存和消息隊列代替基於內存的緩存和隊列,數據可以持久化解決了丟失問題。

 B. Redis是單線程的,利用api自身的原子性,從而可以干掉lock鎖。

 C. 引入進程外的緩存Redis,從而可以把生產者和消費者解耦分離,可以作為兩個單獨的服務運行。

3. 壓測結果

前提:原庫存為10萬,這里統計2s內可處理的並發數,以90%百分位為例,要求錯誤率為0。

代碼分享:

初始化庫存到redis緩存中

    /// <summary>
    /// 后台任務-初始化庫存到緩存中
    /// </summary>
    public class CacheBackService : BackgroundService
    {
        private IMemoryCache _cache;
        private StackExchange.Redis.IDatabase _redisDb;
        private IConfiguration _Configuration;

        public CacheBackService(IMemoryCache cache,RedisHelp redisHelp, IConfiguration Configuration)
        {
            _cache = cache;
            _redisDb = redisHelp.GetDatabase();
            _Configuration = Configuration;
        }

        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            // EFCore的上下文默認注入的請求內單例的,而CacheBackService要注冊成全局單例的
            // 由於二者的生命周期不同,所以不能相互注入調用,這里手動new一個EF上下文
            var optionsBuilder = new DbContextOptionsBuilder<ESHOPContext>();
            optionsBuilder.UseSqlServer(_Configuration.GetConnectionString("EFStr"));
            ESHOPContext context = new ESHOPContext(optionsBuilder.Options);
            IBaseService _baseService = new BaseService(context);

            //初始化庫存信息,連臨時寫在這個位置,充當服務器啟動的時候初始化
            var data = await _baseService.Entities<T_SeckillArticle>().Where(u => u.id == "300001").FirstOrDefaultAsync();
            //Redis緩存
                _redisDb.StringSet($"{data.articleId}-sCount", data.articleStockNum);
        }
    }
View Code

下單接口

        /// <summary>
        /// 04-Redis緩存+隊列
        /// </summary>
        /// <param name="userId">用戶編號</param>
        /// <param name="arcId">商品編號</param>
        /// <param name="totalPrice">訂單總額</param>
        /// <returns></returns>
        public string POrder4(string userId, string arcId, string totalPrice)
        {
            try
            {
                //1. 直接自減1
                int iCount = (int)_redisDb.StringDecrement($"{arcId}-sCount", 1);
                if (iCount >= 0)
                {
                    //2. 將下單信息存到消息隊列中
                    var orderNum = Guid.NewGuid().ToString("N");
                    _redisDb.ListLeftPush(arcId, $"{userId}-{arcId}-{totalPrice}-{orderNum}");

                    //3. 把部分訂單信息返回給前端
                    return $"下單成功,訂單信息為:userId={userId},arcId={arcId},orderNum={orderNum}";
                }
                else
                {
                    //賣完了
                    return "賣完了";
                }
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }
View Code

基於redis隊列的消費者

            {
                Console.WriteLine("下面開始執行消費業務");
                using (ESHOPContext db = new ESHOPContext())
                {
                    RedisHelp redisHelp = new RedisHelp("localhost:6379");
                    var redisDB = redisHelp.GetDatabase();

                    while (true)
                    {
                        try
                        {
                            var data = (string)redisDB.ListRightPop("200001");
                            if (!string.IsNullOrEmpty(data))
                            {
                                List<string> tempData = data.Split('-').ToList();

                                {
                                    //1.扣減庫存 --去掉狀態追蹤
                                    var sArctile = db.Set<T_SeckillArticle>().AsNoTracking().Where(u => u.id == "300001").FirstOrDefault();
                                    sArctile.articleStockNum = sArctile.articleStockNum - 1;
                                    db.Update(sArctile);

                                    //2. 插入訂單信息
                                    T_Order tOrder = new T_Order();
                                    tOrder.id = Guid.NewGuid().ToString("N");
                                    tOrder.userId = tempData[0];
                                    tOrder.orderNum = tempData[3];
                                    tOrder.articleId = tempData[1];
                                    tOrder.orderTotalPrice = Convert.ToDecimal(tempData[2]);
                                    tOrder.addTime = DateTime.Now;
                                    tOrder.orderStatus = 0;
                                    db.Add<T_Order>(tOrder);
                                    int count = db.SaveChanges();

                                    //釋放一下--否則報錯
                                    db.Entry<T_SeckillArticle>(sArctile).State = EntityState.Detached;
                                    Console.WriteLine($"執行成功,條數為:{count},當前庫存為:{ sArctile.articleStockNum}");

                                }

                            }
                            else
                            {
                                Console.WriteLine("暫時沒有訂單信息,休息一下");
                                Thread.Sleep(1000);
                            }
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine($"執行失敗-{ex.Message}");
                        }
                    }
                }
            }
View Code

(1). 1000並發,需要600ms,訂單數量插入正確,庫存扣減正確。

 

(2). 2000並發,需要1560ms,訂單數量插入正確,庫存扣減正確。

 

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鵬飛)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 聲     明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
  • 聲     明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM