最近几年想必大家一听到哪里有抢红包可以抢,马上会拿起手机点去~~~~然后问题来了。。。

如何控制在同一时间保证数据库中扣减红包余额不会出错。之前我们的做法是直接锁程序,这样子带来的坏处就是等待时间太长,每当一个线程进去之后要经过以下几个过程。

过程分别是

1. 查表

2. 校验信息

3. 发送微信服务器

4. 等待反馈

5. 更新表

等这些过程结束之后才轮到下面这个过程。想必这样要等到花儿都谢了~

另外发送微信服务器这个过程时间在0s至9s时间不等。会产生大量的空闲时间,这里CPU会产生大量的空闲。而且这种情况也无法继续做负载均衡,如果有多个站点部署必定会产生数据库并发问题。

若在查表之前加锁更新后释放掉,虽然说不会产生数据库并发。但是在第二个线程进入查询的时候他会一直在等待,其耗时则与更锁程序差不多。

改进

这个想法源于分布式事务的设计,采用预扣红包余额的方式来保证无需等待微信服务器反馈,让下一个线程可继续执行相关任务。当微信服务器反馈回来时,才开始另外一个事务去更改交易状态。若反馈结果为FAIL则需要预扣的红包余额进行还原操作。

粗略写了模拟实际环境的测试代码,模拟抢红包动作

private void task()
{
for (int i = ; i < ; 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();
//得到领取红包的金额
VIPPassRedBag013Mode currentMode = null;
foreach (var modeItem in mode)
{
if (modeItem.RemainCount > )
{
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 * );//要支付的金额(存入到表的)
currentPlayModel.TradeNumber = tradeNo;
currentPlayModel.Status = (int)TradeStatus.Trading;
currentPlayModel.VIPPassRedBag013ModeID = currentMode.ID;
currentPlayModel.ActivitySceneID = ;
dbContext.Insert<VIPPassRedBag013Play>(currentPlayModel);
currentMode.RemainCount -= ;
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();
if (time <= )
{
returnCode = "FAIL";
}
//模拟网络延迟
Thread.Sleep(time * );
//设置重新尝试次数
bool retry = true;
int retryCount = ;
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 += ;
dbContext.Update<VIPPassRedBag013Mode>(mode);
trans.Complete();
retry = false;
}
}
}
}
catch (Exception ex)
{
//如果之前的线程请求数据库时阻塞
//如果执行失败
retryCount++;
retry = true;
}
if (retryCount > )
{
break;
}
} while (retry);
}
}

模拟100个人并发抢红包

public ActionResult Excute()
{
for (int i = ; i < ; 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 > )
{
foreach (var item in playModel)
{
using (var trans = new TransactionScope())
{
//提交至微信查询
string returnCode = "SUCCESS";
Random ran = new Random();
int time = ran.Next();
if (time <= )
{
returnCode = "FAIL";
}
//去查询微信红包的信息
//模拟网络延迟
Thread.Sleep(time * );
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 += ;
dbContext.Update<VIPPassRedBag013Mode>(mode);
trans.Complete();
}
}
}
}
}
return View();
}

PS:经过这样改进,应该比之前的好多了。当然这样还是很远远不够的。希望各位路过的大神能够指点一二,甚是感谢!

