鎖的應用與場景:從單機到分布式
摘要:在多線程和分布式系統中,“鎖”是避免資源競爭、保障數據一致性的核心機制。但你真的了解鎖嗎?什么時候該用鎖?用哪種鎖?本文通過通俗的比喻和代碼示例,帶你徹底搞懂鎖的應用場景!
一、為什么需要鎖?
想象一下:多人同時編輯同一份文檔,如果不加控制,最終文檔內容會亂成一鍋粥。程序中的共享資源(如數據庫字段、文件、內存變量)同樣面臨這個問題——鎖的作用就是讓多個線程/進程“排隊”訪問資源。
常見問題場景:
- 訂單重復處理:用戶瘋狂點擊提交訂單,導致重復扣款。
- 超賣問題:秒殺活動中庫存被減到負數。
- 數據覆蓋:兩個線程同時修改用戶余額,后者覆蓋前者結果。
二、單機環境下的鎖
1. 樂觀鎖 vs 悲觀鎖
- 悲觀鎖:假設一定會發生沖突,先加鎖再操作。
-
應用場景:沖突頻繁、臨界區代碼執行時間長。
-
實現方式:
- 數據庫:
SELECT ... FOR UPDATE
- Java:
synchronized
、ReentrantLock
- 數據庫:
-
維度 | synchronized | ReentrantLock |
---|---|---|
鎖管理 | JVM 自動管理,無需手動釋放 | 需手動獲取和釋放,易忘記導致死鎖 |
靈活性 | 功能簡單,僅支持非公平鎖 | 支持公平鎖、超時、中斷、多條件變量 |
性能 | JVM 優化后性能接近(低競爭場景更優) | 高并發場景更靈活(如 tryLock 減少競爭) |
調試支持 | 鎖信息不易獲取(如等待隊列長度) | 提供 isLocked() , getQueueLength() 等方法 |
適用場景 | 簡單同步需求(如單方法內的線程安全) | 復雜同步邏輯(如多條件協調、精細控制) |
- 樂觀鎖:假設沖突很少,先操作再檢查是否沖突。
- 應用場景:讀多寫少、沖突概率低。
- 實現方式:
- 數據庫:版本號(Version字段)+ CAS更新
- Java:
AtomicInteger
、StampedLock
代碼示例:數據庫樂觀鎖
-- 1. 查詢時獲取版本號
SELECT stock, version FROM product WHERE id = 1;-- 2. 更新時校驗版本號
UPDATE product SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 1; -- 如果version被修改過,更新失敗
2. 讀寫鎖(ReadWriteLock)
- 核心思想:讀操作不互斥,寫操作互斥。
- 應用場景:讀多寫少,如緩存系統。
- Java實現:
ReentrantReadWriteLock
ReadWriteLock rwLock = new ReentrantReadWriteLock();// 讀操作
rwLock.readLock().lock();
try {// 讀取數據(允許多個線程同時讀)
} finally {rwLock.readLock().unlock();
}// 寫操作
rwLock.writeLock().lock();
try {// 修改數據(獨占鎖)
} finally {rwLock.writeLock().unlock();
}
三、分布式鎖
當服務部署在多臺機器上時(即使在同一臺物理機上的多個容器/Pod),單機鎖失效,必須使用分布式鎖協調跨進程的資源訪問。
1. 常見實現方案
方案 | 核心原理 | 優點 | 缺點 |
---|---|---|---|
Redis鎖 | SETNX + 過期時間 + Lua腳本刪除 | 性能高,實現簡單 | 存在鎖過期提前釋放風險 |
ZooKeeper | 創建臨時順序節點,監聽前序節點刪除 | 可靠性高,自動釋放鎖 | 性能較低,需要維護ZK集群 |
數據庫鎖 | 基于唯一索引或行級鎖 | 無需額外組件 | 性能差,高并發易成瓶頸 |
- 高并發且允許偶發鎖失效:Redis + Redisson。
- 強一致性需求:ZooKeeper 或 Etcd。
- 簡單場景:數據庫(不推薦生產環境高頻使用)
2. Redis分布式鎖示例(Redisson實現)
// 1. 獲取鎖對象
RLock lock = redissonClient.getLock("orderLock");// 2. 嘗試加鎖(等待10秒,鎖自動釋放時間30秒)
boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (isLocked) {try {// 處理業務邏輯processOrder();} finally {lock.unlock();}
}
四、如何選擇合適的鎖?
1. 決策流程圖
2. 黃金原則
- 能用單機鎖就別用分布式鎖(復雜度陡增)
- 鎖粒度要小:鎖住的范圍越小,性能越高
- 優先考慮無鎖設計:如使用線程安全的類(
ConcurrentHashMap
)、本地線程存儲(ThreadLocal
)
五、避坑指南
- 死鎖:避免嵌套鎖,設置超時時間。
- 鎖饑餓:公平鎖可緩解,但性能會下降。
- 鎖泄露:確保finally塊中釋放鎖。
- 腦裂問題(分布式鎖):選擇強一致性協調器(如ZooKeeper)。
六、總結
- 單機多線程:本地鎖 + 數據庫唯一索引即可滿足需求,無需分布式鎖。優先選
synchronized
或ReentrantLock
。 - 多機/多實例:必須引入分布式鎖,同時結合數據庫約束保證最終安全。
- 高并發讀:讀寫鎖(
ReadWriteLock
)是救星。 - 分布式系統:Redis鎖(性能)或ZooKeeper鎖(可靠性)二選一。
- 終極目標:在安全性和性能之間找到平衡!
技術沒有銀彈,理解場景才能選出最合適的鎖!