关于.net高并发商品秒杀方案(纯DB,lock,服务器缓存+队列)

新建GrabbingOrdersController.cs控制器,可以开始制作测试接口了

Demo一共有8个接口,两个只做查询(不在文章里描述后续会上源代码),纯db接口、lock锁、服务器缓存+队列、Redis+队列、Redis_lua、rabbit

开始搬砖了
Controllers文件夹新建控制器(api 控制器 空),引用日志接口和数据库连接类

namespace MessageQueuing.Controllers
{
    /// <summary>
    /// 抢单
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class GrabbingOrdersController : ControllerBase
    {
        private readonly ILogger<GrabbingOrdersController> logger;
        private readonly DbHelperContext dbHelper;

        public GrabbingOrdersController(ILogger<GrabbingOrdersController> logger, DbHelperContext dbHelper)
        {
            this.logger = logger;
            this.dbHelper = dbHelper;
        } 
    }
}

一.纯DB接口:在控制器中直接处理查询和新增订单等业务

业务流程:接口触发->DB查询数据->新增订单\扣除库存

功能分析:

  1. 响应较慢,高并发下容易出现错误
  2. 库存扣除出现异常
  3. 产品存在超卖现象

压力测试:
线程数为10,100,1000三种情况进行测试,Ramp-Up时间空,循环次数为1

