关于.net高并发商品秒杀方案(Redis+队列+Lua脚本)

五、Redis_lua: Lua是redis的轻量脚本语言,把部分需要在业务里实现的功能写在脚本然后嵌入到程序中,把它当作“存储过程”就比较容易理解了。其实我也不熟,就是来装一下。

其实就是把上篇Redis+队列的“单品限流、购买次数的限制和方法幂等”写到lua里然后嵌入到程序中

功能分析:

  1. 减少网络开销,将业务放到脚本中执行
  2. 脚本会做作为一个整体执行,不会插入其它命令
  3. 脚本复用
  4. 缺点是出错不会回滚

Lua基础语法:

  1. 基本结构,类似于js,前面声明方法,后面调用方法。
  2. 参数通过ARGV[1]获取,Key通过KEYS[1]获取
  3. redis.call( )方法调用api
  4. int类型需要转换 tonumber

lua环境搭建:
https://blog.csdn.net/sarono/article/details/94266595
https://www.runoob.com/lua/lua-environment.html

本章我们使用新的Redis NuGet包:CSRedisCore。官方推荐库,支持 redis-trib集群、哨兵、私有分区与连接池管理技术,简易 RedisHelper 静态类。

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

首先到NuGet包管理器中安装CSRedisCore

新建Luas文件夹,新建SeckillLua.lua和SeckillLuaCallback.lua。在新建项的时候可以选择文本文件然后把后缀名改成lua就行。

SeckillLua.lua:

--[[本脚本主要整合:单品限流、购买的商品数量限制、方法幂等、扣减库存的业务]]

--[[. 方法声明
]]--

--1. 单品限流--解决缓存覆盖问题
local function seckillLimit()
--(1).获取相关参数
-- 限制请求数量
local tLimits=tonumber(ARGV[1]);
-- 限制秒数
local tSeconds =tonumber(ARGV[2]);
-- 受限商品key
local limitKey = ARGV[3];
--(2).执行判断业务
local myLimitCount = redis.call('INCR',limitKey);

-- 仅当第一个请求进来设置过期时间
if (myLimitCount ==1) 
then
redis.call('expire',limitKey,tSeconds) --设置缓存过期
end;   --对应的是if的结束

-- 超过限制数量,返回失败
if (myLimitCount > tLimits) 
then
return 0;  --失败
end;   --对应的是if的结束

end;   --对应的是整个代码块的结束


--2. 限制一个用户商品购买数量(这里假设一次购买一件,后续改造)
local function userBuyLimit()
--(1).获取相关参数
local tGoodBuyLimits = tonumber(ARGV[5]); 
local userBuyGoodLimitKey = ARGV[6]; 

--(2).执行判断业务
local myLimitCount = redis.call('INCR',userBuyGoodLimitKey);
if (myLimitCount > tGoodBuyLimits)
then
return 0;  --失败
else
redis.call('expire',userBuyGoodLimitKey,600)  --10min过期
return 1;  --成功
end;
end;    --对应的是整个代码块的结束

--3. 方法幂等(防止网络延迟多次下单)
--local function recordOrderSn()
--(1).获取相关参数
--local requestId = ARGV[7];    --请求ID
--(2).执行判断业务
--local requestIdNum = redis.call('INCR',requestId);
--表示第一次请求
--if (requestIdNum==1)                            
--then
--redis.call('expire',requestId,600)  --10min过期
--return 1; --成功
--end;
--第二次及第二次以后的请求
--if (requestIdNum>1)
--then
--return 0;  --失败
--end;
--end;  --对应的是整个代码块的结束

--4、扣减库存
local function subtractSeckillStock()
--(1) 获取相关参数
--local key =KEYS[1];   --传过来的是ypf12345没有什么用处
--local arg1 = tonumber(ARGV[1]);--购买的商品数量
-- (2).扣减库存
-- local lastNum = redis.call('DECR',"sCount");
local lastNum = redis.call('DECRBY',ARGV[8],tonumber(ARGV[4]));  --string类型的自减
-- (3).判断库存是否完成
if lastNum < 0 
then
return 0; --失败
else
return 1; --成功
end
end



--[[. 方法调用   返回值1代表成功,返回:0234 代表不同类型的失败
]]--

--1. 单品限流调用
local status1 = seckillLimit();
if status1 == 0 then
return 2;   --失败
end

--2. 限制购买数量
local status2 = userBuyLimit();
if status2 == 0 then
return 3;   --失败
end


--3.  方法幂等
--local status3 = recordOrderSn();
--if status3 == 0 then
--return 4;   --失败
--end


--4.扣减秒杀库存
local status4 = subtractSeckillStock();
if status4 == 0 then
return 0;   --失败
end
return 1;    --成功

SeckillLuaCallback.lua:

--[[本脚本主要整合:单品限流、购买的商品数量限制、方法幂等、扣减库存的业务的回滚操作]]

--[[. 方法声明
]]--

--1.单品限流恢复
local function RecoverSeckillLimit()
local limitKey = ARGV[1];-- 受限商品key
redis.call('INCR',limitKey);
end;

--2.恢复用户购买数量
local function RecoverUserBuyNum()
local userBuyGoodLimitKey =  ARGV[2]; 
local goodNum = tonumber(ARGV[5]); --商品数量
redis.call("DECRBY",userBuyGoodLimitKey,goodNum);
end

--3.删除方法幂等存储的记录
local function DelRequestId()
local userRequestId = ARGV[3];  --请求ID
redis.call('DEL',userRequestId);
end;

--4. 恢复订单原库存
local function RecoverOrderStock()
local stockKey = ARGV[4];  --库存中的key
local goodNum = tonumber(ARGV[5]); --商品数量
redis.call("INCRBY",stockKey,goodNum);
end;

--[[. 方法调用
]]--
RecoverSeckillLimit();
RecoverUserBuyNum();
DelRequestId();
RecoverOrderStock();

在项目的Common文件夹(不是Common类库)新建LCacheBackService.cs、LCustomerService.cs和LuasLoadService.cs(加载lua脚本到缓存)

LCacheBackService.cs:

    public class LCacheBackService: BackgroundService
    {
        private readonly IConfiguration _Configuration;

        public LCacheBackService(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 _dbHelper = new DbHelperContext(optionsBuilder.Options);
            var data = await _dbHelper.SeckillProduct.Where(x => x.id == "21e86c6cc32b4e7bb80f96c98e4e7998").FirstOrDefaultAsync();
            RedisHelper.Set($"{data.productId}-sCount", data.productStockNum);
        }
}

LCustomerService.cs:

    public class LCustomerService: BackgroundService
    {
        private readonly IConfiguration _configuration;

        public LCustomerService(IConfiguration configuration)
        {
            _configuration = configuration;
        }
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            var optionsBuilder = new DbContextOptionsBuilder<DbHelperContext>();
            optionsBuilder.UseSqlServer(_configuration[string.Join(":", new string[] { "DBConnection", "ConnectionStrings", "Dbconn" })]);
            return Task.Run(() =>
            {
                List<string> orderlist = new List<string>();
                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();
                while (true)
                {
                    try
                    {
                        var data = RedisHelper.RPop("21e86c6cc32b4e7bb80f96c98e4e0005");
                        if (!string.IsNullOrEmpty(data))
                        {
                            orderlist.Add(data);
                        }
                        else
                        {
                            Console.WriteLine();
                            Thread.Sleep(1000);
                        }
                        if (orderlist.Count >= 50 || stopwatch.ElapsedMilliseconds > 2000)
                        {
                            if (orderlist.Count > 0)
                            {
                                using (DbHelperContext _dbHelper = new DbHelperContext(optionsBuilder.Options))
                                {
                                    using (var beginTransaction = _dbHelper.Database.BeginTransaction())
                                    {
                                        try
                                        {
                                            var product = _dbHelper.SeckillProduct.Where(x => x.id == "21e86c6cc32b4e7bb80f96c98e4e7998").FirstOrDefault();
                                            int count = product.productStockNum - orderlist.Count;
                                            int count2 = _dbHelper.SeckillProduct.Where(x => x.id == "21e86c6cc32b4e7bb80f96c98e4e7998")
                                            .BatchUpdate(new SeckillProduct { productStockNum = count });
                                            List<Models.Order> orders = new List<Models.Order>();
                                            foreach (var item in orderlist)
                                            {
                                                List<string> tempData = item.Split('-').ToList();
                                                //2. 插入订单信息
                                                Models.Order tOrder = new Models.Order();
                                                tOrder.id = Guid.NewGuid().ToString("N");
                                                tOrder.userId = tempData[0];
                                                tOrder.orderNum = Guid.NewGuid().ToString("N");
                                                tOrder.productId = tempData[1];
                                                tOrder.orderTotal = product.productPrice;
                                                tOrder.addTime = DateTime.Now;

                                                tOrder.orderStatus = 0;
                                                tOrder.orderPhone = "1565555555";
                                                tOrder.orderAddress = "test";
                                                tOrder.delFlag = 0;
                                                orders.Add(tOrder);
                                            }
                                            _dbHelper.BulkInsert(orders);
                                            beginTransaction.Commit();
                                            Console.WriteLine($"消费成功");
                                            orderlist.Clear();
                                        }
                                        catch (Exception ex)
                                        {
                                            Console.WriteLine($"消费失败:{ex.Message}");
                                        }
                                    }
                                }
                            }
                            stopwatch.Restart();
                        }
                    }
                    catch (Exception ex)
                    {
                        throw new Exception(ex.Message);
                    }
                }
            }, stoppingToken);
        }
    }

