iT邦幫忙

2021 iThome 鐵人賽

DAY 20
6
Software Development

全端工程師生存筆記系列 第 20

[面試][資料庫]如何解決高併發情境的商品秒殺問題

  • 分享至 

  • xImage
  •  

如果今天有上萬人在同一時間搶限量商品,昨天分享的方案基本撐不住。

不過面對這個情境,Redis 表示終於輪到我了!今天這篇文章會以 Node.js + Redis 為範例,帶讀者一起解決這個問題。

大綱

  1. 如何解決高併發情境的商品秒殺問題

    • 1.1 面試官為什麼會問?
    • 1.2 面試官想從答案確認什麼?
    • 1.3 筆者提供的簡答
  2. 回答問題所需具備的知識

    • 2.1 先了解 Redis 的基礎知識
    • 2.2 使用 Node.js + Redis 解決高併發秒殺問題
    • 2.3 模擬秒殺情境確認商品沒有超賣
  3. 衍伸問題

    • 3.1 如果有數萬用戶同時查看 TOP100 的點數排行榜,資料庫如何設計?
    • 3.2 Redis 跟 Memcached 的差異

1. 如何解決高併發商品秒殺問題

1.1 面試官為什麼會問?

這算是資料庫的常見面試題,除了金融、電商喜歡考這題外;社交平台、新創公司、遊戲產業也會換個形式考類似的題目。

因為這算是職缺所需具備的技術,如果求職者沒有相關經驗可能就會止步於這個關卡。

如果有去以上產業的打算,最好有一定的 NoSQL(ex:Redis、MongoDB) 基礎再去面試;因為在面試官的眼中有基礎跟完全不會的差距很大,求職者可以沒有實務經驗,但明知產業會碰到這些技術還不去學習就是態度問題了。


1.2 面試官想從答案確認什麼?

  • 能提出高併發情境商品秒殺的解決方案
  • 如果解決方案為 NoSQL,對它的認知有多少
  • 是否接觸過同類型的 NoSQL

1.3 筆者提供的簡答

通常這類型的秒殺活動都有固定檔期,我會先將活動用的商品及庫存數量同步到 Redis 資料庫;為了避免超賣,在 Client 端下單時會執行 Lua 腳本,當秒殺到指定數量或是時間結束後就不再接受請求,活動結束後將 Redis 的資料同步到關聯式資料庫


2. 回答問題所需具備的知識

2.1 先了解 Redis 的基礎知識

