Redis作為分布式鎖的使用詳解
分布式鎖是控制分布式系統(tǒng)或不同系統(tǒng)之間共同訪問共享資源的一種鎖實現(xiàn)。如果不同的系統(tǒng)或同一個系統(tǒng)的不同主機之間共享了某個資源時,往往通過互斥來防止彼此之間的干擾。
實現(xiàn)分布式鎖的方式有很多,可以通過各種中間件來進行分布式鎖的設計,包括Redis、Zookeeper等。
如下圖所示:

1、實現(xiàn)鎖的方法
如下圖所示鎖的流程:

1.1. setnx命令
屬于最簡單的鎖,不推薦生產(chǎn)使用。
SETNX toilet_1 "occupied" # 嘗試鎖門
- 如果返回1:成功
- 如果返回0:失敗
問題:如果客戶端崩,永遠被占著(死鎖)。
1.2. 帶過期時間的鎖
屬于對setnx命令的改進版:
SETNX toilet_1 "occupied" EXPIRE toilet_1 30 # 30秒后自動解鎖
仍然有問題:兩條命令不是原子的,可能在SETNX和EXPIRE之間崩潰。
1.3. 原子命令(推薦)
該命令可使用于生產(chǎn)級方案:
SET toilet_1 "user_123" NX EX 30 # 原子操作:鎖門+設置30秒自動開鎖
1.4. RedLock算法詳解
當需要更高可靠性時,Redis作者Antirez提出的分布式鎖算法:
1.實現(xiàn)原理
獲取當前毫秒級時間戳T1
依次向N個獨立的Redis實例申請鎖
計算獲取鎖總耗時 = 當前時間T2 - T1
- 必須小于鎖有效時間
- 必須獲得多數(shù)(N/2+1)節(jié)點認可
鎖實際有效時間 = 初始設置時間 - 獲取鎖總耗時。
代碼示例:
// RedissonRedLock.tryLock()的核心邏輯
while (waitTimeRemaining > 0) {
long start = System.nanoTime();
// 嘗試從多數(shù)節(jié)點獲取鎖
int acquiredLocks = tryAcquireMultipleLocks();
if (acquiredLocks >= majority) {
// 計算實際有效時間
long elapsed = System.nanoTime() - start;
long lockTime = leaseTime - TimeUnit.NANOSECONDS.toMillis(elapsed);
if (lockTime > 0) {
// 對所有成功節(jié)點設置統(tǒng)一過期時間
scheduleLockExpiration(lockTime);
return true;
}
// 超時則釋放已獲得的鎖
releaseAcquiredLocks();
}
// 等待隨機時間后重試
waitTimeRemaining -= calculateWaitTime();
}2.設計目的
- 當單個Redis節(jié)點宕機時,系統(tǒng)仍能正常工作
- 防止主從切換時的鎖失效(原主節(jié)點崩潰,從節(jié)點晉升但未同步鎖信息)
3.關鍵保障機制
- 時鐘同步:所有Redis節(jié)點必須時間同步(NTP)
- 過期時間補償:扣除鎖獲取耗時
- 多數(shù)派原則:至少(N/2 + 1)個節(jié)點確認
4.局限性
1. 仍然存在的競爭問題
2. 網(wǎng)絡分區(qū)問題
當發(fā)生網(wǎng)絡分區(qū)時:
- 客戶端可能無法感知部分節(jié)點狀態(tài)
- 可能出現(xiàn)多個客戶端同時認為自己持有鎖
3. 性能開銷
獲取多個鎖的延遲顯著高于單節(jié)點:
- 通常比單節(jié)點慢3-5倍
- 不適合高頻短時鎖場景
而對于RedLock的本質(zhì)作用確實主要是為了解決單點故障問題,而不是提升并發(fā)性能,并未徹底解決一致性,如果要解決一致性問題,需要結合防護令牌或分布式事務。
1.5. 防護令牌(Fencing Token)模式
當發(fā)生鎖競爭的時候,假設5節(jié)點RedLock:
- 客戶端A獲得節(jié)點1、2、3的鎖
- 客戶端B獲得節(jié)點3、4、5的鎖
此時:
- 兩個客戶端都認為自己獲得了鎖(都獲得3個節(jié)點)
- 實際發(fā)生了沖突(節(jié)點3被雙方認為屬于自己)
代碼示例:
// 獲取鎖時返回單調(diào)遞增的token
LockResult result = redLock.tryLockWithToken();
long token = result.getToken();
// 操作資源時驗證token
if (resource.getCurrentToken() < token) {
resource.write(data, token);
} else {
throw new ConcurrentModificationException();
}- 實際實現(xiàn)中會加入** fencing token(防護令牌)機制
- 每次鎖獲取附帶單調(diào)遞增的token
- 資源操作時需要驗證token順序。
1.6. 看門狗機制
在上述的章節(jié)了解到,防護令牌可以解決鎖競爭一致性的問題,那么如果在鎖執(zhí)行過程中,過期時間到期,而業(yè)務還沒執(zhí)行完,那么該怎么辦呢?

