Redis使用lua指令碼實現庫存扣減

IT隨筆發表於2023-05-16

為什麼使用Lua指令碼為什麼能合併多個原子操作?

Redis官方文件:https://redis.io/docs/manual/programmability/eval-intro/

 

Redis 保證指令碼的原子執行。在執行指令碼時,所有伺服器活動在其整個執行期間都被阻止。這些語義意味著指令碼的所有效果要麼尚未發生,要麼已經發生。

指令碼提供了幾個在許多情況下都很有價值的屬性。這些包括:

  • 透過在資料所在的地方執行邏輯來提供區域性性。資料區域性性減少了整體延遲並節省了網路資源。
  • 確保指令碼原子執行的阻塞語義。
  • 啟用 Redis 中缺少的或過於小眾的簡單功能的組合。

Lua 允許您在 Redis 中執行部分應用程式邏輯。這樣的指令碼可以跨多個鍵執行條件更新,可能以原子方式組合幾種不同的資料型別。

 

命令列應用Lua

Redis Eval 命令使用 Lua 直譯器執行指令碼。

這裡能幫我們實現 Redis 執行 Lua 指令碼的命令有兩個,一個是 EVAL,另一個是 EVALSHA。

redis Eval 命令基本語法如下:

EVAL script numkeys key [key ...] arg [arg ...] 

其中 EVAL 是命令,script 是我們 Lua 指令碼的字串形式,numkeys 是我們要傳入的引數數量,key 是我們的入參,可以傳入多個,arg 是額外的入參。

以下嘗試演示指令碼KEYS和ARGV執行時全域性變數之間輸入引數的分佈:

redis> EVAL "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }" 2 key1 key2 arg1 arg2 arg3
1) "key1"
2) "key2"
3) "arg1"
4) "arg2"
5) "arg3"

 

但這種方式需要每次都傳入 Lua 指令碼字串,不僅浪費網路開銷,同時 Redis 需要每次重新編譯 Lua 指令碼,對於我們追求效能極限的系統來說,不是很完美。所以這裡就要說到另一個命令 EVALSHA 了,原生語法如下:

 

EVALSHA sha1 numkeys key [key ...] arg [arg ...]

 

不同的是這裡傳入的不是指令碼字串,而是一個加密串 sha1。這個 sha1 是從哪來的呢?它是透過另一個命令 SCRIPT LOAD 返回的,該命令是預載入指令碼用的。

 

從指令碼與 Redis 互動

可以透過redis.call()或從 Lua 指令碼呼叫 Redis 命令redis.pcall()

兩者幾乎一模一樣。兩者都執行 Redis 命令及其提供的引數(如果這些參數列示格式正確的命令)。但是,這兩個函式之間的區別在於處理執行時錯誤(例如語法錯誤)的方式。呼叫函式引發的錯誤redis.call()直接返回給執行它的客戶端。相反,呼叫redis.pcall()函式時遇到的錯誤將返回到指令碼的執行上下文,而不是進行可能的處理。

Java客戶端應用Lua例項

注意Lua 指令碼並不會自動幫你完成回滾操作,如果你的指令碼邏輯中包含兩步寫操作,需要自己去做回滾。好在我們庫存扣減的邏輯針對 Redis 的命令就兩種,一個讀一個寫,並且寫命令在最後,這樣就不存在需要回滾的問題了。

以庫存扣減核心操作為例,完成核心 Lua 指令碼的編寫。其主要實現的功能就是查詢庫存並判斷庫存是否充足,如果充足,則做相應的扣減操作,指令碼內容如下:

-- 呼叫Redis的get指令,查詢活動庫存,其中KEYS[1]為傳入的引數1,即庫存key
local c_s = redis.call('get', KEYS[1])
-- 判斷活動庫存是否充足,其中KEYS[2]為傳入的引數2,即當前搶購數量
if not c_s or tonumber(c_s) < tonumber(KEYS[2]) then
   return 0
end
-- 如果活動庫存充足,則進行扣減操作。其中KEYS[2]為傳入的引數2,即當前搶購數量
redis.call('decrby',KEYS[1], KEYS[2])

 

然後我們將 Lua 指令碼轉成字串,並新增指令碼預載入機制。

預載入可以有多種實現方式,一個是外部預載入好,生成了 sha1 然後配置到配置中心,這樣 Java 程式碼從配置中心拉取最新 sha1 即可。

另一種方式是在服務啟動時,來完成指令碼的預載入,並生成單機全域性變數 sha1。我們這裡先採取第二種方式,程式碼結構如下圖所示:

 

 

以上是將 Lua 指令碼轉成字串形式,並透過 @PostConstruct 完成指令碼的預載入。然後新增 EVALSHA 方法,如下圖所示:

 

方法入參為活動商品庫存 key 以及單次搶購數量,並在內部呼叫 Lua 指令碼執行庫存扣減操作。看起來是不是很簡單?在寫完底層核心方法之後,我們只需要在下單之前,呼叫該方法即可,具體如下圖所示:

  

指令碼快取

到目前為止,我們已經使用EVAL命令來執行我們的指令碼。

每當我們呼叫時EVAL,我們還會在請求中包含指令碼的原始碼。重複呼叫EVAL執行同一套引數化指令碼,既浪費網路頻寬,也對 Redis 有一定的開銷。當然,節省網路和計算資源是關鍵,因此,Redis 為指令碼提供了一種快取機制。

您執行的每個指令碼都EVAL儲存在伺服器保留的專用快取中。快取的內容由指令碼的 SHA1 摘要總和組織,因此指令碼的 SHA1 摘要總和在快取中唯一標識它。您可以透過執行EVAL並隨後呼叫來驗證此行為INFO。

 

SCRIPT FLUSH

從指令碼快取中移除所有指令碼。

SCRIPT LOAD script

將指令碼 script 新增到指令碼快取中,但並不立即執行這個指令碼。

相關文章