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