經典問題之樂觀鎖和悲觀鎖及使用場景
悲觀鎖
悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會block直到它拿到鎖。
悲觀鎖:假定會發生併發衝突,遮蔽一切可能違反資料完整性的操作。
Java synchronized 就屬於悲觀鎖的一種實現,每次執行緒要修改資料時都先獲得鎖,保證同一時刻只有一個執行緒能運算元據,其他執行緒則會被block。
樂觀鎖
樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在提交更新的時候會判斷一下在此期間別人有沒有去更新這個資料。樂觀鎖適用於讀多寫少的應用場景,這樣可以提高吞吐量。
樂觀鎖:假設不會發生併發衝突,只在提交操作時檢查是否違反資料完整性。
樂觀鎖一般來說有以下2種方式:
1. 使用資料版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂資料版本?即為資料增加一個版本標識,一般是通過為資料庫表增加一個數字型別的 “version” 欄位來實現。當讀取資料時,將version欄位的值一同讀出,資料每更新一次,對此version值加一。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的version值進行比對,如果資料庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期資料。
2. 使用時間戳(timestamp)。樂觀鎖定的第二種實現方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個欄位,名稱無所謂,欄位型別使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本衝突。
Java JUC中的atomic包就是樂觀鎖的一種實現,AtomicInteger 通過CAS(Compare And Set)操作實現執行緒安全的自增。
MySQL隱式和顯示鎖定
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 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){
//先鎖定商品庫存記錄
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 Swap)操作,商品庫存場景中number起到了版本控制(相當於version)的作用( AND number=#{number})。
悲觀鎖之所以是悲觀,在於他認為本次操作會發生併發衝突,所以一開始就對商品加上鎖(SELECT … FOR UPDATE),然後就可以安心的做判斷和更新,因為這時候不會有別人更新這條商品庫存。
還有一種加鎖的樂觀鎖,具體適用場景參考:
https://blog.csdn.net/JiShuiSanQianLi/article/details/85790121
lock in share mode適用於兩張表存在業務關係時的一致性要求,for update適用於操作同一張表時的一致性要求。
小結
這裡我們通過 MySQL 樂觀鎖與悲觀鎖 解決併發更新庫存的問題,當然還有其它解決方案,例如使用 分散式鎖。目前常見分散式鎖實現有兩種:基於Redis和基於Zookeeper,基於這兩種 業界也有開源的解決方案,例如 Redisson Distributed locks 、 Apache Curator Shared Lock ,這裡就不細說,網上Google 一下就有很多資料。
相關文章
- 悲觀鎖和樂觀鎖
- 小議“悲觀鎖和樂觀鎖”的原理、場景、示例
- laravel樂觀鎖和悲觀鎖Laravel
- 理解樂觀鎖和悲觀鎖
- MySQL樂觀鎖和悲觀鎖介紹MySql
- Java中的鎖之樂觀鎖與悲觀鎖Java
- mysql悲觀鎖以樂觀鎖MySql
- 面試必備之悲觀鎖與樂觀鎖面試
- 面試必備之樂觀鎖與悲觀鎖面試
- Java多執行緒的悲觀鎖與樂觀鎖及各自適用場景Java執行緒
- MySQL鎖(樂觀鎖、悲觀鎖、多粒度鎖)MySql
- Redis的事務、樂觀鎖和悲觀鎖Redis
- SQLServer樂觀鎖定和悲觀鎖定例項SQLServer
- 樂觀鎖與悲觀鎖及應用舉例
- java-樂觀鎖與悲觀鎖Java
- MybatisPlus - [03] 樂觀鎖&悲觀鎖MyBatis
- SQL SERVER樂觀鎖定和悲觀鎖定使用例項SQLServer
- 資料庫中的悲觀鎖和樂觀鎖資料庫
- JPA和Hibernate的樂觀鎖與悲觀鎖
- 面試官:你說說互斥鎖、自旋鎖、讀寫鎖、悲觀鎖、樂觀鎖的應用場景面試
- Java 中的悲觀鎖和樂觀鎖的實現Java
- Spring Boot2+JPA之悲觀鎖和樂觀鎖實戰Spring Boot
- MySQL 悲觀鎖與樂觀鎖的詳解MySql
- Java彌散系列 - 樂觀鎖與悲觀鎖Java
- 關於庫存超賣問題,悲觀鎖和樂觀鎖的不同實現
- 利用MySQL中的樂觀鎖和悲觀鎖實現分散式鎖MySql分散式
- 樂觀鎖和悲觀鎖策略的區別與實現
- 【鎖機制】共享鎖、排它鎖、悲觀鎖、樂觀鎖、死鎖等等
- 樂觀鎖和悲觀鎖在kubernetes中的應用
- 面試必備之樂觀鎖與悲觀鎖(程式設計師必看)面試程式設計師
- 【雜談】JPA樂觀鎖改悲觀鎖遇到的一些問題與思考
- Java併發程式設計(05):悲觀鎖和樂觀鎖機制Java程式設計
- [轉帖]SQL Server 鎖機制 悲觀鎖 樂觀鎖 實測解析SQLServer
- 悲觀鎖與樂觀鎖的實現(詳情圖解)圖解
- 【每日鮮蘑】從資料庫看樂觀鎖、悲觀鎖資料庫
- 面試必備的資料庫悲觀鎖與樂觀鎖面試資料庫
- 關於樂觀鎖與悲觀鎖的實際應用
- SSM (十五) 樂觀鎖與悲觀鎖的實際應用SSM