解決並發情況下庫存減為負數問題--update2016.04.24


場景:

一個商品有庫存,下單時先檢查庫存,如果>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的讀是“快照讀”,讀取的是歷史數據,這也是可重復讀級別的特性。

注意方法二中:當一個事務提交后如果庫存正好是0,此時另一個事務讀取到的還是1,這就是可重復讀,但是由於先執行的update,update執行完之后的庫存就是-1,此時再執行select的時候的結果就是-1,所以可重復讀是指當前事務沒有寫語句的時候,如果寫語句發生則是建立在其他事務提交后的結果基礎上的。

詳細的Innodb中的事務隔離級別可以看美團點評技術團隊的一篇文章,有對於MVCC和間隙鎖的解釋:http://tech.meituan.com/innodb-lock.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM