一. Lua簡介
1. 介紹
Lua 是一種輕量小巧的腳本語言,用标準C語言編寫并以源代碼形式開放, 其設計目的是為了嵌入應用程式中,進而為應用程式提供靈活的擴充和定制功能。
該章節主要是Redis調用Lua腳本。
2. 好處
(1). 減少網絡開銷:本來多次網絡請求的操作,可以用一個請求完成,原先多次次請求的邏輯都放在redis伺服器上完成,使用腳本,減少了網絡往返時延。
(2). 原子操作:Redis會将整個腳本作為一個整體執行,中間不會被其他指令插入。
(3). 複用:用戶端發送的腳本會永久存儲在Redis中,意味着其他用戶端可以複用這一腳本而不需要使用代碼完成同樣的邏輯。
注:lua整合一系列redis操作, 是為了保證原子性, 即redis在處理這個lua腳本期間不能執行其它操作, 但是lua腳本自身假設中間某條指令出錯,并不會復原的,會繼續往下執行或者報錯了。
3. 基本文法
(1). 基本結構,類似于js,前面聲明方法,後面調用方法。
(2). 擷取傳過來的參數:ARGV[1]、ARGV[2] 依次類推,擷取傳過來的Key,用KEYS[1]來擷取。
(3). 調用redis的api,用redis.call( )方法調用。
(4). int類型轉換 tonumber
參考代碼:
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; --對應的是整個代碼塊的結束
--1. 單品限流調用
local status1 = seckillLimit();
if status1 == 0 then
return 2; --失敗
end
參考菜鳥教程:https://www.runoob.com/lua/lua-tutorial.html
詳細文法分析參照的Redis章節的文章:https://www.cnblogs.com/yaopengfei/p/13941841.html
二. CSRedisCore使用
1. 簡介
CSRedisCore 比 StackExchange.Redis 性能更高,提供的Api更加豐富,支援主從、哨兵、Cluster等模式,提供一個簡易RedisHelper幫助類,友善快速調用API。
GitHub位址:https://github.com/2881099/csredis
2. 調用Lua腳本
(1). 初始CSRedisCore ,這裡可以直接使用csredis,也可以使用RedisHelper幫助類
var csredis = new CSRedisClient("localhost:6379,defaultDatabase=0");
services.AddSingleton(csredis); //官方建議單例
//初始化RedisHelper幫助類,官方建議靜态
RedisHelper.Initialization(csredis);
(2). 讀取Lua腳本,可以暫時放到緩存中,友善全局調用
FileStream fileStream1 = new FileStream(@"Luas/SeckillLua1.lua", FileMode.Open);
using (StreamReader reader=new StreamReader(fileStream1))
{
string line = reader.ReadToEnd();
string luaSha = RedisHelper.ScriptLoad(line);
//儲存到緩存中
_cache.Set<string>("SeckillLua1", luaSha);
}
(3). 調用lua腳本
RedisHelper.EvalSHA(_cache.Get<string>("SeckillLua1"), "ypf12345", tLimits, tSeconds, limitKey, goodNum, tGoodBuyLimits, userBuyGoodLimitKey, userRequestId, arcKey);
PS:ypf12345隻是一個key,在腳本中可以通過 KEYS[1] 來擷取。
詳細文法分析參照的Redis章節的文章。
三. Lua整合四項操作
1.設計思路
A.編寫Lua腳本,将單品限流、購買商品限制、方法幂等、擴建庫存整合在一個lua腳本中,程式通過相關的Api調用即可。
B.啟動項目的是加載讀取Lua腳本并轉換→轉換後的結果存到伺服器緩存中→業務中調用的時候直接從緩存中讀取傳給Redis的Api。
2.分析
A. 整合在一個腳本中,程式相當于隻連結了一次Redis,提高了性能,解決以上四個業務互相之間可能存在的并發問題
B. 在叢集環境中,能替代分布式鎖嗎?
3.代碼分享
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; --成功
View Code
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();
加載lua腳本到緩存

/// <summary>
/// 背景任務,初始化lua檔案到伺服器緩存中
/// </summary>
public class LuasLoadService : BackgroundService
{
private IMemoryCache _cache;
public LuasLoadService(IMemoryCache cache)
{
_cache = cache;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
FileStream fileStream1 = new FileStream(@"Luas/SeckillLua1.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/SeckillLuaCallback1.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;
}
}
下單接口

/// <summary>
///08-Lua整合
/// </summary>
/// <param name="userId">使用者編号</param>
/// <param name="arcId">商品編号</param>
/// <param name="totalPrice">訂單總額</param>
/// <param name="requestId">請求ID</param>
/// <param name="goodNum">使用者購買的商品數量</param>
/// <returns></returns>
public string POrder8(string userId, string arcId, string totalPrice, string requestId = "125643", int goodNum = 1)
{
int tLimits = 100; //限制請求數量
int tSeconds = 1; //限制秒數
string limitKey = $"LimitRequest{arcId}";//受限商品ID
int tGoodBuyLimits = 3; //使用者單個商品可以購買的數量
string userBuyGoodLimitKey = $"userBuyGoodLimitKey-{userId}-{arcId}"; //使用者單個商品的限制key
string userRequestId = requestId; //使用者下單頁面的請求ID
string arcKey = $"{arcId}-sCount"; //該商品庫存key
try
{
//調用lua腳本
//參數說明:ypf12345沒有什麼用處,當做一個參數傳入進去即可
var result = RedisHelper.EvalSHA(_cache.Get<string>("SeckillLua1"), "ypf12345", tLimits, tSeconds, limitKey, goodNum, tGoodBuyLimits, userBuyGoodLimitKey, userRequestId, arcKey);
//int.Parse("3242fgdfg"); //模拟報錯
if (result.ToString() == "1")
{
//2. 将下單資訊存到消息隊列中
var orderNum = Guid.NewGuid().ToString("N");
_redisDb.ListLeftPush(arcId, $"{userId}-{arcId}-{totalPrice}-{orderNum}");
//3. 把部分訂單資訊傳回給前端
return $"下單成功,訂單資訊為:userId={userId},arcId={arcId},orderNum={orderNum}";
}
else
{
//請求被禁止,或者是商品賣完了
throw new Exception($"沒搶到");
}
}
catch (Exception ex)
{
//lua復原
RedisHelper.EvalSHA(_cache.Get<string>("SeckillLuaCallback1"), "ypf12345", limitKey, userBuyGoodLimitKey, userRequestId, arcKey, goodNum);
throw new Exception(ex.Message);
}
}
!
- 作 者 : Yaopengfei(姚鵬飛)
- 部落格位址 : http://www.cnblogs.com/yaopengfei/
- 聲 明1 : 如有錯誤,歡迎讨論,請勿謾罵^_^。
- 聲 明2 : 原創部落格請在轉載時保留原文連結或在文章開頭加上本人部落格位址,否則保留追究法律責任的權利。