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

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

过程分别是

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

EF+SQLSERVER控制并发下抢红包减余额(改进)相关推荐

  1. 一套WPF+EF+SQLServer 会员卡管理系统源码

    今天分享的是一套WPF+EF+SQLServer 会员卡管理系统源码,界面使用Panuon.WPF.UI,图表工具使用LiveCharts.Wpf,麻雀虽小五脏俱全,该源码作Panuon.WPF.UI ...

  2. java 怎么保证余额_高并发下怎么做余额扣减?

    余额操作在大多数系统都是不可缺少和不允许出现问题的 如何修改余额 , 这个问题可能在实际项目中 没那么简单; 如何修改余额 假设一个用户数据 : id⇒12 | user_name⇒放放 | fee⇒ ...

  3. 怎么解决高并发下抢红包和商品超卖问题?

    一.场景模拟 在抢红包或秒杀商品的时候,肯定会有高并发的情况出现,程序中如果出现库存重复减扣的情况,那肯定是不行的!接下来模拟一下高并发下的库存重复减扣问题以及相应的解决方案. 1.  在测试前,需要 ...

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

    一.需求 在管理数据库过程中,我们经常需要控制某个用户访问数据库的权限,比如只需要给这个用户访问某个表的权限,甚至是CRUD的权限,更小粒度的还可以去到某几个字段的访问权限.写这篇文章就是说明下这个操 ...

  5. ef sqlserver切换到mysql_可以为MySql和SqlServer使用EF上下文吗?

    我有两个实体框架上下文,一个用于MySql,一个用于sql.如果我运行该应用程序,则会出现以下错误 The default DbConfiguration instance was used by t ...

  6. html上下箭头控制文本框加减,HTML5去掉输入框type为number时的上下箭头的实现方法...

    html5中,input type="number"时 右边会有一个上下小箭头,介绍去掉这个箭头的方法,完成浏览器的兼容,页面效果的统一 一.公共样式 去掉输入框type为numb ...

  7. 没有发布服务器的 rpc 安全信息,或该信息无效,SQLServer之创建分布式事务

    分布式事务创建注意事项 指定一个由 Transact-SQL 分布式事务处理协调器 (MS DTC) 管理的 Microsoft 分布式事务的起点. 执行 BEGIN DISTRIBUTED TRAN ...

  8. java 余额_关于java:Java并发编程根源为什么转账后余额总是对不上

    你开发了一套转账零碎,转账的流程没问题,通过了内部测试,上线后看起来也没问题. 然而,过了一段时间,用户竟然能够忽视余额,间接提现.眼看就要就业了,问题到底出在哪里呢? 通过一番查看,你发现每次出事的 ...

  9. sqlserver Distributed Transaction 分布式事务

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

最新文章

  1. python macd背离_Python 精简多品种 MACD 趋势策略
  2. 深入浅出:Linux设备驱动之字符设备驱动
  3. Huawei S8512
  4. 同步I/O 和 异步I/O
  5. 山西计算机应用能力,《山西省计算机应用能力考试练习系统》使用说明
  6. 开课吧9.9元学python靠谱吗-开课吧的python课程怎么样,值得报名吗?
  7. 如何在 macOS 中使用选项卡?
  8. 排序算法之九 基数排序(C++版本)
  9. Unity代码分享——一个快速显示所有Sprite为Image_icon的帮助类
  10. 网易云音乐代码如何写入html,如何将网易云音乐加入到自己的网站!
  11. 黑客穷追不舍攻击“谷姐” [转]
  12. 80后一代开始结婚 独生子女开始承担新的责任
  13. 推荐几个短网址赚钱站
  14. ArcGIS配图/地图符号化的一些技巧与相关资料
  15. 处处吻(粤语汉字英译)
  16. empyrical 模块的学习与分析 note4
  17. 关于设计抗混叠滤波器的三个指导原则
  18. 参数方程求二阶导时候不能直接把y,x分别对t求二阶导然后再相除作为d²y/dx²的原因
  19. 数说故事香氛品类分析及行业新趋势、消费者需求洞察
  20. mybatis 的 insert

热门文章

  1. CSS3 设置特殊字体的方法@font-face
  2. 数据可视化之不婚主义,34%的不婚人群把婚姻和恋爱分为两件事
  3. 供电门面怎么实施RPA提高产品分类准确率
  4. python opencv数字识别_基于模板匹配的手写数字识别(python+opencv)
  5. Manico—这才是你需要的Mac快速启动器
  6. 防火墙硬件_硬件防火墙提供的保护
  7. 2022-2027年中国海上保险行业发展监测及投资战略研究报告
  8. 电脑剪切的文件不显示如何恢复
  9. DivX/XviD编解码器说明
  10. 【pandas数据清洗与处理】项目7-国产烂片深度分析