用 Node.js + Redis 解決高併發秒殺問題

這篇文章寫於 2021 年,文中的工具與版本可能已經更新,實作時請以官方文件為準。

本文目錄
  1. 一、文章目標
  2. 二、使用技術
  3. Redis
  4. Lua
  5. 三、程式架構
  6. 主程式:redisSecKill.js
  7. Lua 腳本:secKill.lua
  8. 四、執行結果
  9. 五、補充

商城在高併發環境下除了要考量到使用者體驗外,還要處理「超賣」的問題;Redis 是一種記憶體資料庫,它有高讀寫效率的特性、能承受高 qps,而這篇文章則是運用它獨特的「鎖」解決超賣問題。

用 Node.js + Redis 解決高併發秒殺問題(圖 1)

大綱
一、文章目標
二、使用技術
  ➤ Redis
  ➤ Lua
三、程式架構
  ➤ 主程式:redisSecKill.js
  ➤ Lua腳本:secKill.lua
四、執行結果
五、補充

一、文章目標

  1. 產生秒殺商品基礎資訊(商品名稱、庫存)
  2. 模擬秒殺情境,確認是否會超賣
  3. 確認有保存購買人資訊

二、使用技術

Redis

  1. 如果完全沒有相關基礎,建議先參考這篇文章來安裝 Redis 以及他的 GUI 工具
  2. 本篇文章使用 Redis 的 Hash、List type 來儲存資訊

Lua

  1. 為了避免超賣,這裡採用 Lua 腳本
  2. 在 Redis Server 執行 EVAL 指令時,在結果回傳前只會執行當下 Lua 腳本的邏輯,其他 Client 端的命令須等待直到 EVAL 執行完為止。

Lua 腳本的邏輯應盡量簡單以保證執行效率,否則會影響 Client 端的體驗


三、程式架構

主程式:redisSecKill.js

  1. 先啟用 Redis Client 端(如未安裝相關套件,請在專案資料夾下輸入 yarn add ioredis)
  2. 用 prepare 函式,以 Hash type 建立參加秒殺的產品庫存
  3. 用 secKill 函式模擬使用者購買行為,緩存並執行 Lua 腳本
const fs = require('fs');
const Redis = require('ioredis');
const redis = new Redis({
    host: '127.0.0.1',
    port: 6379,
    password: ''
});

redis.on("error", function (error) {
    console.error(error);
});

async function prepare (item_name) {
    // 參加秒殺活動的商品庫存
    await redis.hmset(item_name, "Total", 100, "Booked", 0);
}

const secKillScript = fs.readFileSync('./secKill.lua');

async function secKill (item_name, user_name) {
    // 1. 緩存腳本取得 sha1 值
    const sha1 = await redis.script("load", secKillScript);
    // console.log(sha1);

    // 2. 透過 evalsha 執行腳本
    // redis Evalsha 命令基本語法如下
    // EVALSHA sha1 numkeys key [key ...] arg [arg ...] 
    redis.evalsha(sha1, 1, item_name, 1, "order_list", user_name);
}

async function main () {
    console.time('secKill')
    const item_name = "item_name"
    prepare(item_name)
    for (var i = 1; i < 10000; i++) {
        const user_name = "baobao" + i
        secKill(item_name, user_name)
    }
    console.timeEnd('secKill')
}
main()

原始碼:redisSecKill.js(GitHub Gist)

Lua 腳本:secKill.lua

在 Lua 腳本執行「取得商品庫存」➜「如果庫存足夠就-1」➜「儲存購買者資訊(List type)」

local n = tonumber(ARGV[1])
local order_name = ARGV[2]
if not n  or n == 0 then
    return 0       
end                
local vals = redis.call("HMGET", KEYS[1], "Total", "Booked");
local total = tonumber(vals[1])
local blocked = tonumber(vals[2])
if not total or not blocked then
    return 0       
end                
if blocked + n <= total then
    redis.call("HINCRBY", KEYS[1], "Booked", n)
    redis.call("LPUSH", order_name,ARGV[3])                                   
    return n;   
end                
return 0

原始碼:secKill.lua(GitHub Gist)


四、執行結果

SETP 1:在專案資料夾下輸入 node redisSecKill.js 模擬秒殺

四、執行結果(圖 2)

SETP 2:待執行完後,用 Redis GUI 程式確認商品沒有超賣

四、執行結果(圖 3)

SETP 3:確認訂單都有對應的買家

四、執行結果(圖 4)

五、補充

上面程式只是 MVP,現實狀況還有很多要討論的:

  1. 哪些商品被列為秒殺,如何紀錄
  2. 設定秒殺開始時間、結束時間
  3. 存在 Redis 的資料怎麼存入關連式資料庫中
如果你對 Redis 感興趣,筆者這邊也整理了很多相關資源:
1. 一文讓你徹底瞭解 Redis(進階),史上最全,不看後悔!!!【建議收藏】
2. 【总结】瞬时高并发(秒杀/活动)Redis 方案
3. redis 之 sorted sets 类型及操作
4. node.js 中对 redis 的安装和基本操作
5. Node.js 中实践 Redis Lua 脚本
6. 使用 Redis 搭建电商秒杀系统

筆者也是剛學習 Redis 的新人,如果描述有誤,或是有更好的解決方式,再麻煩大神們不吝賜教,感謝~

Talk

留言討論

留言載入中…