悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。html
悲觀鎖:假定會發生併發衝突,屏蔽一切可能違反數據完整性的操做。git
Java synchronized 就屬於悲觀鎖的一種實現,每次線程要修改數據時都先得到鎖,保證同一時刻只有一個線程能操做數據,其餘線程則會被block。github
樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在提交更新的時候會判斷一下在此期間別人有沒有去更新這個數據。樂觀鎖適用於讀多寫少的應用場景,這樣能夠提升吞吐量。redis
樂觀鎖:假設不會發生併發衝突,只在提交操做時檢查是否違反數據完整性。數據庫
樂觀鎖通常來講有如下2種方式:
1. 使用數據版本(Version)記錄機制實現,這是樂觀鎖最經常使用的一種實現方式。何謂數據版本?即爲數據增長一個版本標識,通常是經過爲數據庫表增長一個數字類型的 「version」 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當咱們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,若是數據庫表當前版本號與第一次取出來的version值相等,則予以更新,不然認爲是過時數據。
2. 使用時間戳(timestamp)。樂觀鎖定的第二種實現方式和第一種差很少,一樣是在須要樂觀鎖控制的table中增長一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的version相似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和本身更新前取到的時間戳進行對比,若是一致則OK,不然就是版本衝突。apache
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
接下來,咱們經過一個具體案例來進行分析:考慮電商系統中的下單流程,商品的庫存量是固定的,如何保證商品數量不超賣? 其實須要保證數據一致性:某我的點擊秒殺後系統中查出來的庫存量和實際扣減庫存時庫存量的一致性就能夠。
假設,MySQL數據庫中商品庫存表tb_product_stock 結構定義以下:
CREATE TABLE `tb_product_stock` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID', `product_id` bigint(32) NOT NULL COMMENT '商品ID', `number` INT(8) NOT NULL DEFAULT 0 COMMENT '庫存數量', `create_time` DATETIME NOT NULL COMMENT '建立時間', `modify_time` DATETIME NOT NULL COMMENT '更新時間', PRIMARY KEY (`id`), UNIQUE KEY `index_pid` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品庫存表';
對應的POJO類:
class ProductStock { private Long productId; //商品id private Integer number; //庫存量 public Long getProductId() { return productId; } public void setProductId(Long productId) { this.productId = productId; } public Integer getNumber() { return number; } public void setNumber(Integer number) { this.number = number; } }
不考慮併發的狀況下,更新庫存代碼以下:
/** * 更新庫存(不考慮併發) * @param productId * @return */ public boolean updateStockRaw(Long productId){ ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId); if (product.getNumber() > 0) { int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number=#{number}", productId, product.getNumber()); if(updateCnt > 0){ //更新庫存成功 return true; } } return false; }
多線程併發狀況下,會存在超賣的可能。
/** * 更新庫存(使用悲觀鎖) * @param productId * @return */ public boolean updateStock(Long productId){ //先鎖定商品庫存記錄 ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId} FOR UPDATE", productId); if (product.getNumber() > 0) { int updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_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_stock WHERE product_id=#{productId}", productId);
if (product.getNumber() > 0) {
updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number=#{number}", productId, product.getNumber());
if(updateCnt > 0){ //更新庫存成功
return true;
}
} else { //賣完啦
return false;
}
}
return false;
}
使用樂觀鎖更新庫存的時候不加鎖,當提交更新時須要判斷數據是否已經被修改(AND number=#{number}),只有在 number等於上一次查詢到的number時 才提交更新。
樂觀鎖的思路通常是表中增長版本字段,更新時where語句中增長版本的判斷,算是一種CAS(Compare And Swep)操做,商品庫存場景中number起到了版本控制(至關於version)的做用( AND number=#{number})。
悲觀鎖之因此是悲觀,在於他認爲本次操做會發生併發衝突,因此一開始就對商品加上鎖(SELECT … FOR UPDATE),而後就能夠安心的作判斷和更新,由於這時候不會有別人更新這條商品庫存。
什麼場景須要使用鎖,什麼場景不須要使用鎖?
從中咱們也能夠知道只要更新數據是依賴讀取的數據做爲基礎條件的,就會有併發更新問題,須要樂觀鎖或者悲觀鎖取解決,特別實在計數表現明顯。
又好比在更新數據不依賴查詢的數據的就不會有問題,例如修改用戶的名稱,多人同時修改,結果並不依賴於以前的用戶名字,這就不會有併發更新問題。
這裏咱們經過 MySQL 樂觀鎖與悲觀鎖 解決併發更新庫存的問題,固然還有其它解決方案,例如使用 分佈式鎖。目前常見分佈式鎖實現有兩種:基於Redis和基於Zookeeper,基於這兩種 業界也有開源的解決方案,例如 Redisson Distributed locks 、 Apache Curator Shared Lock ,這裏就不細說,網上Google 一下就有不少資料。