悲觀鎖
悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。
悲觀鎖:假定會發生並發沖突,屏蔽一切可能違反數據完整性的操作。
Java synchronized 就屬於悲觀鎖的一種實現,每次線程要修改數據時都先獲得鎖,保證同一時刻只有一個線程能操作數據,其他線程則會被block。
悲觀鎖
樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在提交更新的時候會判斷一下在此期間別人有沒有去更新這個數據。樂觀鎖適用於讀多寫少的應用場景,這樣可以提高吞吐量
樂觀鎖:假設不會發生並發沖突,只在提交操作時檢查是否違反數據完整性
樂觀鎖一般來說有以下2種方式:
- 使用數據版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂數據版本?即為數據增加一個版本標識,一般是通過為數據庫表增加一個數字類型的 “version” 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期數據。
- 使用時間戳(timestamp)。樂觀鎖定的第二種實現方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本沖突。
Java JUC中的atomic包就是樂觀鎖的一種實現,AtomicInteger 通過CAS(Compare And Set)操作實現線程安全的自增。
MySQL InnoDB采用的是兩階段鎖定協議(two-phase locking protocol)。在事務執行過程中,隨時都可以執行鎖定,鎖只有在執行 COMMIT或者ROLLBACK的時候才會釋放,並且所有的鎖是在同一時刻被釋放。前面描述的鎖定都是隱式鎖定,InnoDB會根據事務隔離級別在需要的時候自動加鎖。
另外,InnoDB也支持通過特定的語句進行顯示鎖定,這些語句不屬於SQL規范:
- SELECT ... LOCK IN SHARE MODE
- SELECT ... FOR UPDATE
案例
通過一個小案例展示樂觀鎖和悲觀鎖的使用
考慮電商秒殺系統中,如果保證商品不超買,要保證數據的一致性
假設一張商品表
create table tb_product(
id int not null auto_increment primary key,
stock int not null
)ENGINE=InnoDB DEFAULT CHARSET=utf8
在不考慮並發的情況下,修改商品庫存的偽代碼如下:
/**
* 更新庫存(不考慮並發)
* @param productId
* @return
*/
public boolean updateStockRaw(Long productId){
ProductStock product = query("SELECT * FROM tb_product WHERE id=#{productId}", productId);
if (product.getNumber() > 0) {
int updateCnt = update("UPDATE tb_product SET stock=stock-1 WHERE id=#{productId}", productId);
if(updateCnt > 0){ //更新庫存成功
return true;
}
}
return false;
}
但是這種方式在多線程並發的情況下可能會出現超賣問題。
下面演示使用悲觀鎖和樂觀鎖來解決這個問題。
使用悲觀鎖
/**
* 更新庫存(使用悲觀鎖)
* @param productId
* @return
*/
public boolean updateStock(Long productId){
//先鎖定商品庫存記錄
ProductStock product = query("SELECT * FROM tb_product WHERE id=#{productId} FOR UPDATE", productId);
if (product.getNumber() > 0) {
int updateCnt = update("UPDATE tb_product SET stock=stock-1 WHERE id=#{productId}", productId);
if(updateCnt > 0){ //更新庫存成功
return true;
}
}
return false;
}
使用樂觀鎖
/**
* 下單減庫存
* @param productId
* @return
*/
public boolean updateStock(Long productId){
int updateCnt = 0;
while (updateCnt == 0) {
ProductStock product = query("SELECT * FROM tb_product WHERE product_id=#{productId}", productId);
if (product.getNumber() > 0) {
updateCnt = update("UPDATE tb_product SET stock=stock-1 WHERE product_id=#{productId} AND number=#{number}", productId, product.getNumber());
if(updateCnt > 0){ //更新庫存成功
return true;
}
} else { //賣完啦
return false;
}
}
return false;
}