看門狗(Watchdog)機制是Redis分布式鎖中確保業(yè)務執(zhí)行期間鎖不會意外釋放的關鍵設計,尤其在Redisson等客戶端中廣泛使用。
當業(yè)務執(zhí)行時間超過鎖的初始過期時間時,防止其他客戶端提前獲取鎖導致數(shù)據(jù)競爭。
流程:
// 獲取鎖(默認30秒看門狗時間)
RLock lock = redisson.getLock("order_lock");
lock.lock(); // 內(nèi)部啟動看門狗線程
try {
// 執(zhí)行業(yè)務邏輯(可能超過30秒)
processOrder();
} finally {
lock.unlock(); // 釋放時會停止看門狗
}鎖獲取時:
public void lock() {
try {
lockInterruptibly();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void lockInterruptibly() throws InterruptedException {
// 嘗試獲取鎖,默認leaseTime=30秒
tryAcquireAsync(leaseTime, TimeUnit.MILLISECONDS).sync();
// 啟動看門狗線程
scheduleExpirationRenewal();
}看門狗線程:
protected void scheduleExpirationRenewal() {
Thread renewalThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 每10秒(leaseTime/3)續(xù)期一次
try {
Thread.sleep(leaseTime / 3);
// 通過Lua腳本續(xù)期
String script =
"if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then " +
"return redis.call('pexpire', KEYS[1], ARGV[1]); " +
"else return 0; end";
redis.eval(script,
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(Thread.currentThread().getId()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
renewalThread.start();
}參數(shù)和配置方式如下:

jpg jpg
2、使用場景
用一個電影院搶座的例子,通過Java代碼展示Redis分布式鎖的實際應用。這個場景非常貼近生活,容易理解分布式鎖的必要性。
假設有一個熱門電影場次,多個用戶同時在線選座,我們需要保證:
- 一個座位只能被一個用戶選中
- 用戶有10分鐘支付時間
- 超時未支付自動釋放座位
1、基礎配置
首先添加Redis和Redisson(Redis Java客戶端)依賴:
<!-- pom.xml -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.8</version>
</dependency>初始化Redis連接:
public class RedisLockDemo {
private static RedissonClient redisson;
static {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
redisson = Redisson.create(config);
}
}簡單實現(xiàn):選座鎖
1. 獲取座位鎖
public boolean lockSeat(String seatNumber, String userId) {
// 獲取分布式鎖對象
RLock lock = redisson.getLock("seat:" + seatNumber);
try {
// 嘗試加鎖,waitTime=0表示不等待,leaseTime=10分鐘自動解鎖
return lock.tryLock(0, 10, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}2. 釋放座位鎖
public void unlockSeat(String seatNumber, String userId) {
RLock lock = redisson.getLock("seat:" + seatNumber);
// 檢查是否還被當前線程持有
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}3. 完整選座流程
public boolean selectSeat(String seatNumber, String userId) {
if (!lockSeat(seatNumber, userId)) {
System.out.println(userId + " 搶座失敗,座位已被鎖定");
return false;
}
try {
System.out.println(userId + " 成功鎖定座位 " + seatNumber);
// 模擬用戶支付流程
boolean paid = mockPaymentProcess(userId);
if (paid) {
System.out.println(userId + " 支付成功,座位確認");
return true;
} else {
System.out.println(userId + " 支付超時,座位釋放");
return false;
}
} finally {
unlockSeat(seatNumber, userId);
}
}
private boolean mockPaymentProcess(String userId) {
// 模擬50%概率支付成功
try {
Thread.sleep(2000); // 模擬支付思考時間
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return new Random().nextBoolean();
}3、高級特性:鎖續(xù)期
當用戶支付時間可能超過10分鐘時,需要自動續(xù)期:
public boolean lockSeatWithRenewal(String seatNumber, String userId) {
RLock lock = redisson.getLock("seat:" + seatNumber);
try {
// 獲取鎖,并設置看門狗自動續(xù)期(默認30秒)
lock.lock();
// 啟動一個線程定期續(xù)期
new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(5000); // 每5秒續(xù)期一次
lock.expire(10, TimeUnit.MINUTES); // 續(xù)期10分鐘
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
return true;
} catch (Exception e) {
return false;
}
}4、測試用例
public static void main(String[] args) {
RedisLockDemo demo = new RedisLockDemo();
// 模擬3個用戶同時搶5號座位
new Thread(() -> demo.selectSeat("A05", "用戶1")).start();
new Thread(() -> demo.selectSeat("A05", "用戶2")).start();
new Thread(() -> demo.selectSeat("A05", "用戶3")).start();
}輸出可能結果:
用戶1 成功鎖定座位 A05用戶2 搶座失敗,座位已被鎖定用戶3 搶座失敗,座位已被鎖定用戶1 支付成功,座位確認
5、關鍵點解析
- 鎖的Key設計:
seat:A05明確表示對A05座位的鎖 - 唯一標識:雖然沒有直接使用userId作為value,但Redisson內(nèi)部會維護線程與鎖的關系
- 自動釋放:即使程序崩潰,10分鐘后鎖也會自動釋放
- 可重入性:同一個線程可以多次獲取同一把鎖(Redisson特性)
6、對比生活場景
| 技術概念 | 電影院例子 |
|---|---|
| Redis服務器 | 電影院售票系統(tǒng) |
| 分布式鎖 | 座位鎖定狀態(tài) |
| 鎖的Key | 座位號(如A05) |
| 鎖的Value | 售票員記錄的本子(誰鎖的) |
| 鎖過期時間 | "保留座位10分鐘"的告示牌 |
| 獲取鎖失敗 | 看到座位已經(jīng)被標記"已預訂" |
| 鎖續(xù)期 | 顧客請求延長保留時間 |
這個例子展示了:
- 如何用Redis解決現(xiàn)實中的資源競爭問題
- Java中實際使用Redis分布式鎖的代碼寫法
- 處理鎖超時、續(xù)期等常見場景的方法
通過電影院選座這種熟悉的場景,應該能更直觀地理解Redis分布式鎖的工作機制了。實際開發(fā)中,使用Redisson等成熟客戶端可以避免很多邊界條件的處理。
3、Redis分布式鎖的局限性
時鐘漂移問題:
- 依賴系統(tǒng)時鐘,多節(jié)點時鐘不同步可能影響RedLock
持久化延遲:
- 異步復制可能導致主節(jié)點崩潰后從節(jié)點丟失鎖信息
長時間阻塞:
- 獲取不到鎖的客戶端需要合理處理等待/超時
4、對比

總結
Redis分布式鎖憑借其優(yōu)異的性能和足夠的可靠性,已成為互聯(lián)網(wǎng)公司的首選方案。理解其實現(xiàn)原理和限制條件,能夠幫助我們在不同業(yè)務場景中做出合理的技術選型。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
redis事務執(zhí)行常用命令及watch監(jiān)視詳解
這篇文章主要為大家介紹了redis事務執(zhí)行常用命令及watch監(jiān)視詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11

