第四節:搶單流程優化3(lua整合限流、購買限制、方法冪等、扣減庫存)


一. 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();
View Code

加載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;
        }
    }
View Code

下單接口

/// <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);
            }
        }
View Code

 

 

 

!

  • 作       者 : Yaopengfei(姚鵬飛)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 聲     明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
  • 聲     明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM