赞
踩
五、Redis_lua: Lua是redis的轻量脚本语言,把部分需要在业务里实现的功能写在脚本然后嵌入到程序中,把它当作“存储过程”就比较容易理解了。其实我也不熟,就是来装一下。
其实就是把上篇Redis+队列的“单品限流、购买次数的限制和方法幂等”写到lua里然后嵌入到程序中
功能分析:
Lua基础语法:
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代表成功,返回:0,2,3,4 代表不同类型的失败 ]]-- --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>();
测试结果(我把限制放宽,注销方法幂等后测试的):
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。