iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0

Day 19 的 MULTI 排隊的時候指令還沒執行,拿不到值,沒辦法「先看有沒有庫存、有才扣」。

Lua 是一種很小的程式語言,Redis 裡面就有內建。把判斷跟扣庫存寫成一段 Lua 腳本丟給 Redis,整段跑完之前,別人的指令都插不進來。之前說過 Redis 是單執行緒,一段腳本對它來說就跟一個指令一樣。

今天用扣庫存當例子,之後的秒殺會直接用到。

常用指令

  • EVAL 腳本 key個數 key ... 參數 ...(執行一段 Lua 腳本)
  • EVALSHA sha1 key個數 key ... 參數 ...(Redis 存過的腳本,只傳編號就能執行)
  • SCRIPT KILL(停掉跑太久的腳本)

EVAL

進 redis-cli,用腳本設定庫存(在主機的終端機下):

docker exec -it redis30days redis-cli

EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 stock:1 3

GET stock:1

https://ithelp.ithome.com.tw/upload/images/20261002/20184209F5VUS2ykkM.png

redis.call 就是在腳本裡下 Redis 指令。stock 前面的 1 是 key 的個數,所以 stock:1 放進 KEYS[1],剩下的 3 放進 ARGV[1](Lua 從 1 開始數)。

key 跟參數分開放,是因為 Redis 要先知道腳本會動到哪些 key。Day 18 的 Cluster 就是看 KEYS 算 slot,決定要送去哪一台。

腳本裡用到的 key 一律從 KEYS 傳進來,不要寫死在腳本裡,Cluster 下寫死的 key 剛好在別台的話,會噴 Script attempted to access a non local key。

扣庫存

新增 resources/scripts/deduct.lua:

-- Day 20:庫存大於 0 才扣一個,整段跑完之前不會被插隊
-- KEYS[1]:庫存的 key,例如 stock:1

local stock = tonumber(redis.call('GET', KEYS[1])) or 0 -- key 不存在當 0

if stock <= 0 then
    return -1 -- 沒庫存,不扣
end

return redis.call('DECR', KEYS[1]) -- 扣一個,回剩幾個

新增一個 StockController 來執行這支腳本:

/** 腳本放在 resources/scripts/,Lua 回的整數對到 Long */
private static final RedisScript<Long> DEDUCT =
        RedisScript.of(new ClassPathResource("scripts/deduct.lua"), Long.class);

/** 買一個:回扣完剩幾個,沒庫存回 -1 */
@PostMapping("/buy")
public Long buy(@RequestParam long id) {
    return redis.execute(DEDUCT, List.of("stock:" + id)); // List 裡的就是 KEYS
}

重新啟動 Java,開另一個終端機,盯著 Redis 收到的指令(在主機的終端機下):

docker exec redis30days redis-cli MONITOR | grep -E "EVAL|lua"

回到 redis-cli 那個終端機,exit 出來,連買四次(在主機的終端機下):

curl -X POST "localhost:8080/stock/buy?id=1" # 回 2

curl -X POST "localhost:8080/stock/buy?id=1" # 回 1

curl -X POST "localhost:8080/stock/buy?id=1" # 回 0

curl -X POST "localhost:8080/stock/buy?id=1" # 回 -1,沒庫存了

庫存 3 個,第四次回 -1,沒有被扣成負的。

EVALSHA

看一下 MONITOR:

https://ithelp.ithome.com.tw/upload/images/20261002/20184209GQiEi2Te4Z.png

  • 第一次 Spring 先送 EVALSHA,只送腳本的編號(SHA1)。Redis 沒看過這支腳本,回 NOSCRIPT,Spring 才改送 EVAL 把整支腳本傳過去(後面那一長串就是腳本,中文被轉成 \x 開頭的編碼)
  • 之後 Redis 記住了,Spring 只送 EVALSHA,腳本再長也只傳 40 個字的編號
  • [0 lua] 是腳本裡下的指令:前三次 GET 完接著 DECR,第四次庫存是 0,GET 完就回 -1 了

不會回滾

再進 redis-cli,把 Day 19 的轉帳寫成腳本,B 一樣是小數(在主機的終端機下):

docker exec -it redis30days redis-cli

MSET balance:A 100 balance:B 80.5

EVAL "redis.call('DECRBY', KEYS[1], ARGV[1]) redis.call('INCRBY', KEYS[2], ARGV[1])" 2 balance:A balance:B 30

MGET balance:A balance:B

https://ithelp.ithome.com.tw/upload/images/20261002/20184209YwB5umyJez.png

A 被扣成 70,B 還是 80.5。Lua 腳本跟 MULTI 一樣不會回滾,跑到一半出錯就停在那裡,前面寫的不會還原。

跑太久

腳本跑的時候,其他人都要等。一樣在 redis-cli 裡,跑一個不會結束的腳本:

EVAL "while true do end" 0

開另一個終端機,買一次(在主機的終端機下):

curl -X POST "localhost:8080/stock/buy?id=1"

https://ithelp.ithome.com.tw/upload/images/20261002/20184209LC4bcPbaPl.png

https://ithelp.ithome.com.tw/upload/images/20261002/20184209F6j71W0505.png

/stock/buy 回 500,Java 的 console 噴 RedisBusyException: BUSY Redis is busy running a script。腳本跑超過 5 秒(busy-reply-threshold 預設 5000 毫秒),Redis 才會回其他連線 BUSY,在那之前都是卡著等。

再把腳本停掉(在主機的終端機下):

docker exec redis30days redis-cli SCRIPT KILL

https://ithelp.ithome.com.tw/upload/images/20261002/20184209MqNUaiewhy.png

SCRIPT KILL 之後,redis-cli 那邊收到 Script killed by user,Redis 就恢復了。

換一個先寫一筆資料、再跑不停的腳本:

EVAL "redis.call('SET', KEYS[1], 1) while true do end" 1 lua:busy

一樣到另一個終端機(在主機的終端機下):

docker exec redis30days redis-cli SCRIPT KILL

docker exec redis30days redis-cli SHUTDOWN NOSAVE

docker compose up -d

https://ithelp.ithome.com.tw/upload/images/20261002/20184209XBmIEJMrqi.png

https://ithelp.ithome.com.tw/upload/images/20261002/20184209ri32pnHTu0.png

SCRIPT KILL 回 UNKILLABLE:腳本已經寫過資料,這時候停下來就只做了一半,Redis 不讓你停。只剩兩條路,等它跑完,或是 SHUTDOWN NOSAVE 把整台 Redis 關掉後再 docker compose up -d 開回來。

為什麼要在意

  • 腳本越短越好。 腳本跑的時候整台 Redis 只做這件事,跟 Day 3 的 KEYS * 一樣,所有人都卡住。超過 5 秒還能 SCRIPT KILL,寫過資料的連 KILL 都不行,只能把 Redis 關掉。
  • 先檢查,最後才寫。 腳本不會回滾,跑到一半出錯,前面寫的就留著。要像扣庫存的腳本,先判斷有沒有庫存,最後才真的扣。

相關範例程式碼可以參考 https://github.com/gary880306/redis-30days/tree/dev

明天

明天回到 Day 4 的商品快取,來看如果一直有人查不存在的商品,會發生什麼事/images/emoticon/emoticon12.gif


上一篇
Day 19|Pipeline 與事務
下一篇
Day 21|快取穿透
系列文
從購物車到秒殺:30 天 Redis 高併發自學筆記 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言