詳解解Spring Boot高并發(fā)鎖的使用方法
在高并發(fā)場景中(比如電商秒殺、搶票系統(tǒng)、轉(zhuǎn)賬交易),多個線程/用戶會同時操作同一共享資源(如庫存、賬戶余額、訂單號)。如果不做控制,會導致數(shù)據(jù)錯誤(如庫存超賣、余額負數(shù))、業(yè)務邏輯混亂(如重復下單)。鎖(Lock)是解決這類問題的核心工具之一。
一、概述:為什么高并發(fā)下需要鎖?
1. 高并發(fā)的“數(shù)據(jù)競爭”問題
當多個線程同時修改同一個共享資源時(如數(shù)據(jù)庫的庫存字段、內(nèi)存中的緩存值),如果沒有控制,會出現(xiàn)“數(shù)據(jù)不一致”。例如:
- 電商場景:商品庫存剩余10件,用戶A和用戶B同時下單,兩個線程同時讀取到庫存為10,都扣減1后寫回9,最終庫存變成9(實際應賣出2件,庫存應為8)。
- 轉(zhuǎn)賬場景:用戶賬戶余額100元,同時發(fā)起兩筆50元轉(zhuǎn)賬,兩個線程都讀到余額100,都扣減50后寫回50,最終余額變成50(實際應扣減100,余額0)。
2. 鎖的核心作用
鎖是一種“互斥機制”,保證同一時刻只有一個線程能操作共享資源,避免數(shù)據(jù)競爭。類比現(xiàn)實中的“公共衛(wèi)生間”:鎖門后,其他人必須等待,直到當前用戶釋放鎖(開門)。
二、鎖的類型與適用場景
在Spring Boot中,常用的鎖分為3類,需根據(jù)業(yè)務場景選擇:
| 鎖類型 | 實現(xiàn)方式 | 適用場景 | 優(yōu)點 | 缺點 |
|---|---|---|---|---|
| JVM內(nèi)置鎖 | synchronized關鍵字 | 單體應用(單進程)的小范圍并發(fā) | 代碼簡單,JVM自動管理鎖 | 無法跨進程(分布式場景無效) |
| JUC顯式鎖 | ReentrantLock(Lock接口) | 單體應用需要靈活控制鎖(如超時、可中斷) | 支持超時、可中斷、公平鎖 | 需要手動釋放鎖(否則死鎖) |
| 分布式鎖 | Redis(Redisson)、ZooKeeper | 分布式系統(tǒng)(多進程/多服務器)的并發(fā) | 跨進程協(xié)調(diào),全局唯一 | 依賴外部組件(如Redis),有性能開銷 |
三、鎖的具體使用與代碼實現(xiàn)
場景說明:模擬“電商庫存扣減”
需求:用戶下單時扣減商品庫存,要求高并發(fā)下庫存不能超賣(庫存≥0)。
假設商品ID為1001,初始庫存10件。
1. JVM內(nèi)置鎖:synchronized
適用于單體應用(只有1個Spring Boot實例),代碼簡單,JVM自動加鎖/釋放。
@Service
public class StockService {
// 模擬數(shù)據(jù)庫中的庫存(實際開發(fā)中用數(shù)據(jù)庫或緩存)
private int stock = 10;
// 下單扣減庫存(synchronized保證同一時刻只有1個線程執(zhí)行)
public synchronized boolean deductStock(int productId, int count) {
// 檢查庫存是否足夠
if (stock < count) {
return false; // 庫存不足
}
// 模擬業(yè)務耗時(如查詢數(shù)據(jù)庫、記錄日志)
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 扣減庫存
stock -= count;
System.out.println("扣減成功,剩余庫存:" + stock);
return true;
}
}
關鍵說明
synchronized修飾方法時,鎖的是當前對象(this);若修飾靜態(tài)方法,鎖的是類(StockService.class)。- 缺點:無法跨進程(如果部署多個Spring Boot實例,每個實例的
stock是獨立的,鎖無效)。
2. JUC顯式鎖:ReentrantLock
適用于單體應用,但需要更靈活的鎖控制(如設置超時、可中斷)。
@Service
public class StockService {
private int stock = 10;
// 顯式鎖(可重入鎖,支持公平/非公平)
private final ReentrantLock lock = new ReentrantLock();
public boolean deductStock(int productId, int count) {
// 嘗試加鎖(最多等待2秒,避免死鎖)
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
if (stock >= count) {
Thread.sleep(100); // 模擬業(yè)務耗時
stock -= count;
System.out.println("扣減成功,剩余庫存:" + stock);
return true;
} else {
System.out.println("庫存不足");
return false;
}
} else {
System.out.println("獲取鎖超時");
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
// 必須在finally中釋放鎖(避免異常導致鎖未釋放)
lock.unlock();
}
}
}
關鍵說明
tryLock(timeout, unit):嘗試加鎖,超時未獲取則放棄(避免線程無限等待)。finally中釋放鎖:必須手動釋放,否則其他線程永遠無法獲取鎖(死鎖)。- 優(yōu)點:比
synchronized靈活(支持超時、可中斷),適合復雜業(yè)務邏輯。
3. 分布式鎖:Redisson(基于Redis)
適用于分布式系統(tǒng)(多個Spring Boot實例部署),解決跨進程的并發(fā)問題。
@Service
public class StockService {
@Autowired
private RedissonClient redissonClient;
// 模擬數(shù)據(jù)庫庫存(實際用數(shù)據(jù)庫或緩存,如Redis存儲庫存)
private int stock = 10;
public boolean deductStock(int productId, int count) {
// 定義鎖的名稱(按商品ID隔離,不同商品用不同鎖)
String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 加鎖(自動續(xù)期,防止業(yè)務耗時過長鎖過期)
// waitTime: 等待鎖的最大時間(5秒)
// leaseTime: 鎖自動釋放時間(30秒,防止死鎖)
boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!locked) {
System.out.println("獲取鎖失敗,稍后再試");
return false;
}
// 檢查并扣減庫存
if (stock >= count) {
Thread.sleep(100); // 模擬業(yè)務耗時
stock -= count;
System.out.println("扣減成功,剩余庫存:" + stock);
return true;
} else {
System.out.println("庫存不足");
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
// 釋放鎖(只有自己加的鎖才能釋放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
關鍵說明
- 鎖名稱:用
lock:product:1001隔離不同商品,避免不同商品的庫存操作互相阻塞。 - 自動續(xù)期:Redisson默認會為鎖“續(xù)期”(每10秒續(xù)30秒),防止業(yè)務邏輯未執(zhí)行完鎖就過期(比如扣庫存需要20秒,鎖30秒過期,續(xù)期避免提前釋放)。
- 分布式場景有效性:多個Spring Boot實例通過Redis的
lockKey協(xié)調(diào),同一時刻只有1個實例能獲取鎖,避免跨進程的庫存超賣。
四、實際業(yè)務舉例:電商秒殺場景
場景描述
某商品開啟秒殺(庫存100件),1000個用戶同時點擊“立即購買”,需要保證:
- 只有前100個用戶能成功購買(庫存不超賣)。
- 后續(xù)用戶提示“已售罄”。
解決方案(分布式鎖)
- 用戶點擊下單時,先通過Redisson獲取該商品的分布式鎖(
lock:seckill:productId)。 - 獲得鎖的線程檢查庫存是否足夠(
stock > 0)。 - 庫存足夠則扣減庫存,生成訂單;否則返回“已售罄”。
- 釋放鎖,讓其他線程繼續(xù)競爭。
關鍵點
- 鎖粒度:按商品ID加鎖(如
lock:seckill:1001),不同商品的秒殺互不影響,提升并發(fā)效率。 - 防死鎖:設置鎖的自動釋放時間(如30秒),即使業(yè)務異常未釋放鎖,鎖也會自動過期。
- 性能優(yōu)化:庫存可存儲在Redis中(
GET/SET操作比數(shù)據(jù)庫快),減少數(shù)據(jù)庫壓力。
五、總結(jié)
1. 鎖的選擇原則
- 單體應用:優(yōu)先用
synchronized(簡單)或ReentrantLock(需要靈活控制)。 - 分布式系統(tǒng):必須用分布式鎖(如Redisson),避免跨進程數(shù)據(jù)競爭。
2. 注意事項
- 鎖粒度:盡量縮小鎖的范圍(只鎖共享資源的操作代碼),避免“鎖整個方法”降低性能。
- 防死鎖:設置鎖的超時時間(如
tryLock(5, 30, TimeUnit.SECONDS)),避免線程無限等待。 - 性能權衡:鎖會降低并發(fā)吞吐量(同一時刻只有1個線程操作),需結(jié)合業(yè)務場景(如秒殺允許少量延遲)。
3. 擴展思考
- 無鎖方案:對于簡單計數(shù)(如訪問量),可用
AtomicInteger(基于CAS無鎖操作),但無法解決復雜業(yè)務邏輯(如庫存扣減+訂單生成)。 - 讀寫鎖:讀多寫少場景(如商品詳情頁緩存),可用
ReentrantReadWriteLock(允許多個讀鎖并發(fā),寫鎖互斥)。
到此這篇關于詳解解Spring Boot高并發(fā)鎖的使用方法的文章就介紹到這了,更多相關Spring Boot高并發(fā)鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java如何將ResultSet結(jié)果集遍歷到List中
這篇文章主要介紹了Java如何將ResultSet結(jié)果集遍歷到List中問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02
使用Spring?Batch實現(xiàn)大數(shù)據(jù)處理的操作方法
通過使用Spring?Batch,我們可以高效地處理大規(guī)模數(shù)據(jù),本文介紹了如何配置和實現(xiàn)一個基本的Spring?Batch作業(yè),包括讀取數(shù)據(jù)、處理數(shù)據(jù)和寫入數(shù)據(jù)的全過程,感興趣的朋友跟隨小編一起看看吧2024-07-07
MybatisPlus使用queryWrapper如何實現(xiàn)復雜查詢
這篇文章主要介紹了MybatisPlus使用queryWrapper如何實現(xiàn)復雜查詢,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教。2022-01-01
你必須得會的SpringBoot全局統(tǒng)一處理異常詳解
程序在運行的過程中,不可避免會產(chǎn)生各種各樣的錯誤,這個時候就需要進行異常處理,本文主要為大家介紹了SpringBoot實現(xiàn)全局統(tǒng)一處理異常的方法,需要的可以參考一下2023-06-06
深入理解Java虛擬機 JVM 內(nèi)存結(jié)構(gòu)
本節(jié)將會介紹一下JVM的內(nèi)存結(jié)構(gòu),JVM運行時數(shù)據(jù)區(qū)的各個組成部分:堆,方法區(qū),程序計數(shù)器,Java虛擬機棧,本地方法棧,還會對Java堆的分代劃分做個簡單的介紹2021-09-09

