asp.net c# 经过消息队列处理高并发请求(以抢小米手机为例)

  网站面对高并发的状况下,除了增长硬件, 优化程序提升以响应速度外,还能够经过并行改串行的思路来解决。这种思想常见的实践方式就是数据库锁和消息队列的方式。这种方式的缺点是须要排队,响应速度慢,优势是节省成本。数据库

演示一下现象

建立一个在售产品表多线程

CREATE TABLE [dbo].[product](
    [id] [int] NOT NULL,--惟一主键
    [name] [nvarchar](50) NULL,--产品名称
    [status] [int] NULL ,--0未售出  1 售出  默认为0
    [username] [nvarchar](50) NULL--下单用户
 )

添加一条记录并发

insert into product(id,name,status,username) values(1,'小米手机',0,null)

建立一个抢票程序async

public ContentResult PlaceOrder(string userName)
        {
            using (RuanMou2020Entities db = new RuanMou2020Entities())
            {
                    var product = db.product.Where<product>(p => p.status== 0).FirstOrDefault();
                    if (product.status == 1)
                    {
                        return Content("失败,产品已经被卖光");
                    }
                    else
                    {
                        //模拟数据库慢形成并发问题
                        Thread.Sleep(5000);
                        product.status = 1;
product.username= userName;
              
db.SaveChanges();
              
return Content("成功购买");
             }
      }
    }

若是咱们在5秒内一次访问如下两个地址,那么返回的结果都是成功购买且数据表中的username是lisi。高并发

/controller/PlaceOrder?username=zhangsan大数据

/controller/PlaceOrder?username=lisi优化

这就是并发带来的问题。网站

第一阶段,利用线程锁简单粗暴

Web程序是多线程的,那咱们把他在容易出现并发的地方加一把锁就能够了,以下图处理方式。spa

        private static object _lock = new object();

        public ContentResult PlaceOrder(string userName)
        {
            using (RuanMou2020Entities db = new RuanMou2020Entities())
            {
                lock (_lock)
                {
                    var product = db.product.Where<product>(p => p.status == 0).FirstOrDefault();
                    if (product.status == 1)
                    {
                        return Content("失败,产品已经被卖光");
                    }
                    else
                    {
                        //模拟数据库慢形成并发问题
                        Thread.Sleep(5000);
                        product.status = 1;
                        product.username = userName;
                        db.SaveChanges();
                        return Content("成功购买");
                    }
                }
            }
        }

这样每个请求都是依次执行,不会出现并发问题了。线程

优势:解决了并发的问题。

缺点:效率太慢,用户体验性太差,不适合大数据量场景。

第二阶段,拉消息队列,经过生产者,消费者的模式

1,建立订单提交入口(生产者)

public class HomeController : Controller
    {

        /// <summary>
        /// 接受订单提交(生产者)
        /// </summary>
        /// <returns></returns>
        public ContentResult PlaceOrderQueen(string userName)
        {
            //直接将请求写入到订单队列
            OrderConsumer.TicketOrders.Enqueue(userName);
            return Content("wait");
        }

        /// <summary>
        /// 查询订单结果
        /// </summary>
        /// <returns></returns>
        public ContentResult PlaceOrderQueenResult(string userName)
        {
            var rel = OrderConsumer.OrderResults.Where(p => p.userName == userName).FirstOrDefault();
            if (rel == null)
            {
                return Content("还在排队中");
            }
            else
            {
                return Content(rel.Result.ToString());
            }
        }
}

 

2,建立订单处理者(消费者)

/// <summary>
    /// 订单的处理者(消费者)
    /// </summary>
    public class OrderConsumer
    {
        /// <summary>
        /// 订票的消息队列
        /// </summary>
        public static ConcurrentQueue<string> TicketOrders = new ConcurrentQueue<string>();
        /// <summary>
        /// 订单结果消息队列
        /// </summary>
        public static List<OrderResult> OrderResults = new List<OrderResult>();
        /// <summary>
        /// 订单处理
        /// </summary>
        public static void StartTicketTask()
        {
            string userName = null;
            while (true)
            {
                //若是没有订单任务就休息1秒钟
                if (!TicketOrders.TryDequeue(out userName))
                {
                    Thread.Sleep(1000);
                    continue;
                }
                //执行真实的业务逻辑(如插入数据库)
                bool rel = new TicketHelper().PlaceOrderDataBase(userName);
                //将执行结果写入结果集合
                OrderResults.Add(new OrderResult() { Result = rel, userName = userName });
            }
        }
    }

3,建立订单业务的实际执行者

/// <summary>
    /// 订单业务的实际处理者
    /// </summary>
    public class TicketHelper
    {
        /// <summary>
        /// 实际库存标识
        /// </summary>
        private bool hasStock = true;
        /// <summary>
        /// 执行一个订单到数据库
        /// </summary>
        /// <returns></returns>
        public bool PlaceOrderDataBase(string userName)
        {
            //若是没有了库存,则直接返回false,防止频繁读库
            if (!hasStock)
            {
                return hasStock;
            }
            using (RuanMou2020Entities db = new RuanMou2020Entities())
            {
                var product = db.product.Where(p => p.status == 0).FirstOrDefault();
                if (product == null)
                {
                    hasStock = false;
                    return false;
                }
                else
                {
                    Thread.Sleep(10000);//模拟数据库的效率比较慢,执行插入时间比较久
                    product.status = 1;
                    product.username = userName;
                    db.SaveChanges();
                    return true;
                }
            }
        }
    }
    /// <summary>
    /// 订单处理结果实体
    /// </summary>
    public class OrderResult
    {
        public string userName { get; set; }
        public bool Result { get; set; }
    }

4,在程序启动前,启动消费者线程

protected void Application_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,建立生产者而且在程序启动前调用其初始化程序

public class ProductForSaleManager
    {
        /// <summary>
        /// 待售商品队列
        /// </summary>
        public static ConcurrentQueue<int> ProductsForSale = new ConcurrentQueue<int>();
        /// <summary>
        /// 初始化待售商品队列
        /// </summary>
        public static void Init()
        {
            using (RuanMou2020Entities db = new RuanMou2020Entities())
            {
                db.product.Where(p => p.status == 0).Select(p => p.id).ToList().ForEach(p =>
                {
                    ProductsForSale.Enqueue(p);
                });
            }
        }
    }
 public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            //程序启动前,先初始化待售产品消息队列
            ProductForSaleManager.Init();
        }
    }

2,建立消费者

public class OrderController : Controller
    {
        /// <summary>
        /// 下订单
        /// </summary>
        /// <param name="userName">订单提交者</param>
        /// <returns></returns>
        public async Task<ContentResult> PlaceOrder(string userName)
        {
            if (ProductForSaleManager.ProductsForSale.TryDequeue(out int pid))
            {
                await new TicketHelper2().PlaceOrderDataBase(userName, pid);
                return Content($"下单成功,对应产品id为:{pid}");
            }
            else
            {
                await Task.CompletedTask;
                return Content($"商品已经被抢光");
            }
        }
    }

3,固然还须要一个业务的实际执行者

/// <summary>
    /// 订单业务的实际处理者
    /// </summary>
    public class TicketHelper2
    {
        /// <summary>
        /// 执行复杂的订单操做(如数据库)
        /// </summary>
        /// <param name="userName">下单用户</param>
        /// <param name="pid">产品id</param>
        /// <returns></returns>
        public async Task PlaceOrderDataBase(string userName, int pid)
        {
            using (RuanMou2020Entities db = new RuanMou2020Entities())
            {
                var product = db.product.Where(p => p.id == pid).FirstOrDefault();
                if (product != null)
                {
                    product.status = 1;
                    product.username = userName;
                    await db.SaveChangesAsync();
                }
            }
        }
    }

这样咱们同时访问下面三个地址,若是数据库里只有两个商品的话,会有一个请求结果为:商品已经被抢光。

http://localhost:88/Order/PlaceOrder?userName=zhangsan

http://localhost:88/Order/PlaceOrder?userName=lisi

http://localhost:88/Order/PlaceOrder?userName=wangwu

这种处理方式的优势为:执行效率快,相比第二种方式不须要第二个接口来返回查询结果。

缺点:暂时没想到,欢迎你们补充。

说明:该方式只是我的猜测,并不是实际项目经验,你们只能做为参考,慎重用于项目。欢迎你们批评指正。

相关文章
相关标签/搜索