最近幾年想必大家一聽到哪裡有搶紅包可以搶,馬上會拿起手機點去~~~~然後問題來了。。。 如何控制在同一時間保證資料庫中扣減紅包餘額不會出錯。之前我們的做法是直接鎖程式,這樣子帶來的壞處就是等待時間太長,每當一個線程進去之後要經過以下幾個過程。 過程分別是 1. 查表 2. 校驗信息 3. 發送微信服 ...
最近幾年想必大家一聽到哪裡有搶紅包可以搶,馬上會拿起手機點去~~~~然後問題來了。。。
如何控制在同一時間保證資料庫中扣減紅包餘額不會出錯。之前我們的做法是直接鎖程式,這樣子帶來的壞處就是等待時間太長,每當一個線程進去之後要經過以下幾個過程。
過程分別是
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:經過這樣改進,應該比之前的好多了。當然這樣還是很遠遠不夠的。希望各位路過的大神能夠指點一二,甚是感謝!