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

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

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

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

總結(jié)
Redis分布式鎖憑借其優(yōu)異的性能和足夠的可靠性,已成為互聯(lián)網(wǎng)公司的首選方案。理解其實(shí)現(xiàn)原理和限制條件,能夠幫助我們?cè)诓煌瑯I(yè)務(wù)場(chǎng)景中做出合理的技術(shù)選型。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
redis中如何使用lua腳本讓你的靈活性提高5個(gè)逼格詳解
這篇文章主要給大家介紹了關(guān)于redis中如何使用lua腳本讓你的靈活性提高5個(gè)逼格的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-10-10
Redis?的內(nèi)存淘汰策略和過(guò)期刪除策略的區(qū)別
這篇文章主要介紹了Redis?的內(nèi)存淘汰策略和過(guò)期刪除策略的區(qū)別,Redis?是可以對(duì)?key?設(shè)置過(guò)期時(shí)間的,因此需要有相應(yīng)的機(jī)制將已過(guò)期的鍵值對(duì)刪除,而做這個(gè)工作的就是過(guò)期鍵值刪除策略2022-07-07
redis集合類型_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章給大家介紹了redis集合類型的常用方法,感興趣的朋友參考下吧2017-08-08
redis監(jiān)聽(tīng)key過(guò)期事件的詳細(xì)步驟
本文主要介紹了redis監(jiān)聽(tīng)key過(guò)期事件的詳細(xì)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
Redis使用SETNX命令實(shí)現(xiàn)分布式鎖
分布式鎖是一種用于在分布式系統(tǒng)中控制多個(gè)節(jié)點(diǎn)對(duì)共享資源進(jìn)行訪問(wèn)的機(jī)制,本文主要為大家詳細(xì)介紹了Redis如何使用SETNX命令實(shí)現(xiàn)分布式鎖,需要的可以參考下2025-01-01
Redis中有序集合的內(nèi)部實(shí)現(xiàn)方式的詳細(xì)介紹
本文主要介紹了Redis中有序集合的內(nèi)部實(shí)現(xiàn)方式的詳細(xì)介紹,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03

