并發(fā)下的庫存如何扣?
背景
業(yè)務(wù)反饋归斤,項(xiàng)目出現(xiàn)庫存超賣/負(fù)值現(xiàn)象。
原因
//簡(jiǎn)易demo
$conn = mysqli_connect('localhost','root','123456','shop') or die('數(shù)據(jù)庫連接失敗');
$conn->query("SET NAMES 'UTF8'");
$query = "SELECT num FROM stock where id = 1";
$rs = $conn->query($query);
$row = $rs-> fetch_array();
$num = $row['num'];
$reduce = 1;
$update = 'update stock set num = num - '.$reduce.' where id = 1';
if($conn->query($update)){
echo 'success';
}else{
//重試
$conn->query($update)
};
假設(shè)庫存為5刁岸,用戶一次買了1個(gè)脏里,于是庫存扣減為4,如果更新失敗則重新設(shè)置虹曙,看似好像沒問題迫横。
問題1
庫存扣為負(fù)值
$num = $row['num'];
$reduce = 1;
if($num >= $reduce){
$update = 'update stock set num = num - '.$reduce.' where id = 1';
if($conn->query($update)){
echo 'success';
}else{
//重試
$conn->query($update)
}
}
這樣就不會(huì)出現(xiàn)庫存為負(fù)值
問題2
非冪等操作,庫存重復(fù)扣得問題(庫存為5個(gè),用戶想買2個(gè)酝碳,因?yàn)椴l(fā)&重試矾踱,實(shí)際扣了4個(gè)庫存)
此時(shí),我們必須知道疏哗,“set num=num-$reduce”扣減操作是個(gè)“非冪等操作”呛讲,即每次sql執(zhí)行得到的結(jié)果不一樣,實(shí)際我們需要的是用戶重試多次得到的應(yīng)該是同樣的結(jié)果沃斤,這卻是個(gè)“冪等操作”圣蝎,所以換個(gè)思路,我們將“非冪等”的“扣減操作”改為“冪等”的“設(shè)置庫存操作”
$num = $row['num'];
$reduce = 1;
$newNum = $num - $reduce;
if($newNum >= 0){
$update = 'update stock set num = '.$newNum.' where id = 1';
if($conn->query($update)){
echo 'success';
}else{
//重試
$conn->query($update)
}
}
問題3
并發(fā)問題
一共5個(gè)庫存
A買了3衡瓶,庫存設(shè)置為2
B買了2個(gè)庫存徘公,庫存要設(shè)置為3
這兩個(gè)設(shè)置庫存并發(fā)執(zhí)行,庫存會(huì)先變成2哮针,再變成3关面,導(dǎo)致數(shù)據(jù)不一致(實(shí)際賣出了5件商品,但庫存只扣減了2十厢,最后一次設(shè)置庫存會(huì)覆蓋和掩蓋前一次并發(fā)操作)
其根本原因是等太,設(shè)置操作發(fā)生的時(shí)候,沒有檢查庫存與查詢出來的庫存有沒有變化蛮放,理論上:
庫存為5時(shí)缩抡,A的庫存設(shè)置才能成功
庫存為5時(shí),B的庫存設(shè)置才能成功
實(shí)際執(zhí)行的時(shí)候:
庫存為5包颁,A的set stock 2確實(shí)應(yīng)該成功
庫存變?yōu)?了瞻想,B的set stock 3應(yīng)該失敗掉
修改 (CAS原理)
$num = $row['num'];
$reduce = 1;
$newNum = $num - $reduce;
if($newNum >= 0){
$update = 'update stock set num = '.$newNum.' where id = 1 and num = '.$num;
if($conn->query($update)){
echo 'success';
}else{
//重試
$conn->query($update)
}
}
問題4 CAS引發(fā)的ABA問題
ABA問題
并發(fā)1(上):獲取出數(shù)據(jù)的初始值是A压真,后續(xù)計(jì)劃實(shí)施CAS樂觀鎖,期望數(shù)據(jù)仍是A的時(shí)候蘑险,修改才能成功
并發(fā)2:將數(shù)據(jù)修改成B
并發(fā)3:將數(shù)據(jù)修改回A
并發(fā)1(下):CAS樂觀鎖滴肿,檢測(cè)發(fā)現(xiàn)初始值還是A,進(jìn)行數(shù)據(jù)修改
ABA在多數(shù)庫存情況下不會(huì)引發(fā)業(yè)務(wù)問題佃迄,但是少數(shù)的情況下會(huì)出現(xiàn)錯(cuò)誤泼差。
所以,周全的辦法是也要解決ABA問題
ABA本質(zhì)上是“僅比對(duì)值”造成的問題呵俏,所以我們可以對(duì)庫存加上一個(gè)版本號(hào)來解決該問題堆缘,每修改一次數(shù)據(jù)則版本號(hào)變更一次
update stock set num=$new_num, version=$new_version where sid=$sid and version=$version_old
$num = $row['num'];
$oldVersion = $row['version '];
$newVersion = $oldVersion++;
$reduce = 1;
$newNum = $num - $reduce;
if($newNum >= 0){
$update = 'update stock set num = '.$newNum.',version='.newVersion .' where id = 1 and version= '.$oldVersion ;
if($conn->query($update)){
echo 'success';
}else{
//重試
$conn->query($update)
}
}
總結(jié)
高并發(fā)下庫存應(yīng)如下優(yōu)化
- “設(shè)置庫存”保證冪等性
- “設(shè)置庫存”,需要加上原有庫存的比較柴信,才允許設(shè)置成功套啤,能解決高并發(fā)下庫存扣減的一致性問題
- CAS引發(fā)的ABA問題采用version比對(duì)解決