之前寫了一篇PHP+Redis連結串列解決高併發下商品超賣問題,今天介紹一些如何使用PHP+Redis+Lua解決高併發下商品超賣問題。
為何要使用Lua指令碼解決商品超賣的問題呢?
- Redis在2.6版本後原生支援Lua指令碼功能,允許開發者使用Lua語言編寫指令碼傳到Redis中執行。
- 將複雜的或者多步的redis操作,寫為一個指令碼,一次提交給redis執行,減少反覆連線redis的次數,提升效能。
- 原子操作。Redis會將整個指令碼作為一個整體執行,中間不會被其他請求插入。因此在指令碼執行過程中無需擔心會出現競態條件,無需使用事務。
- 複用。客戶端傳送的指令碼會永久存在redis中,這樣其他客戶端可以複用這一指令碼,而不需要使用程式碼完成相同的邏輯。
首先,編寫lua指令碼,指令碼名為secKill.lua:
-- 接收引數
local user_id = KEYS[1]
local goods_id = KEYS[2]
-- 拼接字串
local stock_key = "secKill:"..goods_id..":stock" -- 秒殺商品庫存key
local users_key = "secKill:"..goods_id..":users" -- 成功秒殺商品的使用者集合key
-- 判斷使用者是否已經成功秒殺過該商品,如果已經存在在集合中,說明已經成功秒殺該商品,直接返回標誌2,防止重複搶購
local user_exists = redis.call('sismember', users_key, user_id)
if tonumber(user_exists, 10) == 1 then
return 2
end
-- 獲取當前商品庫存,如果庫存小於等於0,表名商品已經被搶購完了,否則庫存-1,並將搶購成功的使用者放入集合中
local left_goods_count = redis.call('get', stock_key)
if tonumber(left_goods_count, 10) <= 0 then
return 0
else
redis.call('decr', stock_key)
redis.call('sadd', users_key, user_id)
end
return 1
上述程式碼中返回的數字0,1,2只是一種約定,自己可以根據自己的有業務約定不同狀態返回的值。示例程式碼0:庫存為0,1:秒殺成功,2:已秒殺成功的使用者重複搶購。
lua指令碼編寫完成後,使用redis-cli命令生成該指令碼的sha祕鑰
redis-cli script load "$(cat /usr/local/redis/lua/secKill.lua)"
"63454a53284d9f6b30bdb6e5e12796a74f61f718"
最後,拿到lua指令碼的sha祕鑰,我們就可以在我們的程式碼中使用了。
$redis = new Redis();
$redis->connect("192.168.111.128", 6379);
$goodsId = 11211;
$userId = mt_rand(10000, 99999);
$res = $redis->evalSha('63454a53284d9f6b30bdb6e5e12796a74f61f718', [$userId, $goodsId], 2);
可以看到,我們將搶購邏輯寫到lua指令碼後,PHP程式碼就變得很少了,僅僅只有5行程式碼。
編寫好程式碼,接著我們開始對上述程式碼進行測試。
首先,我們需要設定商品的庫存量,正常邏輯是在後臺商品管理頁填寫具體商品的庫存量,此處假設我們的商品ID是11211(這個數字是不是很熟悉?是的,這是memcached的預設埠),商品數量為10個。
$redis-cli
> set secKill:11211:stock 10
我們使用ab壓測工具模擬2000個使用者併發量200來模擬搶購商品ID為11211的商品。
$ ab -n 2000 -c 200 http://www.master.com/index.php
如果沒有ab工具需要使用 yum -y install httpd-tools安裝
壓測完成後,我們通過RedisDesktopManager(RDM)軟體來檢視搶購結果,可以看到即使是200的併發量,最終也只有10個使用者搶購到商品,並且搶購成功的使用者被寫入到了secKill:11211:users的集合中,我們可以另外開一個守護程式專門用於從集合中獲取使用者ID處理後續事宜(將資料落盤寫入資料庫、給使用者發簡訊等)
使用Redis+Lua來解決搶購秒殺類問題是當前比較流行的一種做法,希望對正在開發秒殺搶購功能的你能產生幫助。