EF+SQLSERVER控制並發下搶紅包減余額(改進)


最近幾年想必大家一聽到哪里有搶紅包可以搶,馬上會拿起手機點去~~~~然后問題來了。。。

如何控制在同一時間保證數據庫中扣減紅包余額不會出錯。之前我們的做法是直接鎖程序,這樣子帶來的壞處就是等待時間太長,每當一個線程進去之后要經過以下幾個過程。

過程分別是

1. 查表

2. 校驗信息

3. 發送微信服務器

4. 等待反饋

5. 更新表

等這些過程結束之后才輪到下面這個過程。想必這樣要等到花兒都謝了~

另外發送微信服務器這個過程時間在0s至9s時間不等。會產生大量的空閑時間,這里CPU會產生大量的空閑。而且這種情況也無法繼續做負載均衡,如果有多個站點部署必定會產生數據庫並發問題。

若在查表之前加鎖更新后釋放掉,雖然說不會產生數據庫並發。但是在第二個線程進入查詢的時候他會一直在等待,其耗時則與更鎖程序差不多。


改進

這個想法源於分布式事務的設計,采用預扣紅包余額的方式來保證無需等待微信服務器反饋,讓下一個線程可繼續執行相關任務。當微信服務器反饋回來時,才開始另外一個事務去更改交易狀態。若反饋結果為FAIL則需要預扣的紅包余額進行還原操作。

粗略寫了模擬實際環境的測試代碼,模擬搶紅包動作

