場景:
一個商品有庫存,下單時先檢查庫存,如果>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