代码:

        [HttpGet]
        [Route("[action]")]
        public string SetOrder(string userId, string proId)
        {
            try
            {
                var product = dbHelper.SeckillProduct.Where(x => x.productId == proId).FirstOrDefault();
                if (product.productStockNum - 1 > 0)
                {
                    product.productStockNum--;
                    Models.Order order = new Models.Order();
                    order.id = Guid.NewGuid().ToString("N");
                    order.userId = userId;
                    order.orderNum = Guid.NewGuid().ToString("N");
                    order.productId = product.id;
                    order.orderTotal = Convert.ToDecimal(product.productPrice);
                    order.addTime = DateTime.Now;

                    order.orderStatus = 0;
                    order.orderPhone = "1565555555";
                    order.orderAddress = "test";
                    order.delFlag = 0;
                    dbHelper.Add(order);
                    dbHelper.SaveChanges();
                    return "下单成功";
                }
                else
                {
                    return "下单成功";
                }
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }

测试结果:1000并发下异常为0说明都正常触发了,但如果查看数据会发现库存并没有扣1000
在这里插入图片描述

二、lock锁: 使用lock锁,在处理业务时不让其它进程出入,直到进程结束为止,这样能解决库存错误导致多卖的情况

业务流程:
接口触发-> 单线程进入其余等待->DB查询数据->新增订单\扣除库存->业务完成,释放线程->其它线程进入

功能分析:

  1. 响应很慢,高并发的情况下容易导致请求排队时间过长而后报错
  2. 解决库存错误

压力测试:
线程数为10,100,1000三种情况进行测试,Ramp-Up时间空,循环次数为1

代码:
首先控制器里建一个静态只读的object
在这里插入图片描述新建SetOrderLock,lock调用建好的(object)

        [HttpGet]
        [Route("[action]")]
        public string SetOrderLock(string userId, string proId)
        {
            try
            {
                lock (objlock)
                {
                    var product = dbHelper.SeckillProduct.Where(x => x.productId == proId).FirstOrDefault();
                    if (product.productStockNum - 1 > 0)
                    {
                        product.productStockNum--;
                        Models.Order order = new Models.Order();
                        order.id = Guid.NewGuid().ToString("N");
                        order.userId = userId;
                        order.orderNum = Guid.NewGuid().ToString("N");
                        order.productId = product.id;
                        order.orderTotal = Convert.ToDecimal(product.productPrice);
                        order.addTime = DateTime.Now;

                        order.orderStatus = 0;
                        order.orderPhone = "1565555555";
                        order.orderAddress = "test";
                        order.delFlag = 0;
                        dbHelper.Add(order);
                        dbHelper.SaveChanges();
                        return "下单成功";
                    }
                    else
                    {
                        return "下单成功";
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }

测试结果:慢于纯db接口,但不会出现库存错误的情况
在这里插入图片描述

三、服务器缓存+队列:生产者和消费者模式,接口只负责向计算库存和新增随机订单号然后写入缓存中,会建两个后台任务进行处理插入和查询

业务流程:
接口触发-> 利用lock来处理简单的库存和新增随机订单号->利用后台任务进行查询数据->新增订单\扣除库存

功能分析:

  1. 同时解决响应慢和库存错误问题
  2. 如服务器异常容易导致缓存里的信息丢失

压力测试:
线程数为10,100,1000三种情况进行测试,Ramp-Up时间空,循环次数为1

代码:
在项目里新建Common文件夹,然后新建CacheBackService.cs、CustomerService.cs和MyQueue.cs

MyQueue.cs:基于内存的队列

        private static ConcurrentQueue<string> _queue = new ConcurrentQueue<string>();
        public static ConcurrentQueue<string> GetQueue()
        {
            return _queue;
        }

CacheBackService.cs:用来初始化库存等信息

    public class CacheBackService : BackgroundService
    {
        private IMemoryCache _cache;
        private IConfiguration _Configuration;

        public CacheBackService(IMemoryCache cache, IConfiguration Configuration)
        {
            _cache = cache;
            _Configuration = Configuration;
        }
        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            // EFCore的上下文默认注入的请求内单例的,而CacheBackService要注册成全局单例的
            // 由于二者的生命周期不同,所以不能相互注入调用,这里手动new一个EF上下文
            var optionsBuilder = new DbContextOptionsBuilder<DbHelperContext>();
            optionsBuilder.UseSqlServer(_Configuration[string.Join(":", new string[] { "DBConnection", "ConnectionStrings", "Dbconn" })]);
            DbHelperContext context = new DbHelperContext(optionsBuilder.Options);

            //初始化库存信息,连临时写在这个位置,充当服务器启动的时候初始化
            var data = await context.SeckillProduct.Where(u => u.id == "21e86c6cc32b4e7bb80f96c98e4e7994").FirstOrDefaultAsync();
            //服务器缓存
            _cache.Set<int>($"{data.productId}-sCount", data.productStockNum);
        }
}

CustomerService.cs:处理消费业务

    public class CustomerService: BackgroundService
    {
        private IConfiguration _Configuration;
        public CustomerService(IConfiguration Configuration)
        {
            _Configuration = Configuration;
        }
        protected async override Task ExecuteAsync(CancellationToken stoppingToken)
        {

            // EFCore的上下文默认注入的请求内单例的,而CacheBackService要注册成全局单例的
            // 由于二者的生命周期不同,所以不能相互注入调用,这里手动new一个EF上下文
            var optionsBuilder = new DbContextOptionsBuilder<DbHelperContext>();
            optionsBuilder.UseSqlServer(_Configuration[string.Join(":", new string[] { "DBConnection", "ConnectionStrings", "Dbconn" })]);
            DbHelperContext context = new DbHelperContext(optionsBuilder.Options);

            Console.WriteLine("下面开始执行消费业务");
            while (true)
            {
                try
                {
                    string data = "";
                    MyQueue.GetQueue().TryDequeue(out data);
                    if (!string.IsNullOrEmpty(data))
                    {
                        List<string> tempData = data.Split('-').ToList();
                        //1.扣减库存---禁止状态追踪
                        var sArctile =await context.SeckillProduct.Where(u => u.id == "21e86c6cc32b4e7bb80f96c98e4e7994").FirstOrDefaultAsync();
                        sArctile.productStockNum = sArctile.productStockNum - 1;
                        context.Update(sArctile);

                        //2. 插入订单信息
                        Order tOrder = new Order();
                        tOrder.id = Guid.NewGuid().ToString("N");
                        tOrder.userId = tempData[0];
                        tOrder.orderNum = Guid.NewGuid().ToString("N");
                        tOrder.productId = tempData[1];
                        tOrder.orderTotal = 50;
                        tOrder.addTime = DateTime.Now;

                        tOrder.orderStatus = 0;
                        tOrder.orderPhone = "1565555555";
                        tOrder.orderAddress = "test";
                        tOrder.delFlag = 0;
                        context.Add<Order>(tOrder);
                        int count = await context.SaveChangesAsync();

                        //释放一下
                        context.Entry<SeckillProduct>(sArctile).State = EntityState.Detached;
                        Console.WriteLine($"执行成功,条数为:{count},当前库存为:{ sArctile.productStockNum}");
                    }
                    else
                    {
                        Console.WriteLine("暂时没有订单信息,休息一下");
                        await Task.Delay(TimeSpan.FromSeconds(1));
                    }
                }
                catch (Exception ex)
                {

                    Console.WriteLine($"执行失败:{ex.Message}");
                }
            }
        }
    }

控制器引入IMemoryCache接口,

        private static readonly object objlock = new object();
        private readonly ILogger<GrabbingOrdersController> logger;
        private readonly DbHelperContext dbHelper;
        private readonly IMemoryCache cache;
        public GrabbingOrdersController(ILogger<GrabbingOrdersController> logger, DbHelperContext dbHelper, IMemoryCache cache)
        {
            this.logger = logger;
            this.dbHelper = dbHelper;
            this.cache = cache;
        }

然后新建SetOrderServercache

        [HttpGet]
        [Route("[action]")]
        public string SetOrderServercache(string userId, string proId)
        {
            try
            {
                lock (objlock)
                {
                    int count = cache.Get<int>($"{proId}-sCount");
                    if (count - 1 >= 0)
                    {
                        count--;
                        cache.Set<int>($"{userId}-sCount", count);

                        var orderNum = Guid.NewGuid().ToString("N");
                        MyQueue.GetQueue().Enqueue($"{userId}-{proId}-{orderNum}");

                        return "下单成功";
                    }
                    else
                    {
                        return "下单成功";
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
        }

最后到Startup.cs注册后台任务

            //注册后台任务
            services.AddHostedService<CacheBackService>();
            services.AddHostedService<CustomerService>();

测试结果:
在这里插入图片描述

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值