目錄
1. 什么是分布式鎖?
分布式鎖的核心要求
2. 基于Redis的分布式鎖實現方案
(1)基礎方案:SETNX + EXPIRE
(2)優化方案:SET NX PX(原子性加鎖)
(3)進階方案:RedLock(Redis官方推薦)
3. 實戰案例
案例1:防止重復下單
1. 加鎖階段
2. 業務邏輯階段
3. 釋放鎖階段
案例2:秒殺庫存扣減
案例3:分布式定時任務調度
4. 常見問題與解決方案
(1)鎖過期但業務未執行完?
(2)鎖被其他客戶端誤刪?
(3)Redis主從切換導致鎖丟失?
5. 總結
1. 什么是分布式鎖?
在分布式系統中,多個服務實例可能同時訪問共享資源(如數據庫、緩存等),為了避免并發問題(如超賣、重復提交等),我們需要一種跨JVM的鎖機制——分布式鎖。
分布式鎖的核心要求
-
互斥性:同一時刻只有一個客戶端能持有鎖。
-
防死鎖:即使客戶端崩潰,鎖也能自動釋放。
-
高可用:鎖服務必須高可用(如Redis集群)。
-
可重入性(可選):同一個客戶端可以多次獲取同一把鎖。
2. 基于Redis的分布式鎖實現方案
Redis因其高性能和原子性操作(如SETNX
),成為實現分布式鎖的常用方案。
(1)基礎方案:SETNX + EXPIRE
// 加鎖(錯誤示范,非原子性) Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:order123", "1"); if (locked) {redisTemplate.expire("lock:order123", 10, TimeUnit.SECONDS); // 設置過期時間// 執行業務邏輯...redisTemplate.delete("lock:order123"); // 釋放鎖 }
問題:SETNX
和EXPIRE
不是原子操作,如果加鎖后客戶端崩潰,鎖永遠不會釋放!
(2)優化方案:SET NX PX(原子性加鎖)
Redis 2.6+ 支持SET
命令的NX
(不存在才設置)和PX
(毫秒級過期時間)參數:
// 正確方式:原子性加鎖 + 設置過期時間 Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:order123", "client1", 10, TimeUnit.SECONDS );if (locked) {try {// 執行業務邏輯...} finally {// 釋放鎖(需判斷是否是自己加的鎖)if ("client1".equals(redisTemplate.opsForValue().get("lock:order123"))) {redisTemplate.delete("lock:order123");}} }
關鍵改進:
-
使用
SET NX PX
保證原子性。 -
設置唯一標識(如
client1
),避免誤刪其他客戶端的鎖。
(3)進階方案:RedLock(Redis官方推薦)
如果單點Redis不可靠,可以使用RedLock算法(需多個獨立Redis實例):
// RedLock示例(使用Redisson客戶端) RLock lock1 = redissonClient1.getLock("lock:order123"); RLock lock2 = redissonClient2.getLock("lock:order123"); RLock lock3 = redissonClient3.getLock("lock:order123");RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3); try {if (redLock.tryLock(10, 30, TimeUnit.SECONDS)) { // 最多等待10秒,鎖30秒后自動過期// 執行業務邏輯...} } finally {redLock.unlock(); }
適用場景:對一致性要求極高的場景(如金融交易)。
3. 實戰案例
案例1:防止重復下單
public String createOrder(String userId, String productId) {String lockKey = "lock:order:" + userId + ":" + productId;String clientId = UUID.randomUUID().toString(); // 唯一標識try {// 嘗試加鎖(等待5秒,鎖10秒后自動釋放)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);if (!locked) {throw new RuntimeException("操作太頻繁,請稍后再試!");}// 檢查是否已下單if (orderService.hasOrder(userId, productId)) {throw new RuntimeException("請勿重復下單!");}// 創建訂單...return orderService.create(userId, productId);} finally {// 釋放鎖(需校驗clientId)if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);}} }
這段代碼實現了一個防并發重復下單的訂單創建邏輯,核心是使用Redis分布式鎖來保證同一用戶對同一商品的訂單操作是串行化的。下面逐部分解析:
1. 加鎖階段
String lockKey = "lock:order:" + userId + ":" + productId; String clientId = UUID.randomUUID().toString();
-
lockKey
:鎖的鍵,格式為lock:order:{userId}:{productId}
,確保不同用戶或不同商品的鎖互不影響。 -
clientId
:生成唯一標識(UUID),用于后續校驗鎖的歸屬,防止誤刪其他客戶端的鎖。
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS );
-
setIfAbsent
:Redis的SETNX
命令(原子性操作),如果lockKey
不存在則加鎖,并設置:-
值:
clientId
(鎖的持有者標識)。 -
過期時間:10秒(防止死鎖)。
-
-
返回值:
true
表示加鎖成功,false
表示鎖已被占用。
if (!locked) {throw new RuntimeException("操作太頻繁,請稍后再試!"); }
-
如果加鎖失敗,直接拋出異常,提示用戶"操作太頻繁"(類似秒殺場景的限流)。
2. 業務邏輯階段
if (orderService.hasOrder(userId, productId)) {throw new RuntimeException("請勿重復下單!"); }
-
檢查是否已下單:在鎖的保護下查詢訂單系統,防止重復下單(即使通過了前端校驗,仍需后端保證冪等性)。
return orderService.create(userId, productId);
-
創建訂單:執行業務邏輯(如扣減庫存、生成訂單等)。
3. 釋放鎖階段
finally {if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);} }
-
finally
塊:確保鎖一定會被釋放,即使業務邏輯拋出異常。 -
校驗
clientId
:-
只釋放自己加的鎖(避免誤刪其他客戶端的鎖)。
-
如果鎖已自動過期(10秒后),
get
操作返回null
,不會執行刪除。
-
-
原子性問題:
-
這里的
get
和delete
是兩步操作,非原子性,極端情況下可能誤刪鎖(如鎖過期后,其他客戶端加鎖成功,但當前線程仍執行刪除)。 -
改進方案:使用Lua腳本保證原子性(見下文補充)。
-
案例2:秒殺庫存扣減
public boolean seckill(Long productId, Long userId) {String lockKey = "lock:seckill:" + productId;String stockKey = "stock:" + productId;String clientId = UUID.randomUUID().toString();try {// 加鎖(防止超賣)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 3, // 鎖3秒(避免長時間阻塞)TimeUnit.SECONDS);if (!locked) {return false; // 搶鎖失敗}// 檢查庫存Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(stockKey));if (stock <= 0) {return false; // 已售罄}// 扣減庫存(原子操作)redisTemplate.opsForValue().decrement(stockKey);// 生成訂單...orderService.createSeckillOrder(userId, productId);return true;} finally {// 釋放鎖if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);}} }
案例3:分布式定時任務調度
多個服務實例同時運行定時任務時,需確保只有一個實例執行:
@Scheduled(cron = "0 */5 * * * ?") // 每5分鐘執行一次 public void scheduledTask() {String lockKey = "lock:scheduled:report";String clientId = "server-" + System.getProperty("server.port"); // 用服務實例標識try {// 嘗試加鎖(鎖5分鐘)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 5, TimeUnit.MINUTES);if (!locked) {return; // 其他實例已執行}// 執行業務邏輯(生成報表...)reportService.generateDailyReport();} finally {// 釋放鎖if (clientId.equals(redisTemplate.opsForValue().get(lockKey))) {redisTemplate.delete(lockKey);}} }
4. 常見問題與解決方案
(1)鎖過期但業務未執行完?
-
問題:鎖自動釋放后,其他客戶端可能獲取鎖,導致并發問題。
-
解決方案:使用看門狗機制(如Redisson的
lockWatchdogTimeout
),自動續期鎖。
(2)鎖被其他客戶端誤刪?
-
問題:客戶端A釋放了客戶端B的鎖。
-
解決方案:加鎖時設置唯一標識(如UUID),釋放時校驗。
(3)Redis主從切換導致鎖丟失?
-
問題:主節點加鎖后崩潰,從節點晉升但未同步鎖數據。
-
解決方案:使用RedLock(多Redis實例)或ZooKeeper替代。
5. 總結
方案 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
SETNX + EXPIRE | 簡單高效 | 非原子性,可能死鎖 | 低并發場景 |
SET NX PX | 原子操作 | 單點故障 | 一般分布式系統 |
RedLock | 高可用 | 實現復雜 | 金融級高一致性場景 |
最佳實踐:
-
優先使用
SET NX PX
?+ 唯一標識。 -
高可用場景選擇Redisson的
RLock
或RedLock。 -
結合業務設置合理的鎖超時時間。