五、Redis_lua: Lua是redis的轻量脚本语言,把部分需要在业务里实现的功能写在脚本然后嵌入到程序中,把它当作“存储过程”就比较容易理解了。其实我也不熟,就是来装一下。
其实就是把上篇Redis+队列的“单品限流、购买次数的限制和方法幂等”写到lua里然后嵌入到程序中
功能分析:
- 减少网络开销,将业务放到脚本中执行
- 脚本会做作为一个整体执行,不会插入其它命令
- 脚本复用
- 缺点是出错不会回滚
Lua基础语法:
- 基本结构,类似于js,前面声明方法,后面调用方法。
- 参数通过ARGV[1]获取,Key通过KEYS[1]获取
- redis.call( )方法调用api
- 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代表成功,返回: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>();
测试结果(我把限制放宽,注销方法幂等后测试的):