EF+SQLSERVER控制并发下抢红包减余额(改进)的更多相关文章

  1. SQLServer控制用户访问权限表

    连接地址:http://www.cnblogs.com/yxyht/archive/2013/03/22/2975880.html 一.需求 在管理数据库过程中,我们经常需要控制某个用户访问数据库的权 ...

  2. (1)C# 创建ef sqlserver

    连接sql 如果报错不能连接的错误 把这三个IP地址的端口号设置上,并启用.第一个18.6是本机ip,之后就可以测试了 最后重启服务器

  3. sqlserver,mysql,oracle通用的模拟和改进的全文搜索算法

    问:数据库效率最低的地方是什么? 答:表扫描 问:表扫描常见的情况是 答:like '%a%' 这类查询 如果使用全文检索引擎,又无法满足我们的需求的时候怎么办,比如要从 一个商品名称 "农 ...

  4. (转)自己来控制EntityFramework4.1 Code-First,逐步消除EF之怪异现象

    转自:http://www.cnblogs.com/richwong/archive/2011/07/06/2098759.html 最近的项目开始使用EF4.1,拜读各路大侠文章数遍,满以为可以轻车 ...

  5. 调试EF源代码环境配置

    下载EF6的源代码,运行build编译,Nuget会自动下载所需的DLL. 打开EF的工程,可以在EF解决方案下直接新建调试用的项目代码 添加EF引用时选择解决方案中的Entity Framework ...

  6. [翻译][MVC 5 + EF 6] 12[完结]:高级场景

    原文:Advanced Entity Framework 6 Scenarios for an MVC 5 Web Application 1.执行原生SQL查询: EF Code First API ...

  7. sqlserver Distributed Transaction 分布式事务

    在webapi+ef+sqlserver开发项目时,利用transcope实现应用层级的事务时,偶尔会报分布式事务错误,而且很而复现,特别蛋疼.现将自己的解决方法初步整理下. 分析原因:搭建repos ...

  8. ECDSA—模加减模块

    如果a,b GF(P),则加法运算a+b=r (mod p),其中r满足0<r<p-1,即a+b除以p的余数,该操作成为模p加法.对于模减运算可以视为另类的模加运算,即a+(-b)=k ( ...

  9. 有关linqtosql和EF的区别

    LINQ to SQL和Entity Framework都是一种包含LINQ功能的对象关系映射技术.他们之间的本质区别在于EF对数据库架构和我们查询的类型实行了更好的解耦.使用EF,我们查询的对象不再 ...

随机推荐

  1. iOS开发——打包静态库与Framework

    最近老是需要做接口给别的客户,就顺便把打包的过程也写一下吧! 一.静态库 静态图里面只能是纯文件,里面不能再有第三方打包的静态库,也就是说,静态库不能打包静态库.这个用的比较多,一般自己公司写出来的东 ...

  2. iOS推送的众多坑

    新来的一家公司,昨天和同事解决推送问题(工程里有集成百度推送和环信即时通讯),信誓旦旦的声称:" app在前台和后台运行时,推送触发的是didReceiveRemoteNotificatio ...

  3. linux 驱动入门6

    看/sys目录经常看到bus device driver class. 这也是网上大量说的驱动驱动模型.这些的关系得熟悉得明白吧.是的.今天我先不整他们的关系.先逐个击破,然后再统一来理清楚他们之间的 ...

  4. form表单传递下拉框的Value和Text值,不适用Jquery传递

    同时获取下拉框的Value和Text值的解决办法:添加一个<input type="text" >文本框,用户选中一项后就将该项的value值给他 然后接受页面获取该文 ...

  5. STM32+NRF24L01无线(转)

    源:STM32+NRF24L01无线 硬件SPI和模拟SPI源码: nrf24发送(模拟SPI)BHS-STM32.rar nrf24接收(模拟SPI)BHS-STM32.rar nrf24发送(硬件 ...

  6. 修改替换/system/framework/framework.jar后重启手机为何没有效果?

    自Android 5.0开始android默认使用art(Android4.4开始有实验性质的art),取代原来的Dalvik, art会加载boot.art和boot.oat两个文件(静态编译优化, ...

  7. IOS9提示“不受信任的开发者”如何处理

    iPhone升级到IOS9版本后,发现部分APP在下载后首次运行时,都会提示“不受信任的应用程序开发者”,这是因为企业证书发布的APP,没有经过AppStore审核,于是iOS对用户做出一个安全性的提 ...

  8. phpcms的网页替换

    //替换首页header:loge里面的首页不用替换<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ...

  9. iOS开发 调用系统相机和相册 分类: ios技术 2015-03-30 15:52 65人阅读 评论(0) 收藏

     调用系统相机和相册 (iPad,iPhone) 打开相机:(iPad,iPhone) //先设定sourceType为相机,然后判断相机是否可用(ipod)没相机,不可用将sourceType设定为 ...

  10. 微信小程序之----video视频播放

    vidao 我现在看到的官方文档是不带danmu(弹幕)属性的,之前是有的,不过现在这个属性还可以生效.控制视频的状态可以根据video标签的唯一id得到一个对象实例.video组件并不具备actio ...