最近几年想必你们一听到哪里有抢红包能够抢,立刻会拿起手机点去~~~~而后问题来了。。。数据库
如何控制在同一时间保证数据库中扣减红包余额不会出错。以前咱们的作法是直接锁程序,这样子带来的坏处就是等待时间太长,每当一个线程进去以后要通过如下几个过程。服务器
过程分别是微信
1. 查表网络
2. 校验信息并发
3. 发送微信服务器负载均衡
4. 等待反馈dom
5. 更新表分布式
等这些过程结束以后才轮到下面这个过程。想必这样要等到花儿都谢了~测试
另外发送微信服务器这个过程时间在0s至9s时间不等。会产生大量的空闲时间,这里CPU会产生大量的空闲。并且这种状况也没法继续作负载均衡,若是有多个站点部署一定会产生数据库并发问题。spa
若在查表以前加锁更新后释放掉,虽说不会产生数据库并发。可是在第二个线程进入查询的时候他会一直在等待,其耗时则与更锁程序差很少。
改进
这个想法源于分布式事务的设计,采用预扣红包余额的方式来保证无需等待微信服务器反馈,让下一个线程可继续执行相关任务。当微信服务器反馈回来时,才开始另一个事务去更改交易状态。若反馈结果为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:通过这样改进,应该比以前的好多了。固然这样仍是很远远不够的。但愿各位路过的大神可以指点一二,甚是感谢!