如何使用Redis 實現(xiàn)分布式鎖(含自動續(xù)期與安全釋放)
用 Redis 實現(xiàn)分布式鎖(含自動續(xù)期與安全釋放)詳解
在分布式系統(tǒng)中,多個服務實例可能同時操作共享資源(如庫存扣減、訂單生成),為保證數(shù)據(jù)一致性,必須使用 分布式鎖。Redis 憑借其高性能和原子操作能力,成為實現(xiàn)分布式鎖的常用選擇。
本文將深入講解如何用 Redis 實現(xiàn)一個 安全、可重入、帶自動續(xù)期(Watchdog)、支持高可用 的分布式鎖,并提供 Java 實現(xiàn)示例(含 Redisson 與原生 Lua 腳本兩種方式)。
一、分布式鎖的核心要求
| 要求 | 說明 |
|---|---|
| ? 互斥性 | 同一時間只有一個客戶端能持有鎖 |
| ? 可重入性 | 同一線程可多次獲取同一把鎖 |
| ? 鎖釋放安全 | 只能由加鎖的客戶端釋放(防誤刪) |
| ? 自動續(xù)期(Watchdog) | 防止業(yè)務執(zhí)行時間超過鎖過期時間 |
| ? 高可用 | 支持主從、集群、哨兵模式 |
| ? 高性能 | 加鎖/釋放速度快,不影響業(yè)務 |
二、基礎實現(xiàn):SET + NX + EX
最簡單的分布式鎖實現(xiàn):
SET lock:order:12345 "client_1" NX EX 10
NX:key 不存在時才設置(保證互斥)EX 10:10秒后自動過期(防死鎖)client_1:唯一客戶端標識(用于釋放時校驗)
? 問題:無續(xù)期機制,業(yè)務執(zhí)行超時會自動釋放,導致并發(fā)。
三、安全釋放鎖:Lua 腳本防誤刪
直接 DEL 鎖可能誤刪其他客戶端的鎖。應使用 Lua 腳本校驗 value:
? Lua 腳本(unlock.lua)
-- KEYS[1] = lock key
-- ARGV[1] = client_id
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
endJava 調(diào)用示例:
public boolean unlock(String lockKey, String clientId) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
" else " +
" return 0 " +
" end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList(lockKey),
clientId
);
return result != null && result == 1;
}四、自動續(xù)期(Watchdog 機制)
如果業(yè)務執(zhí)行時間超過鎖過期時間,鎖會被自動釋放,導致多個客戶端同時進入臨界區(qū)。
解決方案:啟動一個后臺線程(Watchdog),每隔一段時間檢查鎖是否仍被持有,若持有則延長過期時間。
Watchdog 工作流程:
客戶端A加鎖(EX 30s)
↓
啟動 Watchdog 線程(每10s檢查一次)
↓
若鎖仍存在且屬于本客戶端 → 執(zhí)行 EXPIRE lock:xxx 30
↓
業(yè)務執(zhí)行完成 → 取消續(xù)期 + 釋放鎖
五、完整實現(xiàn)方案對比
| 方案 | 是否推薦 | 說明 |
|---|---|---|
| ?? 原生 SET + Lua | ?? 基礎可用 | 需自行實現(xiàn)續(xù)期、可重入 |
| ?? Redisson(推薦) | ? 強烈推薦 | 內(nèi)置 Watchdog、可重入、公平鎖等 |
| ?? Jedis + 自研 | ? 不推薦 | 容易出錯,維護成本高 |
六、使用 Redisson 實現(xiàn)(推薦方案)
Redisson 提供了開箱即用的分布式鎖,完美支持:
- 可重入
- 自動續(xù)期(Watchdog)
- 公平鎖、讀寫鎖
- 高可用(集群/哨兵)
1. 添加依賴
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.24.1</version>
</dependency>2. 獲取鎖并自動續(xù)期
@Autowired
private RedissonClient redissonClient;
public void doBusiness() {
RLock lock = redissonClient.getLock("lock:order:12345");
try {
// 嘗試加鎖,最多等待10秒,上鎖后30秒自動解鎖
boolean res = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
try {
// 執(zhí)行業(yè)務邏輯(可能耗時較長)
System.out.println("Locked! Doing business...");
Thread.sleep(25000); // 模擬長任務
} finally {
lock.unlock(); // 自動取消續(xù)期 + 安全釋放
}
} else {
System.out.println("Failed to acquire lock");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}3. Redisson 的 Watchdog 原理
- 默認鎖過期時間:
30s - Watchdog 每
10s檢查一次 - 若客戶端仍持有鎖 → 自動
EXPIRE lock:xxx 30 - 解鎖時自動取消續(xù)期
? 無需擔心業(yè)務超時,只要客戶端存活,鎖就不會被釋放。
七、原生 Redis + Lua 實現(xiàn)(學習用)
如果你不想使用 Redisson,也可自行實現(xiàn) Watchdog。
1. 加鎖(SETNX + EX)
public boolean tryLock(String lockKey, String clientId, int expireSeconds) {
String result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, clientId, expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}2. 啟動 Watchdog 線程
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private volatile boolean isLocked = false;
public void watchDog(String lockKey, String clientId, int expireSeconds) {
isLocked = true;
scheduler.scheduleAtFixedRate(() -> {
if (isLocked) {
// 只有當前客戶端持有鎖時才續(xù)期
String current = redisTemplate.opsForValue().get(lockKey);
if (clientId.equals(current)) {
redisTemplate.expire(lockKey, expireSeconds, TimeUnit.SECONDS);
}
}
}, expireSeconds / 3, expireSeconds / 3, TimeUnit.SECONDS);
}3. 釋放鎖(Lua 腳本)
見前文 Lua 腳本實現(xiàn)。
4. 使用示例
String lockKey = "lock:order:12345";
String clientId = "client_" + Thread.currentThread().getId();
if (tryLock(lockKey, clientId, 30)) {
try {
watchDog(lockKey, clientId, 30); // 啟動續(xù)期
// 執(zhí)行業(yè)務
} finally {
isLocked = false; // 停止續(xù)期
unlock(lockKey, clientId); // 安全釋放
}
}八、可重入鎖實現(xiàn)思路
可重入鎖需記錄:
- 當前持有線程
- 重入次數(shù)
可用 Hash 結(jié)構(gòu)實現(xiàn):
# 鎖結(jié)構(gòu) lock:order:12345 field: client_1 value: 2 # 重入次數(shù)
加鎖時:
- 若 key 不存在 → 設置 client_1:1
- 若 key 存在且 field == client_1 → value +1
- 否則失敗
釋放時:
- value -1,為 0 時刪除 key
九、最佳實踐與注意事項
| 項目 | 建議 |
|---|---|
| ?? 安全釋放 | 必須使用 Lua 腳本校驗 client_id |
| ?? 鎖過期時間 | 設置合理(如 10~30s),避免過長導致阻塞 |
| ?? 自動續(xù)期 | 使用 Redisson 或自研 Watchdog |
| ?? 可重入 | 生產(chǎn)環(huán)境必須支持 |
| ?? 阻塞操作 | 鎖內(nèi)避免網(wǎng)絡調(diào)用、sleep |
| ?? 監(jiān)控 | 記錄加鎖失敗、等待時間 |
| ?? 異常處理 | 確保 finally 中釋放鎖 |
| ?? 高可用 | 使用 Redis 集群或哨兵 |
十、常見問題(FAQ)
Q1:Redis 主從切換會導致鎖失效嗎?
? 會!主節(jié)點加鎖后未同步到從節(jié)點,主節(jié)點宕機,從節(jié)點升主,鎖丟失。
解決方案:
- 使用 Redlock 算法(多個獨立 Redis 實例)
- 使用 ZooKeeper 或 etcd 實現(xiàn)更安全的分布式鎖
- 多數(shù)場景下,Redisson + 主從已足夠(犧牲 CAP 中的 CP)
Q2:Watchdog 占用資源嗎?
? 占用少量 CPU 和連接,但可接受。Redisson 默認只在持有鎖時啟動。
Q3:能用 SETNX + DEL 嗎?
? 不安全!DEL 可能誤刪其他客戶端的鎖。
十一、總結(jié):Redis 分布式鎖實現(xiàn)方案對比
| 方案 | 是否可重入 | 自動續(xù)期 | 安全釋放 | 推薦度 |
|---|---|---|---|---|
| SETNX + DEL | ? | ? | ? | ? |
| SETNX + Lua | ?? | ? | ? | ??? |
| 自研 Watchdog | ?? | ? | ? | ???? |
| Redisson | ? | ? | ? | ????? |
? 結(jié)語:
使用 Redis 實現(xiàn)分布式鎖,強烈推薦使用 Redisson。它封裝了復雜的細節(jié)(可重入、續(xù)期、安全釋放),讓你像使用本地鎖一樣操作分布式鎖。
?? 核心代碼一句話:
RLock lock = redisson.getLock("myLock"); lock.tryLock(10, 30, TimeUnit.SECONDS);簡單、安全、高效,是生產(chǎn)環(huán)境的最佳實踐。
到此這篇關于如何使用Redis 實現(xiàn)分布式鎖(含自動續(xù)期與安全釋放)的文章就介紹到這了,更多相關Redis分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Redis配置外網(wǎng)可訪問(redis遠程連接不上)的方法
默認情況下,當我們在部署了redis服務之后,redis本身默認只允許本地訪問。Redis服務端只允許它所在服務器上的客戶端訪問,如果Redis服務端和Redis客戶端不在同一個機器上,就要進行配置。2022-12-12
redis 億級數(shù)據(jù)讀取的實現(xiàn)
本文主要介紹了redis 億級數(shù)據(jù)讀取的實現(xiàn),億級數(shù)據(jù)規(guī)模下實現(xiàn)高效的數(shù)據(jù)讀取成為了許多企業(yè)和開發(fā)者面臨的重大挑戰(zhàn),下面就來介紹一下,感興趣的可以了解一下2024-08-08