如果有短時間大量訪問、需要性能提升的需求,往往會先想到 Redis 這個記憶體資料庫。

  • Redis 為什麼快?

    • 記憶體資料庫
      記憶體讀寫本來就快。
    • 單執行緒(single-threaded)
      如果使用多執行緒會耗費時間在上下文的切換以及加鎖上面;而單執行緒會依照請求順序執行,不需考慮同步以及加鎖帶來的效能問題。
      Redis 是基於記憶體操作,所以效能的瓶頸不在 CPU 而在記憶體、網路頻寬;因為 CPU 不是瓶頸,所以可以開多個 Redis 建立 Cluster 分散壓力。
    • I/O 多路複用(multiplexing)
      Redis 使用了 epoll IO 多路複用,可以用一條執行緒處理併發的網路請求。

      這個有點抽象,我用繳交作業為情境說明
      假設你是一個老師,採用多路復用的原則批改作業,那你批改的順序就是看誰先繳交作業,而不是按照學號順序批改(此作法中間有人沒交作業就會卡住);這樣就能避免大量無用操作,為非阻塞模式的實現。

  • Redis 資料保存方案
    可以使用 RDB 、AOF 來做持久化。

    • RDB 持久化(Redis Database)
      在指定的時間間隔內將資料 dump 到硬碟;因為是定期操作,如果 Redis 當機會遺失部分資料,此方案適合大規模資料恢復
    • AOF 持久化(Append Only File)
      這個方案可以完整紀錄所有資料的變化,因為採用日誌追加的方式,所以就算當機也不會影響已經儲存的日誌,災難復原的完成度高;缺點是檔案比 RDB 大、大規模資料恢復速度較 RDB 慢
  • Redis 資料淘汰機制
    Redis 主要保存的都是熱點資訊,在儲存資料有限的狀態下(記憶體不足,無法寫入新資料);就要合理的設定淘汰機制:

    • 選擇性移除有設定過期時間的資料
      • volatile-lru:挑選最近較少使用的資料淘汰。
      • volatile-ttl:挑選即將過期的資料淘汰。
      • volatile-random:挑選隨機資料淘汰。
    • allkeys-lru:挑選最近較少使用的資料淘汰。
    • allkeys-random:挑選隨機資料淘汰。
    • no-enviction:禁止淘汰;若選用這個設定,當數據到達 maxmemory 時會回傳 OOM 錯誤。

    原則上淘汰策略以「volatile-lru、allkeys-lru」為主(淘汰較少使用的資料)。


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

  • 目標

    • 產生秒殺商品基礎資訊(商品名稱、庫存)
    • 模擬秒殺情境,確認是否會超賣
    • 確認有保存購買人資訊
  • 使用技術

    • Redis
      本篇文章使用 Redis 的 Hash、List type 來儲存資訊。

      如果完全沒有相關基礎,建議先參考這篇文章來安裝 Redis 以及它的 GUI 工具。

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

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

  • 程式架構

    • 主程式:redisSecKill.js

      • 先啟用 Redis Client 端(如未安裝相關套件,請在專案資料夾下輸入 yarn add ioredis)。
      • prepare 函式,以 Hash type 建立參加秒殺的產品庫存。
      • 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);
      }
      
      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();
      
    • Lua 腳本:secKill.lua
      在 Lua 腳本執行邏輯:「確認下單數量」➜「取得商品庫存」➜「如果庫存足夠就下單」➜「儲存購買者資訊(List type)」。

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

2.3 模擬秒殺情境確認商品沒有超賣

  • SETP 1:在專案資料夾下輸入 node redisSecKill.js 模擬秒殺
    https://ithelp.ithome.com.tw/upload/images/20210924/20103256NkIEkkJbW7.png
  • SETP 2:待執行完後,用 Redis GUI 程式確認商品沒有超賣
    https://ithelp.ithome.com.tw/upload/images/20210924/20103256ccyhUz1LOU.png
  • SETP 3:確認訂單都有對應的買家
    https://ithelp.ithome.com.tw/upload/images/20210924/20103256viebw6SuEc.png

補充
文章程式只是 MVP,現實狀況還有很多要設計的:

  • 哪些商品被列為秒殺,如何紀錄。
  • 設定秒殺開始、結束時間。
  • 如何將 Redis 資料存入關聯式資料庫中。

3. 衍伸問題

3.1 如果有數萬用戶同時查看 TOP100 的點數排行榜,資料庫如何設計?

考點:對 Redis Zset 這個資料型態的認知與應用

我會使用 Redis 這個記憶體資料庫,建立一個有序集合(Zset)來儲存用戶的資訊。

在設計上,用戶(member)是唯一值,且每個用戶都會關聯一個點數(score),這樣用戶就可以按照分數來排序。

功能實現上會透過 zrevrange Leaderboard 0 99 withscores 這段指令來顯示 TOP100 的點數排行榜。

  • zrevrange:依照點數(score)檢視排行榜
  • Leaderboard:排行榜名稱(可以自行定義)
  • 0 99:TOP100 的意思
  • withscores:連點數(score)一起顯示

3.2 Redis 跟 Memcached 的差異

考點:是否了解過同類型的技術以及差異

  • Memcached 只支援簡單的資料型態,而 Redis 支援多種資料型態(String、Hash、List、Set、Zset、Stream)。
  • Memcached 不支援資料持久化,而 Redis 提供 RDB 跟 AOF 兩種方案

