asp.net通过消息队列处理高并发请求(以抢小米手机为例)
网站面对高并发的情况下,除了增加硬件,优化程序提高以响应速度外,还可以通过并行改串行的思路来解决。这种思想常见的实践方式就是数据库锁和消息队列的方式。这种方式的缺点是需要排队,响应速度慢,优点是节省成本。
演示一下现象
创建一个在售产品表
CREATETABLE[dbo].[product]( [id][int]NOTNULL,--唯一主键 [name][nvarchar](50)NULL,--产品名称 [status][int]NULL,--0未售出1售出默认为0 [username][nvarchar](50)NULL--下单用户 )
添加一条记录
insertintoproduct(id,name,status,username)values(1,'小米手机',0,null)
创建一个抢票程序
publicContentResultPlaceOrder(stringuserName)
{
using(RuanMou2020Entitiesdb=newRuanMou2020Entities())
{
varproduct=db.product.Where(p=>p.status==0).FirstOrDefault();
if(product.status==1)
{
returnContent("失败,产品已经被卖光");
}
else
{
//模拟数据库慢造成并发问题
Thread.Sleep(5000);
product.status=1;
product.username=userName;
db.SaveChanges();
returnContent("成功购买");
}
}
}
如果我们在5秒内一次访问以下两个地址,那么返回的结果都是成功购买且数据表中的username是lisi。
/controller/PlaceOrder?username=zhangsan
/controller/PlaceOrder?username=lisi
这就是并发带来的问题。
第一阶段,利用线程锁简单粗暴
Web程序是多线程的,那我们把他在容易出现并发的地方加一把锁就可以了,如下图处理方式。
privatestaticobject_lock=newobject();
publicContentResultPlaceOrder(stringuserName)
{
using(RuanMou2020Entitiesdb=newRuanMou2020Entities())
{
lock(_lock)
{
varproduct=db.product.Where(p=>p.status==0).FirstOrDefault();
if(product.status==1)
{
returnContent("失败,产品已经被卖光");
}
else
{
//模拟数据库慢造成并发问题
Thread.Sleep(5000);
product.status=1;
product.username=userName;
db.SaveChanges();
returnContent("成功购买");
}
}
}
}
这样每一个请求都是依次执行,不会出现并发问题了。
优点:解决了并发的问题。
缺点:效率太慢,用户体验性太差,不适合大数据量场景。
第二阶段,拉消息队列,通过生产者,消费者的模式
1,创建订单提交入口(生产者)
publicclassHomeController:Controller
{
///
///接受订单提交(生产者)
///
///
publicContentResultPlaceOrderQueen(stringuserName)
{
//直接将请求写入到订单队列
OrderConsumer.TicketOrders.Enqueue(userName);
returnContent("wait");
}
///
///查询订单结果
///
///
publicContentResultPlaceOrderQueenResult(stringuserName)
{
varrel=OrderConsumer.OrderResults.Where(p=>p.userName==userName).FirstOrDefault();
if(rel==null)
{
returnContent("还在排队中");
}
else
{
returnContent(rel.Result.ToString());
}
}
}
2,创建订单处理者(消费者)
//////订单的处理者(消费者) /// publicclassOrderConsumer { //////订票的消息队列 /// publicstaticConcurrentQueueTicketOrders=newConcurrentQueue (); /// ///订单结果消息队列 /// publicstaticListOrderResults=newList (); /// ///订单处理 /// publicstaticvoidStartTicketTask() { stringuserName=null; while(true) { //如果没有订单任务就休息1秒钟 if(!TicketOrders.TryDequeue(outuserName)) { Thread.Sleep(1000); continue; } //执行真实的业务逻辑(如插入数据库) boolrel=newTicketHelper().PlaceOrderDataBase(userName); //将执行结果写入结果集合 OrderResults.Add(newOrderResult(){Result=rel,userName=userName}); } } }
3,创建订单业务的实际执行者
//////订单业务的实际处理者 /// publicclassTicketHelper { //////实际库存标识 /// privateboolhasStock=true; //////执行一个订单到数据库 /// ///publicboolPlaceOrderDataBase(stringuserName) { //如果没有了库存,则直接返回false,防止频繁读库 if(!hasStock) { returnhasStock; } using(RuanMou2020Entitiesdb=newRuanMou2020Entities()) { varproduct=db.product.Where(p=>p.status==0).FirstOrDefault(); if(product==null) { hasStock=false; returnfalse; } else { Thread.Sleep(10000);//模拟数据库的效率比较慢,执行插入时间比较久 product.status=1; product.username=userName; db.SaveChanges(); returntrue; } } } } /// ///订单处理结果实体 /// publicclassOrderResult { publicstringuserName{get;set;} publicboolResult{get;set;} }
4,在程序启动前,启动消费者线程
protectedvoidApplication_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
//在Global的Application_Start事件里单独开启一个消费者线程
Task.Run(OrderConsumer.StartTicketTask);
}
这样程序的运行模式是:用户提交的需求里都会添加到消息队列里去排队处理,程序会依次处理该队列里的内容(当然可以一次取出多条来进行处理,提高效率)。
优点:比上一步快了。
缺点:不够快,而且下单后需要轮询另外一个接口判断是否成功。
第三阶段反转生产者消费者的角色,把可售产品提前放到队列里,然后让提交的订单来消费队列里的内容
1,创建生产者并且在程序启动前调用其初始化程序
publicclassProductForSaleManager
{
///
///待售商品队列
///
publicstaticConcurrentQueueProductsForSale=newConcurrentQueue();
///
///初始化待售商品队列
///
publicstaticvoidInit()
{
using(RuanMou2020Entitiesdb=newRuanMou2020Entities())
{
db.product.Where(p=>p.status==0).Select(p=>p.id).ToList().ForEach(p=>
{
ProductsForSale.Enqueue(p);
});
}
}
}
publicclassMvcApplication:System.Web.HttpApplication
{
protectedvoidApplication_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
//程序启动前,先初始化待售产品消息队列
ProductForSaleManager.Init();
}
}
2,创建消费者
publicclassOrderController:Controller
{
///
///下订单
///
///订单提交者
///
publicasyncTaskPlaceOrder(stringuserName)
{
if(ProductForSaleManager.ProductsForSale.TryDequeue(outintpid))
{
awaitnewTicketHelper2().PlaceOrderDataBase(userName,pid);
returnContent($"下单成功,对应产品id为:{pid}");
}
else
{
awaitTask.CompletedTask;
returnContent($"商品已经被抢光");
}
}
}
3,当然还需要一个业务的实际执行者
//////订单业务的实际处理者 /// publicclassTicketHelper2 { //////执行复杂的订单操作(如数据库) /// ///下单用户 /// 产品id /// publicasyncTaskPlaceOrderDataBase(stringuserName,intpid) { using(RuanMou2020Entitiesdb=newRuanMou2020Entities()) { varproduct=db.product.Where(p=>p.id==pid).FirstOrDefault(); if(product!=null) { product.status=1; product.username=userName; awaitdb.SaveChangesAsync(); } } } }
这样我们同时访问下面三个地址,如果数据库里只有两个商品的话,会有一个请求结果为:商品已经被抢光。
http://localhost:88/Order/PlaceOrder?userName=zhangsan
http://localhost:88/Order/PlaceOrder?userName=lisi
http://localhost:88/Order/PlaceOrder?userName=wangwu
这种处理方式的优点为:执行效率快,相比第二种方式不需要第二个接口来返回查询结果。
缺点:暂时没想到,欢迎大家补充。
说明:该方式只是个人猜想,并非实际项目经验,大家只能作为参考,慎重用于项目。欢迎大家批评指正。
到此这篇关于asp.net通过消息队列处理高并发请求(以抢小米手机为例)的文章就介绍到这了,更多相关asp.net消息队列处理高并发内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!