解決並發情況下庫存減為負數問題,庫存負數
情境:
一個商品有庫存,下單時先檢查庫存,如果>0,把庫存-1然後下單,如果<=0,則不能下單,事務包含兩條sql語句:
select quantity from products WHERE id=3;update products set quantity = ($quantity-1) WHERE id=3;
在並發情況下,可能會把庫存減為負數(兩個進程同時select出來的都>0,然後都會執行update),怎麼辦呢?
方法1:
InnoDB支援通過特定的語句進行顯示加鎖:
select...lock in share mode
select...for udpate
select quantity from products WHERE id=3 for update;update products set quantity = ($quantity-1) WHERE id=3;
但是執行for update會產生一些其他的影響
1.select語句變慢
2.一些最佳化無法正常使用,例如索引覆蓋掃描
3.很容易造成伺服器的鎖爭用問題
方法二:
把udpate語句寫在前邊,先把數量-1,之後select出庫存如果>-1就commit,否則rollback。
update products set quantity = quantity-1 WHERE id=3;select quantity from products WHERE id=3 for update;
上邊的事務中先執行了update,所以id=3的行被加了行鎖,只有commit/rollback是才會被釋放(事務中鎖是逐步獲得的,但是都是commit時所釋放的)。很好的解決了並發問題。
方法三:
update語句在更新的同時加上一個條件
$quantity = select quantity from products WHERE id=3;update products set quantity = ($quantity-1) WHERE id=3 and queantity = $quantity;
這樣雖然select語句沒有加鎖,但是因為mysql的交易隔離等級是可重複讀,所以其他事務的修改不會影響到select的結果,當執行到update時,如果有其他事務鎖住了這條記錄,update會等待,等到其他事務釋放鎖,update會執行,但此時如果quantity的數量已經被修改,update的執行會返回影響行數為0。
原因:
情境比如先select出來的$quantity=3,然後執行update的條件為id = 3 and quantity=3,執行更新返回影響函數為0,但再次執行select時發現id=3的記錄quantity確實是3啊,怎麼有這條記錄卻更新不了呢?
這”歸功於“mysql的事物隔離等級和MVCC,當第一次select時$quantity=3,然後其他事務先於這個update執行了,導致update的條件並沒有找到合適的記錄,因為在可重複讀層級中,update的讀是“當前讀”,讀取的是最新的資料。而再次select時查到的$quantity還是等於3,因為對於select的讀是“快照讀”,讀取的是曆史資料,這也是可重複讀層級的特性。
詳細的Innodb中的交易隔離等級可以看美團點評技術團隊的一篇文章,有對於MVCC和間隙鎖的解釋:http://tech.meituan.com/innodb-lock.html