热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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(@"select * from VIPPassRedBag013 with(updlock) where ActivitySceneID=199").FirstOrDefault();
var mode = dbContext.Database.SqlQuery(@"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
(currentPlayModel);
currentMode.RemainCount
-= 1;
dbContext.Update
(currentMode);
model.RedBagBalance
-= currentMode.Money;
dbContext.Update
(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(@"select * from VIPPassRedBag013 with(updlock) where ActivitySceneID=199").FirstOrDefault();
playModel
= dbContext.Database.SqlQuery(@"select * from VIPPassRedBag013Play with(updlock) where TradeNumber='" + tradeNo + "'").FirstOrDefault();
mode
= dbContext.Database.SqlQuery(@"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
(playModel);
trans.Complete();
retry
= false;
}
else
{
model.RedBagBalance
+= mode.Money;
dbContext.Update
(model);
playModel.Status
= (int)TradeStatus.Fail;
playModel.Remark
= "retry=" + retryCount + ",fail;time=" + DateTime.Now.ToString();
playModel.FinishTime
= DateTime.Now;
dbContext.Update
(playModel);
mode.RemainCount
+= 1;
dbContext.Update
(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
(@"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
(item);
trans.Complete();
}
else
{
Qxun.Activity.Contract.VIPPassRedBag013 model
= dbContext.Database
.SqlQuery
(@"select * from VIPPassRedBag013 with(updlock) where ActivitySceneID=199")
.FirstOrDefault();
VIPPassRedBag013Mode mode
= dbContext.Database
.SqlQuery
(@"select * from VIPPassRedBag013Mode with(updlock) where ID=" + item.VIPPassRedBag013ModeID).FirstOrDefault();
model.RedBagBalance
+= item.Money;
dbContext.Update
(model);
item.Status
= (int)TradeStatus.Fail;
item.Remark
= "fail;time=" + DateTime.Now.ToString();
item.FinishTime
= DateTime.Now;
dbContext.Update
(item);
mode.RemainCount
+= 1;
dbContext.Update
(mode);
trans.Complete();
}
}
}
}
}
return View();
}


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


推荐阅读
  • 本文详细介绍如何在Spring Boot项目中集成和使用JPA,涵盖JPA的基本概念、Spring Data JPA的功能以及具体的操作步骤,帮助开发者快速掌握这一强大的持久化技术。 ... [详细]
  • 设计模式系列-原型模式
    一、上篇回顾上篇创建者模式中,我们主要讲述了创建者的几类实现方案,和创建者模式的应用的场景和特点,创建者模式适合创建复杂的对象,并且这些对象的每个组成部分的详细创建步骤可以是动态的变化的,但 ... [详细]
  • SQLite是一种轻量级的关系型数据库管理系统,尽管体积小巧,却能支持高达2TB的数据库容量,每个数据库以单个文件形式存储。本文将详细介绍SQLite在Android开发中的应用,包括其数据存储机制、事务处理方式及数据类型的动态特性。 ... [详细]
  • 深入浅出:Hadoop架构详解
    Hadoop作为大数据处理的核心技术,包含了一系列组件如HDFS(分布式文件系统)、YARN(资源管理框架)和MapReduce(并行计算模型)。本文将通过实例解析Hadoop的工作原理及其优势。 ... [详细]
  • 本文详细解析了Java中流的概念,特别是OutputStream和InputStream的区别,并通过实际案例介绍了如何实现Java对象的序列化。文章不仅解释了流的基本概念,还探讨了序列化的重要性和具体实现步骤。 ... [详细]
  • 抽象工厂模式 c++
    抽象工厂模式包含如下角色:AbstractFactory:抽象工厂ConcreteFactory:具体工厂AbstractProduct:抽象产品Product:具体产品https ... [详细]
  • 本文探讨了在Node.js环境中如何有效地捕获标准输出(stdout)的内容,并将其存储到变量中。通过具体的示例和解决方案,帮助开发者解决常见的输出捕获问题。 ... [详细]
  • 本文探讨了SQLAlchemy ORM框架中如何利用外键和关系(relationship)来建立表间联系,简化复杂的查询操作。通过示例代码详细解释了relationship的定义、使用方法及其与外键的相互作用。 ... [详细]
  • 本文档提供了详细的MySQL安装步骤,包括解压安装文件、选择安装类型、配置MySQL服务以及设置管理员密码等关键环节,帮助用户顺利完成MySQL的安装。 ... [详细]
  • 深入解析Android Activity生命周期
    本文详细探讨了Android中Activity的生命周期,通过实例代码和详细的步骤说明,帮助开发者更好地理解和掌握Activity各个阶段的行为。 ... [详细]
  • Navicat Premium中MySQL用户管理:创建新用户及高级设置
    本文作为Navicat Premium用户管理系列的第二部分,主要介绍如何创建新的MySQL用户,包括设置基本账户信息、密码策略、账户限制以及SSL配置等。 ... [详细]
  • 帝国cms各数据表有什么用
    CMS教程|帝国CMS帝国cmsCMS教程-帝国CMS精易编程助手源码,ubuntu桥接设置,500错误是tomcat吗,爬虫c原理,php会话包括什么,营销seo关键词优化一般多 ... [详细]
  • 时序数据是指按时间顺序排列的数据集。通过时间轴上的数据点连接,可以构建多维度报表,揭示数据的趋势、规律及异常情况。 ... [详细]
  • 构建高性能Feed流系统的设计指南
    随着移动互联网的发展,Feed流系统成为了众多社交应用的核心组成部分。本文将深入探讨如何设计一个高效、稳定的Feed流系统,涵盖从基础架构到高级特性的各个方面。 ... [详细]
  • 本文探讨了在Qt框架下实现TCP多线程服务器端的方法,解决了一个常见的问题:服务器端仅能与最后一个连接的客户端通信。通过继承QThread类并利用socketDescriptor标识符,实现了多个客户端与服务器端的同时通信。 ... [详细]
author-avatar
手机用户2602897931
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有