LuasLoadService.cs:

    public class LuasLoadService: BackgroundService
    {
        private readonly IMemoryCache _cache;

        public LuasLoadService(IMemoryCache cache)
        {
            _cache = cache;
        }
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            FileStream fileStream1 = new FileStream(@"Luas/SeckillLua.lua", FileMode.Open);
            using (StreamReader reader = new StreamReader(fileStream1))
            {
                string line = reader.ReadToEnd();
                string luaSha = RedisHelper.ScriptLoad(line);

                //保存到缓存中
                _cache.Set<string>("SeckillLua1", luaSha);
            }
            FileStream fileStream2 = new FileStream(@"Luas/SeckillLuaCallback.lua", FileMode.Open);
            using (StreamReader reader = new StreamReader(fileStream2))
            {
                string line = reader.ReadToEnd();
                string luaSha = RedisHelper.ScriptLoad(line);

                //保存到缓存中
                _cache.Set<string>("SeckillLuaCallback1", luaSha);
            }
            return Task.CompletedTask;
        }
    }

控制器新建SetOrderLuas

        [HttpGet]
        [Route("[action]")]
        public string SetOrderLuas(string userId, string proId, string requestId = "125643")
        {
            int tLimits = 1000;    //限制请求数量
            int tSeconds = 2;     //限制秒数
            int goodNum = 1;    //用户购买的商品数量
            string limitKey = $"LimitRequest{proId}";//受限商品ID
            int tGoodBuyLimits = 1000;  //用户单个商品可以购买的数量
            string userBuyGoodLimitKey = $"userBuyGoodLimitKey-{userId}-{proId}";  //用户单个商品的限制key
            string userRequestId = requestId;    //用户下单页面的请求ID
            string proKey = $"{proId}-sCount";  //该商品库存keyint 
            try {
                //调用lua脚本
                //参数说明:ypf12345没有什么用处,当做一个参数传入进去即可
                var result = RedisHelper.EvalSHA(cache.Get<string>("SeckillLua1"), "ypf12345", tLimits, tSeconds, limitKey, goodNum, tGoodBuyLimits, userBuyGoodLimitKey, userRequestId, proKey);
                if (result.ToString() == "1")
                {
                    var orderNum = Guid.NewGuid().ToString("N");
                    _redis.ListLeftPush(userId, $"{userId}-{proId}-{orderNum}");
                    return $"下单成功,订单信息为:userId={userId},arcId={proId},orderNum={orderNum}";
                }
                else
                {
                    return "卖完了";
                }
            }
            catch (Exception ex)
            {
                //lua回滚
                Console.WriteLine("新的进程5");
                RedisHelper.EvalSHA(cache.Get<string>("SeckillLuaCallback1"), "ypf12345", limitKey, userBuyGoodLimitKey, userRequestId, proKey, goodNum);
                throw new Exception(ex.Message);
            }
        }

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

            //注册后台任务
            services.AddHostedService<RCacheBackService>();
            services.AddHostedService<RCustomerService>();
            services.AddHostedService<CacheBackService>();
            services.AddHostedService<CustomerService>();
            services.AddHostedService<LCacheBackService>();
            services.AddHostedService<LCustomerService>();
            services.AddHostedService<LuasLoadService>();

测试结果(我把限制放宽,注销方法幂等后测试的):
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值