private void task()
{
    for (int i = 0; i < 50; i++)
    {
        string tradeNo = Qxun.Framework.Utility.CreateOrderNo.DateTimeAndNumber();
        try
        {
            using (var trans = new TransactionScope())
            {
                using (var dbContext = new ActivityDbContext())
                {
                    //加鎖
                    var model = dbContext.Database.SqlQuery<Qxun.Activity.Contract.VIPPassRedBag013>(@"select * from VIPPassRedBag013 with(updlock) where ActivitySceneID=199").FirstOrDefault();
                    var mode = dbContext.Database.SqlQuery<Qxun.Activity.Contract.VIPPassRedBag013Mode>(@"select * from VIPPassRedBag013Mode with(updlock) where ActivitySceneID=199").ToList();
                    //模擬校驗延遲
                    Thread.Sleep(5);
                    //得到領取紅包的金額
                    VIPPassRedBag013Mode currentMode = null;
                    foreach (var modeItem in mode)
                    {
                        if (modeItem.RemainCount > 0)
                        {
                            currentMode = modeItem;
                            break;
                        }
                    }
                    //判斷是否領完
                    if (currentMode != null && model != null && model.RedBagBalance >= currentMode.Money)
                    {
                        VIPPassRedBag013Play currentPlayModel = new VIPPassRedBag013Play();//本次的參與記錄對象
                        currentPlayModel.VIPPassRedBag013ModeID = currentMode.ID;
                        currentPlayModel.WeixinUserID = Thread.CurrentThread.ManagedThreadId;
                        currentPlayModel.Money = Convert.ToInt32(currentMode.Money * 100);//要支付的金額(存入到表的)
                        currentPlayModel.TradeNumber = tradeNo;
                        currentPlayModel.Status = (int)TradeStatus.Trading;
                        currentPlayModel.VIPPassRedBag013ModeID = currentMode.ID;
                        currentPlayModel.ActivitySceneID = 199;
                        dbContext.Insert<VIPPassRedBag013Play>(currentPlayModel);
                        currentMode.RemainCount -= 1;
                        dbContext.Update<VIPPassRedBag013Mode>(currentMode);
                        model.RedBagBalance -= currentMode.Money;
                        dbContext.Update<Qxun.Activity.Contract.VIPPassRedBag013>(model);
                        trans.Complete();

                    }
                    else
                    {
                        trans.Complete();
                    }
                }
            }
        }
        catch (Exception ex){}
        //提交至微信           
        string returnCode = "SUCCESS";
        Random ran = new Random();
        int time = ran.Next(100);
        if (time <= 1)
        {
            returnCode = "FAIL";
        }
        //模擬網絡延遲
        Thread.Sleep(time * 100);
        //設置重新嘗試次數
        bool retry = true;
        int retryCount = 0;
        do
        {
            Qxun.Activity.Contract.VIPPassRedBag013 model = null;
            VIPPassRedBag013Play playModel = null;
            VIPPassRedBag013Mode mode = null;
            try
            {
                using (var trans = new TransactionScope())
                {
                    using (var dbContext = new ActivityDbContext())
                    {
                        //這里獲取很容易異常
                        model = dbContext.Database.SqlQuery<Qxun.Activity.Contract.VIPPassRedBag013>(@"select * from VIPPassRedBag013 with(updlock) where ActivitySceneID=199").FirstOrDefault();
                        playModel = dbContext.Database.SqlQuery<Qxun.Activity.Contract.VIPPassRedBag013Play>(@"select * from VIPPassRedBag013Play with(updlock) where TradeNumber='" + tradeNo + "'").FirstOrDefault();
                        mode = dbContext.Database.SqlQuery<Qxun.Activity.Contract.VIPPassRedBag013Mode>(@"select * from VIPPassRedBag013Mode with(updlock) where ID=" + playModel.VIPPassRedBag013ModeID).FirstOrDefault();
                        if (returnCode == "SUCCESS")
                        {
                            playModel.Status = (int)TradeStatus.Success;
                            playModel.Remark = "retry=" + retryCount + ",success;time=" + DateTime.Now.ToString();
                            playModel.FinishTime = DateTime.Now;
                            dbContext.Update<VIPPassRedBag013Play>(playModel);
                            trans.Complete();
                            retry = false;
                        }
                        else
                        {
                            model.RedBagBalance += mode.Money;
                            dbContext.Update<Qxun.Activity.Contract.VIPPassRedBag013>(model);
                            playModel.Status = (int)TradeStatus.Fail;
                            playModel.Remark = "retry=" + retryCount + ",fail;time=" + DateTime.Now.ToString();
                            playModel.FinishTime = DateTime.Now;
                            dbContext.Update<VIPPassRedBag013Play>(playModel);
                            mode.RemainCount += 1;
                            dbContext.Update<VIPPassRedBag013Mode>(mode);
                            trans.Complete();
                            retry = false;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                //如果之前的線程請求數據庫時阻塞
                //如果執行失敗
                retryCount++;
                retry = true;
            }
            if (retryCount > 5)
            {
                break;
            }
        } while (retry);
    }
}

 
模擬100個人並發搶紅包

public ActionResult Excute()
{
    for (int i = 0; i < 100; i++)
    {
        Thread thread = new Thread(new ThreadStart(task));
        thread.Start();
    }
    return Content("完成!");
}

 

上面代碼還用了一個retry變量控制防止由於長等待產生的超時,好讓每個訂單都能夠處理的到。但是實際上當線程數量為100-200時候,會有10至20個VIPPassRedBag013Play訂單狀態一直為Trading。當線程數量大於200的時候就變得及不穩定,目前一直沒有找到是什么原因。希望有緣人指點一二。

為了解決這種現象,我在Global寫了周期去查找10分鍾前的VIPPassRedBag013Play,且訂單狀態為Trading的單子(都10分鍾了還沒有處理,那就是處理不到了)。得到訂單號,去反查微信的紅包交易記錄。通過微信紅包反饋的結果去更新數據庫的交易狀態。

public ActionResult Check()
{
    using (var dbContext = new ActivityDbContext())
    {
        //查詢十分鍾之前狀態仍為交易中的訂單
        var playModel = dbContext.Database
            .SqlQuery<Qxun.Activity.Contract.VIPPassRedBag013Play>(@"select * from VIPPassRedBag013Play with(nolock) where ActivitySceneID=199 and[status] = 2 and DATEDIFF(MINUTE, CreateTime, GETDATE()) > 10").ToList();
        if (playModel != null && playModel.Count > 0)
        {
            foreach (var item in playModel)
            {
                using (var trans = new TransactionScope())
                {
                    //提交至微信查詢       
                    string returnCode = "SUCCESS";
                    Random ran = new Random();
                    int time = ran.Next(100);
                    if (time <= 1)
                    {
                        returnCode = "FAIL";
                    }
                    //去查詢微信紅包的信息
                    //模擬網絡延遲
                    Thread.Sleep(time * 100);
                    if (returnCode == "SUCCESS")
                    {
                        item.Status = (int)TradeStatus.Success;
                        item.Remark = "success;time=" + DateTime.Now.ToString();
                        item.FinishTime = DateTime.Now;
                        dbContext.Update<VIPPassRedBag013Play>(item);
                        trans.Complete();
                    }
                    else
                    {
                        Qxun.Activity.Contract.VIPPassRedBag013 model = dbContext.Database
                            .SqlQuery<Qxun.Activity.Contract.VIPPassRedBag013>(@"select * from VIPPassRedBag013 with(updlock) where ActivitySceneID=199")
                            .FirstOrDefault();
                        VIPPassRedBag013Mode mode = dbContext.Database
                            .SqlQuery<Qxun.Activity.Contract.VIPPassRedBag013Mode>(@"select * from VIPPassRedBag013Mode with(updlock) where ID=" + item.VIPPassRedBag013ModeID).FirstOrDefault();
                        model.RedBagBalance += item.Money;
                        dbContext.Update<Qxun.Activity.Contract.VIPPassRedBag013>(model);
                        item.Status = (int)TradeStatus.Fail;
                        item.Remark = "fail;time=" + DateTime.Now.ToString();
                        item.FinishTime = DateTime.Now;
                        dbContext.Update<VIPPassRedBag013Play>(item);
                        mode.RemainCount += 1;
                        dbContext.Update<VIPPassRedBag013Mode>(mode);
                        trans.Complete();
                    }
                }
            }
        }
    }
    return View();
}


PS:經過這樣改進,應該比之前的好多了。當然這樣還是很遠遠不夠的。希望各位路過的大神能夠指點一二,甚是感謝!


免責聲明!

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



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