同樣身為記憶體資料庫,Memcached 提供簡單的使用方式,而 Redis 提供豐富的功能。


感謝大家的閱讀,如果喜歡我的文章可以訂閱接收通知;如果有幫助到你,按Like可以讓我更有寫文的動力,我們明天見~

參考資源

  1. 手把手帶你在 MacOS 安裝 Redis &Another Redis Desktop Manager(筆者部落格)
  2. 用 Node.js + Redis 解決高併發秒殺問題(筆者部落格)
  3. redis 之 sorted sets 類型及操作
  4. 使用 redis 的有序集合實現排行榜功能

我在 Medium 平台 也分享了許多技術文章
❝ 主題涵蓋「MIS & DEVOPS資料庫前端後端MICROSFT 365GOOGLE 雲端應用自我修煉」希望可以幫助遇到相同問題、想自我成長的人。❞


https://ithelp.ithome.com.tw/upload/images/20230512/20103256twZPv1G4XH.jpg

在許多人的幫助下,本系列文章已成功出版,除了添加新的篇章,更完善了每個案例的應對進退;如果對現在的職涯感到迷茫,也許這本書能帶給你不一樣的觀點~

天瓏書局: https://www.tenlong.com.tw/products/9786263334571


上一篇
[面試][資料庫]關聯式資料庫要如何設計避免超賣?
下一篇
[面試][設計模式]Code Review 會注意哪些事?會依照什麼原則對程式做 Refactoring?
系列文
全端工程師生存筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
qpalzm
iT邦研究生 5 級 ‧ 2021-10-05 08:40:49

您好~很喜歡您寫的面試相關的類型文章,針對[短時間大量訪問、需要性能提升],想請教幾個問題:
1.對於大型專案來說,為甚麼不選擇oracle?
2.如果是短時間大量訪問,對於使用PHP+oracle來解決的話,不知道您有甚麼看法?
/images/emoticon/emoticon41.gif

看更多先前的回應...收起先前的回應...

Oracle 一定也有方案可以處理高併發的情境,只是費用相對昂貴,相關的設計可以參考我之前分享的文章[面試][資料庫]面對高流量的的系統,會採取哪些措施?

但上面的方案主要是為了解決長時間高流量而做的設計;如果只針對「短時間大流量」,使用記憶體資料庫的性價比更高。

另外 PHP 因為語言的特性,相對不適合處理短時間的大量訪問,可以參考我之前分享的文章PHP 跟 Node.js 的比較

面對同一個問題,每個語言與資料庫通常都有對應的解法,只是要付出的代價不同;就看設計者要如何取捨。

qpalzm iT邦研究生 5 級 ‧ 2021-10-05 14:08:51 檢舉

所以對於[短於時間,大量訪問,解決長時間高流量]如電商平台,是不建議使用php比較建議使用Node.js語法,這樣的說法不知對不對;
另外oracle其實本身是能處理[短於時間,大量訪問,解決長時間高流量],只是需要如何去設計以及考量到價格,所以市場上是偏少人使用oracle再電商嗎?謝謝解惑

ps:由於小弟目前使用oracle,所以想詢問看看大大的意見謝謝您

我個人是建議用 Node.js 來做電商平台,但請考慮到團隊成員的技能組合;如果大部分的成員都對 Node.js 不熟悉,出問題時沒人知道如何解決會更傷腦筋。

電商還是會需要關聯式資料庫的,這部分看每間公司的選擇,選擇Oracle 也可以;Redis 主要是針對「短時間大流量」的資料處理,活動結束後資料還是要回存到關聯式資料庫。

qpalzm iT邦研究生 5 級 ‧ 2021-10-05 15:17:49 檢舉

恩恩了解了,還是要考慮到技能樹的組合以及資料庫的配置 ,謝謝大大~

我要留言

立即登入留言