Spring Boot高并發 鎖的使用方法
在高并發場景中(比如電商秒殺、搶票系統、轉賬交易),多個線程/用戶會同時操作同一共享資源(如庫存、賬戶余額、訂單號)。如果不做控制,會導致數據錯誤(如庫存超賣、余額負數)、業務邏輯混亂(如重復下單)。鎖(Lock)是解決這類問題的核心工具之一。
一、概述:為什么高并發下需要鎖?
1. 高并發的“數據競爭”問題
當多個線程同時修改同一個共享資源時(如數據庫的庫存字段、內存中的緩存值),如果沒有控制,會出現“數據不一致”。例如:
- 電商場景:商品庫存剩余10件,用戶A和用戶B同時下單,兩個線程同時讀取到庫存為10,都扣減1后寫回9,最終庫存變成9(實際應賣出2件,庫存應為8)。
- 轉賬場景:用戶賬戶余額100元,同時發起兩筆50元轉賬,兩個線程都讀到余額100,都扣減50后寫回50,最終余額變成50(實際應扣減100,余額0)。
2. 鎖的核心作用
鎖是一種“互斥機制”,保證同一時刻只有一個線程能操作共享資源,避免數據競爭。類比現實中的“公共衛生間”:鎖門后,其他人必須等待,直到當前用戶釋放鎖(開門)。
二、鎖的類型與適用場景
在Spring Boot中,常用的鎖分為3類,需根據業務場景選擇:
鎖類型 | 實現方式 | 適用場景 | 優點 | 缺點 |
---|---|---|---|---|
JVM內置鎖 | synchronized 關鍵字 | 單體應用(單進程)的小范圍并發 | 代碼簡單,JVM自動管理鎖 | 無法跨進程(分布式場景無效) |
JUC顯式鎖 | ReentrantLock (Lock接口) | 單體應用需要靈活控制鎖(如超時、可中斷) | 支持超時、可中斷、公平鎖 | 需要手動釋放鎖(否則死鎖) |
分布式鎖 | Redis(Redisson)、ZooKeeper | 分布式系統(多進程/多服務器)的并發 | 跨進程協調,全局唯一 | 依賴外部組件(如Redis),有性能開銷 |
三、鎖的具體使用與代碼實現
場景說明:模擬“電商庫存扣減”
需求:用戶下單時扣減商品庫存,要求高并發下庫存不能超賣(庫存≥0)。
假設商品ID為1001,初始庫存10件。
1. JVM內置鎖:synchronized
適用于單體應用(只有1個Spring Boot實例),代碼簡單,JVM自動加鎖/釋放。
@Service
public class StockService {// 模擬數據庫中的庫存(實際開發中用數據庫或緩存)private int stock = 10;// 下單扣減庫存(synchronized保證同一時刻只有1個線程執行)public synchronized boolean deductStock(int productId, int count) {// 檢查庫存是否足夠if (stock < count) {return false; // 庫存不足}// 模擬業務耗時(如查詢數據庫、記錄日志)try {Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}// 扣減庫存stock -= count;System.out.println("扣減成功,剩余庫存:" + stock);return true;}
}
關鍵說明
synchronized
修飾方法時,鎖的是當前對象(this
);若修飾靜態方法,鎖的是類(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); // 模擬業務耗時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
中釋放鎖:必須手動釋放,否則其他線程永遠無法獲取鎖(死鎖)。- 優點:比
synchronized
靈活(支持超時、可中斷),適合復雜業務邏輯。
3. 分布式鎖:Redisson(基于Redis)
適用于分布式系統(多個Spring Boot實例部署),解決跨進程的并發問題。
@Service
public class StockService {@Autowiredprivate RedissonClient redissonClient;// 模擬數據庫庫存(實際用數據庫或緩存,如Redis存儲庫存)private int stock = 10;public boolean deductStock(int productId, int count) {// 定義鎖的名稱(按商品ID隔離,不同商品用不同鎖)String lockKey = "lock:product:" + productId;RLock lock = redissonClient.getLock(lockKey);try {// 加鎖(自動續期,防止業務耗時過長鎖過期)// 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); // 模擬業務耗時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
隔離不同商品,避免不同商品的庫存操作互相阻塞。 - 自動續期:Redisson默認會為鎖“續期”(每10秒續30秒),防止業務邏輯未執行完鎖就過期(比如扣庫存需要20秒,鎖30秒過期,續期避免提前釋放)。
- 分布式場景有效性:多個Spring Boot實例通過Redis的
lockKey
協調,同一時刻只有1個實例能獲取鎖,避免跨進程的庫存超賣。
四、實際業務舉例:電商秒殺場景
場景描述
某商品開啟秒殺(庫存100件),1000個用戶同時點擊“立即購買”,需要保證:
- 只有前100個用戶能成功購買(庫存不超賣)。
- 后續用戶提示“已售罄”。
解決方案(分布式鎖)
- 用戶點擊下單時,先通過Redisson獲取該商品的分布式鎖(
lock:seckill:productId
)。 - 獲得鎖的線程檢查庫存是否足夠(
stock > 0
)。 - 庫存足夠則扣減庫存,生成訂單;否則返回“已售罄”。
- 釋放鎖,讓其他線程繼續競爭。
關鍵點
- 鎖粒度:按商品ID加鎖(如
lock:seckill:1001
),不同商品的秒殺互不影響,提升并發效率。 - 防死鎖:設置鎖的自動釋放時間(如30秒),即使業務異常未釋放鎖,鎖也會自動過期。
- 性能優化:庫存可存儲在Redis中(
GET/SET
操作比數據庫快),減少數據庫壓力。
五、總結
1. 鎖的選擇原則
- 單體應用:優先用
synchronized
(簡單)或ReentrantLock
(需要靈活控制)。 - 分布式系統:必須用分布式鎖(如Redisson),避免跨進程數據競爭。
2. 注意事項
- 鎖粒度:盡量縮小鎖的范圍(只鎖共享資源的操作代碼),避免“鎖整個方法”降低性能。
- 防死鎖:設置鎖的超時時間(如
tryLock(5, 30, TimeUnit.SECONDS)
),避免線程無限等待。 - 性能權衡:鎖會降低并發吞吐量(同一時刻只有1個線程操作),需結合業務場景(如秒殺允許少量延遲)。
3. 擴展思考
- 無鎖方案:對于簡單計數(如訪問量),可用
AtomicInteger
(基于CAS無鎖操作),但無法解決復雜業務邏輯(如庫存扣減+訂單生成)。 - 讀寫鎖:讀多寫少場景(如商品詳情頁緩存),可用
ReentrantReadWriteLock
(允許多個讀鎖并發,寫鎖互斥)。