当前位置:   article > 正文

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

.net lock(db)

新建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;
        } 
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

一.纯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);
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

测试结果: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);
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

测试结果:慢于纯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;
        }
  • 1
  • 2
  • 3
  • 4
  • 5

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);
        }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

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}");
                }
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

控制器引入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;
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

然后新建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);
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/84205
推荐阅读
  

闽ICP备14008679号