[分散式]高併發案例---庫存超發問題
1. 庫存超發的原因是什麼?
在執行商品購買操作時,有一個基本流程:
例如初始庫存有3個。
第一個購買請求來了,想買2個,從資料庫中讀取到庫存有3個,數量夠,可以買,減庫存後,更新庫存為1個。
接下來第二個購買請求來了,想買2個,發現庫存為1,不夠,不可以買了。
這樣是沒問題的,但在高併發情況下,這2個購買請求很可能是一起來的,他們都讀到庫存是3,都可以買,就都去減庫存,這時超發就發生了,結果庫存變成 -1
了。
有多種方案來解決這個問題,我們主要看3種方案:
-
悲觀鎖
-
樂觀鎖
-
Redis + Lua
下面分別看一下各個方案的實現思路,和優缺點。
2. 解決方案
2.1 悲觀鎖
出現超發現象的根本在於共享的資料被多個執行緒所修改,多個執行緒交織在一起。
如果一個執行緒讀庫存時就將資料鎖定,不允許別的執行緒進行讀寫操作,直到庫存修改完成才釋放鎖,那麼就不會出現超發問題了。
例如:
// 事務開始 select id, product_name, stock, ... from t_product where id=#{id} for update update t_product set stock = stock - #{quantity} where id = #{id} // 事務結束
在查詢庫存時使用了 for update
,這樣在事務執行的過程中,就會鎖定查詢出來的資料,其他事務不能對其進行讀寫(注意,讀也不行),這就避免了資料的不一致,直至事務完成釋放鎖。
-
優點
思路簡單,程式碼實現也非常簡單,從資料庫層面解決了超發問題。
-
缺點
這種獨佔鎖的方式對效能的影響是比較大的。
2.2 樂觀鎖
悲觀鎖有效但不高效,為了提高效能,出現了樂觀鎖方案,不使用資料庫鎖,不阻塞執行緒併發。
思路:
給商品記錄新增一個 version
欄位,讀取庫存時拿到這個 version
版本,更新庫存時要對比這個 version
值,如果版本相同,說明庫存沒被別人改過,可以更新,同時把 version
值加1,如果版本不同,說明被別人改過了,則取消庫存修改操作,購買失敗。
update t_product set stock = stock - #{quantity}, version = version +1 where id = #{id} and version = #{version}
通過 version
版本號,就可以知道自己讀取的資料在更新時是不是舊的,如果是舊資料,就不能更新了。
這種方式有點像碰運氣,運氣好,沒人和我一起更新,那麼就成功;如果運氣不好,被別人搶先修改了,那麼就失敗。
從而可以知道,在併發量很大的時候,失敗的概率會比較高。
為了提升成功率,可以引入重試機制,當更新失敗後,再走一遍流程(讀取、更新),具體重走幾遍比較好呢?可以規定一個次數,例如3次,如果重試了3次還是失敗,就放棄;還可以規定一個時間段,比如在 100ms 內迴圈操作,期間如果某次成功了就退出,否則一直重試到時間到為止。
-
優點
沒有阻塞,效能優於悲觀鎖。
-
缺點
實現思路較悲觀鎖複雜,增加了 version
的控制,還需要新增重試機制。
2.3 Redis + Lua
在高併發環境中,資料庫的方案較慢,如果寫入記憶體的 Redis 就會快很多。
此方案思路與悲觀鎖類似,都是把查詢庫存的操作與更新庫存的操作繫結在一起,不被其他執行緒影響,區別在於儲存介質,從資料庫換為Redis。
Lua 指令碼中可以編寫邏輯(取庫存、判斷是否夠用、更新庫存),Redis 中執行 Lua 時可以保證原子性,所以能夠滿足我們的需求,而且記憶體操作非常快,我們也不用擔心效能。
Lua 指令碼的邏輯:
示例程式碼:
-- 獲取當前庫存 local stock = tonumber(redis.call('hget', product, 'stock')) -- 如果庫存小於購買數量,說明庫存不足,返回0(失敗) if stock < quantity then return 0 end -- 減少庫存,得到新的庫存數量 stock = stock - quantity -- 更新庫存 redis.call('hset', product, 'stock', tostring(stock)) -- 字串拼接,生成購買記錄 local purchaseRecord = ... -- 把購買記錄儲存到 redis redis.call('rpush', purchaseList, purchaseRecord) -- 返回1(成功) return 1
我們的程式接收到使用者的購買請求時,就呼叫 Lua 進行處理。
上面的處理流程中有一步”把購買記錄寫入 redis“,這是因為 redis 不適合做持久化,我們還是需要把資料同步到資料庫中,可以使用一個定時程式,把 redis 中的記錄寫入資料庫。購買記錄也可以不放在 redis 中,寫入訊息佇列,然後通過消費者同步到資料庫。
-
優點
效能最優,實現簡單。
-
缺點
增加了輔助工作,需要額外處理資料庫的同步,還要保證 redis 本身是高可用的。
3. 小結
內容參考自《深入淺出 Spring Boot 2.x》。
相關文章
- [分散式][高併發]高併發架構分散式架構
- 用分散式鎖解決併發問題分散式
- PHP高併發情況下防止商品庫存超賣PHP
- 函式儲存過程併發控制-案例函式儲存過程
- 高併發-「搶紅包案例」之一:SSM環境搭建及復現紅包超發問題SSM
- [分散式][高併發]限流的四種策略分散式
- 解決資料庫高併發訪問瓶頸問題資料庫
- 【高併發寫】庫存系統設計
- PHP+Redis解決高併發下商品超賣問題PHPRedis
- 併發減庫存
- 面試集錦(八)分散式與高併發面試分散式
- 【高併發】之分散式全域性唯一 ID分散式
- [分散式]架構設計原則--高併發分散式架構
- jmeter介面效能測試-高併發分散式部署JMeter分散式
- 電商庫存系統的防超賣和高併發扣減方案
- mysql 高併發 select update 併發更新問題解決方案MySql
- 記一次專案中解決 -- 併發減庫存超賣問題過程(Java)Java
- 基於 Python 自建分散式高併發 RPC 服務Python分散式RPC
- Jmeter效能測試:高併發分散式效能測試JMeter分散式
- [分散式][高併發]熔斷策略和最佳實踐分散式
- 分散式叢集與多執行緒高併發分散式執行緒
- 「分散式技術專題」併發系列三:樂觀併發控制之原型系統(分散式驗證)分散式原型
- [分散式][高併發]訊息佇列的使用場景、概念、常見問題及解決方案分散式佇列
- 高併發快取面臨的問題快取
- .NET WebSocket高併發通訊阻塞問題Web
- 高併發核心技術 - 冪等性 與 分散式鎖分散式
- [分散式][高併發]負載均衡方案和演算法分散式負載演算法
- [分散式]對高併發流量控制的一點思考分散式
- 「分散式技術專題」併發系列一:基於加鎖的併發控制分散式
- 「分散式技術專題」併發系列二:基於時間的併發控制分散式
- 「分散式技術專題」併發系列三:樂觀併發控制之理論研究分散式
- PHP+Redis連結串列解決高併發下商品超賣問題PHPRedis
- 使用redis分散式鎖解決併發執行緒資源共享問題Redis分散式執行緒
- [分散式][高併發]熱點快取的架構優化分散式快取架構優化
- 分散式鎖--高併發優化實踐(分段加鎖思想)!分散式優化
- 關於高併發和分散式中的冪等處理分散式
- 分散式、高併發與多執行緒有何區別分散式執行緒
- 「分散式技術專題」併發系列三:樂觀併發控制之原型系統分散式原型