情境
想像一下
- 現在有個A產品線上購買功能
- 後台有
100
筆該產品目前的庫存數量 - 現在同時有
25
個http request同時湧入進行-1 扣庫存
的動作
結束後請問你後台的產品庫存真的有正確-25數量嗎?
如果沒有,該如何調整?
資料表設計
實作功能(1)
- 第一次我們先實作簡單直覺的範例
minus()功能流程 :- 找出product id 為 1 的產品資料
- 判斷product數量是否大於0
- 若大於0, 則進行-1
- 否則回傳庫存不足的error
- -1成功後, 回傳目前庫存數量
app/Http/Controllers/StockController.php
|
|
routes/api.php
|
|
Postman測試
確認API是否正常-1
JMeter壓力測試
使用JMeter開啟20個Thread
進行1輪api密集發送
結果發現庫存資料只有
-12
看起來扣庫存的動作是有問題的
正確來說,我們應該得到-20
的結果
使用postman一筆一筆扣庫存時並無異狀
但當同一時間內湧入大量扣庫存的程序時
同一個資源被互搶的問題就產生了
舉例來說,當A和B同時執行api時
他們倆個都是取到相同庫存數量100
結果相互執行-1的動作後
雖說有可能能sql update時間不一樣
但2筆update都是99的情況就是不正確
應該是一筆99, 另一筆98才對
所以,當A拿到庫存資源時
此資源應該處於被鎖定
狀態
當A完成update資料時才會釋放資源
然後讓B繼續存取
否則B必須等待A完成釋放資源
PS : 若A沒有在時間內
完成也必須釋放,避免DeadLock
(但本篇沒有實作)
那如何實作呢? 讓我們繼續看下去
實作功能(2) : Transaction
|
|
有關laravel transaction介紹 : 參考
lockForUpdate()
方法等於SQL中的SELECT ... FOR UPDATE
假設A process開始進行minus()時,此時product id 1的row會進行鎖表
資料尚未提交前,其他process無法進行UPDATE
,DELETE
,SELECT ... FOR UPDATE
lockForUpdate()
屬於排他鎖
,延伸閱讀 : 深入理解SELECT … LOCK IN SHARE MODE和SELECT … FOR UPDATE
實作功能(3) : Redis
|
|
事先在Redis存入一筆product = 100 的key value值
然後再透過Redissetnx
,ttl
的特性設計互斥鎖
但有個問題,這些值是存在Redis裡
之前有看到其他文章說,可以設計一個背景程式
專門定時同步Redis與資料庫的資料
透過這個方式來同步資料
但此方法我還在評估,不確定是否適合用在業界系統功能上
補充
-
mysql transaction default timeout is
50 sec
=> my suggestion is between3 ~ 5 sec
1 2 3 4 5 6
mysql> show variables like 'innodb_lock_wait_timeout'; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | innodb_lock_wait_timeout | 50 | +--------------------------+-------+
結論
透過mysql transaction方法是可以解決高併發問題
在多個http請求下,透過互斥鎖來確保每個process須等前一個process處理完才能進行
取值or更新值的動作才會是正確的
另外,Redis扣庫存方式也是一種方法
但關於與資料庫如何同步、是不適合用在業界系統上
這個就真的還待評估中
不過本文目前也只有考慮一個DB和一個Redis的情況下
如果後面進化成叢集分散架構,就會衍生更複雜的分散式互斥鎖問題
這就等以後實力長進一點後再來研究吧