電商庫存系統設計案例詳解(下)

6 純 Redis 扣減方案

Redis 單線程模型,具原子性。當有多個客戶端給 Redis 發命令,Redis 會按接收順序串行執行。對於還未被調度的命令,則放在隊列裏排隊。

庫存扣減爲保證數據併發安全,要求原子性,而 Redis 正好滿足扣減類需求。

6.1 基於的 Redis 庫存扣減

圖片

6.2 Redis 數據模型

剩餘庫存(KV 結構)

K = sku_leaved_amount_{sku_id} V = 剩餘的庫存數值

流水(hash 結構)

K = inventory_flow_{sku_id} hash—K = 訂單明細 id(不同業務場景的全局性 id,用來做冪等控制) hash—V = 本次購買的數量

購物車下單,多個 sku 批量扣減,需按單個 sku 循環發起 Redis 調用。但多個 Redis 命令無法保證原子性。可採用 lua,將這些命令打包到一個腳本,作爲一個命令發給 Redis 執行,保證原子性。

lua 是一個類似 JavaScript、Shell 等的解釋性語言,它可以完成 Redis 已有命令不支持的功能。用戶在編寫完 lua 腳本之後,將此腳本上傳至 Redis 服務端,服務端會返回一個標識碼代表此腳本。在實際執行具體請求時,將數據和此標識碼發送至 Redis 即可。Redis 會和執行普通命令一樣,採用單線程執行此 lua 腳本和對應數據。

6.3 Lua 執行流程

批量扣減,是對單個扣減的循環調用,所以這裏只討論單次扣減的處理步驟:

  1. 1. 先據【訂單明細 id】查詢【扣減流水】,是否已操作過,做冪等性校驗

  2. 2. 再查詢 sku 的剩餘庫存,並根據 【下單購買數】 做校驗,只要有一個 sku 數量不足,則返回失敗

  3. 3. 修改所有 sku 的緩存中的剩餘庫存數

  4. 4. 緩存中插入扣減流水記錄

當 Redis 扣減成功後,應用程序再將此次扣減 異步化 保存到 MySQL。

6.4 Redis 方案利弊分析:

6.5 風險

上述 Lua 腳本 把多條命令打包在一起,雖保證原子性,但不具備 事務回滾。如庫存扣減成功,此時 Redis 宕機 ,扣減流水並沒有插入成功,應用程序認爲本次 Redis 調用失敗,前臺給用戶反饋錯誤提示,但已扣減的數量不會回滾。當 Redis 故障修復後,再次啓動,此時恢復的數據已不一致。需要結合 Redis 和 數據庫 做數據覈對 check,並結合扣減服務的日誌,做數據的增量修正。

9 分庫分表的扣減方案

上面提到的數據庫方式基於 單庫單表,雖藉助 ACID 保證數據一致性,但是單臺 MySQL 併發有限,如何提升性能?

除了 純緩存 化方案外,還可考慮將 庫存表 進行 水平拆分 ,分攤洪峯壓力。

圖片

假如庫存表 QPS 要求 1.6w,經過拆分成 16 張表後,若數據分佈均勻,每個物理表預計處理 1000 QPS,完全處於 MySQL 單實例的承載範圍之內。

拆分後,單表數據量減少很多,假如分表前有一個億數據,分表後每張表不到 1 千萬,索引查詢性能也會提高。

同一次扣減業務,庫存扣減和插入流水要放在同一個分庫,通過事務保證一致性。若數據分佈和業務請求夠均勻,經過分庫分表後,整個系統的吞吐量是線性增長,主要取決於分表的實例數量。

10 其他扣減方案

1、如果某個 sku_id 的庫存扣減過熱,單臺實例支撐不了( mysql 官方測評:一般單行更新的 QPS 在 500 以內 ),可以考慮將一個 sku 的大庫存拆分成 N 份,放在不同的庫中(也就是說所有子庫的庫存數總和纔是一件 sku 的真實庫存),由於前臺的訪問流量非常大,按照 均分原則 ,每個子庫分到的流量應該差不多。上層路由時只需要在 sku_id 後面拼接 一個範圍內的隨機數 ,即可找到對應的子庫,有效減輕系統壓力。

2、單條 sku 庫存記錄更新過熱,也可以採用批量提交方式,將多次扣減累計計數,集中成一次扣減, 從而實現了將串行處理變成了批處理 ,也可以大大減輕數據庫壓力。

3、引入 RocketMQ 消息隊列,經過前置校驗後,如果有剩餘庫存,則把創建訂單的操作封裝成消息發送給 MQ,訂單系統從 RocketMQ 中以特定的頻率消費,創建訂單,該方案有一定的延遲性。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sDwQoYMTQ4an3ml8v1WI4g