redis實(shí)現(xiàn)紅鎖的示例代碼
舉個(gè)真實(shí)的例子:你的團(tuán)隊(duì)剛上線了一個(gè)秒殺系統(tǒng),用Redis鎖來防止超賣。測(cè)試環(huán)境明明跑得好好的,但大促當(dāng)晚卻出現(xiàn)了100件庫(kù)存賣出了120單的靈異事件。查看日志才發(fā)現(xiàn):就在用戶瘋狂點(diǎn)擊的瞬間,Redis主節(jié)點(diǎn)突然掛了,新的主節(jié)點(diǎn)還沒拿到鎖的信息,結(jié)果兩個(gè)用戶同時(shí)搶到了"同一把鎖"。
這就是很多開發(fā)者踩過的坑——你以為用了Redis分布式鎖就萬事大吉,其實(shí)這些情況隨時(shí)可能讓鎖失效:
- 主節(jié)點(diǎn)剛給你加完鎖就崩潰了,從節(jié)點(diǎn)接班時(shí)一臉懵:“什么鎖?我沒聽說過啊”
- 你的程序正處理到一半突然卡住了(比如GC停頓),等回過神來鎖早就過期了
- 網(wǎng)絡(luò)抽風(fēng)導(dǎo)致鎖信息沒傳到位,多個(gè)客戶端都覺得自己拿到了鎖
為了解決這些頭疼問題,Redis作者提出了**紅鎖(RedLock)**方案。簡(jiǎn)單來說就是"不要把雞蛋放在一個(gè)籃子里":讓多個(gè)獨(dú)立的Redis節(jié)點(diǎn)投票決定鎖的歸屬,只有半數(shù)以上同意才算真正拿到鎖。
但這套方案也引發(fā)過激烈爭(zhēng)論,有人甚至說它"數(shù)學(xué)上就不安全"。本文將用最直白的語(yǔ)言:
- 先帶你看看傳統(tǒng)Redis鎖在集群環(huán)境為什么容易翻車
- 拆解紅鎖這個(gè)"少數(shù)服從多數(shù)"的解決方案
- 手把手教你用Java代碼實(shí)現(xiàn)紅鎖
- 揭秘Redisson框架如何簡(jiǎn)化紅鎖的使用
讀完本文你會(huì)明白:沒有完美的分布式鎖,只有適合場(chǎng)景的選擇。下次設(shè)計(jì)系統(tǒng)時(shí),至少能清楚知道手里的鎖到底有幾成把握。
集群鎖的缺陷與挑戰(zhàn)
在Redis Cluster環(huán)境中,傳統(tǒng)的SETNX分布式鎖存在以下致命缺陷:主從切換導(dǎo)致鎖失效。
問題步驟復(fù)現(xiàn):
客戶端A通過
SET key random_val NX PX 30000在主節(jié)點(diǎn)成功獲取鎖主節(jié)點(diǎn)宕機(jī),Redis Cluster觸發(fā)故障轉(zhuǎn)移,從節(jié)點(diǎn)升級(jí)為新主節(jié)點(diǎn)
由于Redis主從復(fù)制是異步的,鎖可能未同步到新主節(jié)點(diǎn)
客戶端B向新主節(jié)點(diǎn)申請(qǐng)相同資源的鎖,成功獲取導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)
# 主節(jié)點(diǎn)寫入鎖 SET resource_1 8a3e72 NX PX 10000 OK # 主節(jié)點(diǎn)宕機(jī),從節(jié)點(diǎn)晉升但未同步鎖數(shù)據(jù) # 新主節(jié)點(diǎn)處理客戶端B的請(qǐng)求 SET resource_1 5b9fd2 NX PX 10000 OK # 鎖被重復(fù)獲取!
紅鎖(RedLock)的設(shè)計(jì)與實(shí)現(xiàn)
在N個(gè)獨(dú)立Redis節(jié)點(diǎn)(非Cluster模式)中,當(dāng)客戶端在半數(shù)以上節(jié)點(diǎn)成功獲取鎖,且總耗時(shí)小于鎖有效期時(shí),才認(rèn)為鎖獲取成功。
實(shí)現(xiàn)步驟詳解
假設(shè)部署5個(gè)Redis節(jié)點(diǎn)(N=5):
獲取當(dāng)前時(shí)間:記錄開始時(shí)間
T1(毫秒精度)依次向所有節(jié)點(diǎn)申請(qǐng)鎖:
SET lock_key valueNX PX $ttl
value:全局唯一值(如UUID)ttl:鎖自動(dòng)釋放時(shí)間(如10秒)
- 計(jì)算鎖有效性:
客戶端計(jì)算獲取鎖總耗時(shí)
T_elapsed = T2 - T1(T2為最后響應(yīng)時(shí)間)僅當(dāng)以下兩個(gè)條件滿足時(shí),鎖才有效:
成功獲取鎖的節(jié)點(diǎn)數(shù) ≥ 3(N/2 + 1)
T_elapsed < ttl(確保鎖未過期)
加鎖成功,去操作共享資源
釋放鎖:向所有節(jié)點(diǎn)發(fā)送Lua腳本刪除鎖(需驗(yàn)證值)
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
NPC爭(zhēng)議問題
紅鎖算法自誕生起就伴隨著**N(網(wǎng)絡(luò)延遲)、P(進(jìn)程暫停)、C(時(shí)鐘漂移)**三個(gè)核心爭(zhēng)議,這些現(xiàn)實(shí)世界中的不確定因素,動(dòng)搖了紅鎖在數(shù)學(xué)意義上的絕對(duì)安全性。
網(wǎng)絡(luò)延遲(Network Delay)的致命時(shí)間差
問題場(chǎng)景:
客戶端在節(jié)點(diǎn)A、B、C成功獲取鎖,總耗時(shí)48ms(小于TTL 50ms)
但由于跨機(jī)房網(wǎng)絡(luò)波動(dòng),實(shí)際鎖在節(jié)點(diǎn)上的有效時(shí)間存在差異:
- 節(jié)點(diǎn)A記錄的鎖過期時(shí)間:客戶端本地時(shí)間+50ms = T+50
- 節(jié)點(diǎn)B因網(wǎng)絡(luò)延遲,實(shí)際鎖過期時(shí)間為T+52
- 節(jié)點(diǎn)C因網(wǎng)絡(luò)擁塞,實(shí)際鎖過期時(shí)間僅T+48
在時(shí)間窗口
T+48到T+50之間,客戶端認(rèn)為鎖仍有效,但節(jié)點(diǎn)C的鎖已提前失效
后果:
其他客戶端可能在此期間獲取節(jié)點(diǎn)C的鎖,導(dǎo)致鎖狀態(tài)分裂,多個(gè)客戶端同時(shí)進(jìn)入臨界區(qū)。
進(jìn)程暫停(Process Pause)的「薛定諤鎖」
經(jīng)典案例:
// 偽代碼:獲取鎖后執(zhí)行業(yè)務(wù)邏輯
if (redLock.tryLock()) {
// 觸發(fā)Full GC暫停300ms
System.gc();
// 此時(shí)鎖已過期,但客戶端仍在寫數(shù)據(jù)
updateInventory();
}
關(guān)鍵時(shí)間線:
- T0: 獲取鎖(TTL=200ms)
- T0+100ms: 進(jìn)入GC暫停,持續(xù)300ms
- T0+400ms: GC結(jié)束,繼續(xù)執(zhí)行業(yè)務(wù)邏輯
- 鎖實(shí)際在T0+200ms已失效,但客戶端在T0+400ms仍以為自己持有鎖
數(shù)據(jù)災(zāi)難:
其他客戶端在T0+200ms到T0+400ms期間可能修改數(shù)據(jù),導(dǎo)致最終結(jié)果錯(cuò)亂。
時(shí)鐘漂移(Clock Drift)的時(shí)空扭曲
物理機(jī)時(shí)鐘偏移實(shí)驗(yàn)數(shù)據(jù):
| 節(jié)點(diǎn) | 時(shí)鐘誤差范圍 | 常見誘因 |
|---|---|---|
| 節(jié)點(diǎn)A | ±200ms/分鐘 | 虛擬機(jī)時(shí)鐘不同步 |
| 節(jié)點(diǎn)B | ±500ms/天 | NTP服務(wù)異常 |
| 節(jié)點(diǎn)C | ±10秒/小時(shí) | 宿主機(jī)硬件時(shí)鐘故障 |
連鎖反應(yīng):
- 客戶端計(jì)算鎖有效期基于本地時(shí)鐘(假設(shè)為T+100ms)
- 但節(jié)點(diǎn)B的時(shí)鐘比實(shí)際快30秒,導(dǎo)致其記錄的鎖過期時(shí)間為T-29000ms
- 鎖在客戶端認(rèn)為的有效期內(nèi)提前被節(jié)點(diǎn)B自動(dòng)釋放
行業(yè)領(lǐng)袖的正面交鋒
Martin Kleppmann(《數(shù)據(jù)密集型應(yīng)用設(shè)計(jì)》作者):
“紅鎖依賴的假設(shè)——『客戶端能準(zhǔn)確感知鎖存活時(shí)間』,在異步分布式系統(tǒng)中根本無法保證。即使沒有節(jié)點(diǎn)故障,NPC問題也會(huì)導(dǎo)致鎖狀態(tài)的不確定性。”
Antirez(Redis作者)的反駁:
"工程實(shí)踐中可以通過以下手段控制風(fēng)險(xiǎn):
使用帶溫度補(bǔ)償?shù)脑隅娪布?/p>
禁用NTP服務(wù)的時(shí)鐘跳變調(diào)整
監(jiān)控進(jìn)程暫停(如GC日志分析)
為鎖TTL設(shè)置冗余緩沖時(shí)間(如額外20%)"
紅鎖的Java實(shí)現(xiàn)示例
使用Jedis客戶端實(shí)現(xiàn)紅鎖:
package com.morris.redis.demo.redlock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 使用jedis手寫RedLock
*/
public class JedisRedLock {
public static final int EXPIRE_TIME = 30_000;
private final List<JedisPool> jedisPoolList;
private final String lockKey;
private final String lockValue;
public JedisRedLock(List<JedisPool> jedisPoolList, String lockKey) {
this.jedisPoolList = jedisPoolList;
this.lockKey = lockKey;
this.lockValue = UUID.randomUUID().toString();
}
public void lock() {
while (!tryLock()) {
try {
TimeUnit.MILLISECONDS.sleep(100); // 失敗后短暫等待
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public boolean tryLock() {
long startTime = System.currentTimeMillis();
int successCount = 0;
try {
for (JedisPool jedisPool : jedisPoolList) {
try (Jedis jedis = jedisPool.getResource();) {
// 原子化加鎖:SET lockKey UUID NX PX expireTime
String result = jedis.set(lockKey, lockValue,
SetParams.setParams().nx().px(EXPIRE_TIME));
if ("OK".equals(result)) {
successCount++;
}
}
}
// 計(jì)算獲取鎖耗時(shí)
long elapsedTime = System.currentTimeMillis() - startTime;
// 驗(yàn)證:多數(shù)節(jié)點(diǎn)成功 且 耗時(shí)小于TTL
return successCount >= (jedisPoolList.size() / 2 + 1) && elapsedTime < EXPIRE_TIME;
} finally {
// 若加鎖失敗,立即釋放已獲得的鎖
if (successCount < (jedisPoolList.size() / 2 + 1)) {
unlock();
}
}
}
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
for (JedisPool jedisPool : jedisPoolList) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
}
}
}
}
手寫RedLock的使用:
package com.morris.redis.demo.redlock;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 手寫RedLock的使用
*/
public class JedisRedLockDemo {
private volatile static int count;
public static void main(String[] args) throws InterruptedException {
List<JedisPool> jedisPoolList = new ArrayList<>();
jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6379));
jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6380));
jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6381));
jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6382));
jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6383));
int threadCount = 3;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
JedisRedLock jedisRedLock = new JedisRedLock(jedisPoolList, "lock-key");
jedisRedLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "獲得鎖,開始執(zhí)行業(yè)務(wù)邏輯。。。");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "獲得鎖,結(jié)束執(zhí)行業(yè)務(wù)邏輯。。。");
count++;
} finally {
jedisRedLock.unlock();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(count);
}
}
Redisson中紅鎖的使用
Redisson已封裝紅鎖實(shí)現(xiàn),自動(dòng)處理節(jié)點(diǎn)通信與鎖續(xù)期:
package com.morris.redis.demo.redlock;
import org.redisson.Redisson;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Redisson中紅鎖的使用
*/
public class RedissonRedLockDemo {
private volatile static int count;
public static void main(String[] args) throws InterruptedException {
List<String> serverList = Arrays.asList("redis://127.0.0.1:6379", "redis://127.0.0.1:6380", "redis://127.0.0.1:6381",
"redis://127.0.0.1:6382", "redis://127.0.0.1:6383");
List<RedissonClient> redissonClientList = new ArrayList<>(serverList.size());
for (String server : serverList) {
Config config = new Config();
config.useSingleServer()
.setAddress(server);
redissonClientList.add(Redisson.create(config));
}
List<RLock> lockList = new ArrayList<>(redissonClientList.size());
for (RedissonClient redissonClient : redissonClientList) {
lockList.add(redissonClient.getLock("java-lock"));
}
int threadCount = 3;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
RedissonRedLock redissonRedLock = new RedissonRedLock(lockList.toArray(new RLock[0]));
redissonRedLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "獲得鎖,開始執(zhí)行業(yè)務(wù)邏輯。。。");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "獲得鎖,結(jié)束執(zhí)行業(yè)務(wù)邏輯。。。");
count++;
} finally {
redissonRedLock.unlock();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(count);
for (RedissonClient redissonClient : redissonClientList) {
redissonClient.shutdown();
}
}
}
Redisson優(yōu)勢(shì):
- 自動(dòng)續(xù)期:通過WatchDog機(jī)制延長(zhǎng)鎖有效期
- 簡(jiǎn)化API:封裝底層細(xì)節(jié),支持異步/響應(yīng)式編程
- 故障容錯(cuò):自動(dòng)跳過宕機(jī)節(jié)點(diǎn),保證半數(shù)以上成功即可
總結(jié)
紅鎖通過多節(jié)點(diǎn)投票機(jī)制,顯著提升了分布式鎖的可靠性,但需權(quán)衡其實(shí)現(xiàn)復(fù)雜度與運(yùn)維成本。建議在以下場(chǎng)景選擇紅鎖:
- 需要跨機(jī)房/地域部署
- 業(yè)務(wù)對(duì)數(shù)據(jù)一致性要求極高
- 已具備獨(dú)立Redis節(jié)點(diǎn)運(yùn)維能力
對(duì)于大多數(shù)場(chǎng)景,可優(yōu)先使用Redisson等成熟框架,避免重復(fù)造輪子。若對(duì)一致性有極致要求,可考慮ZooKeeper/etcd等基于共識(shí)算法的方案。
到此這篇關(guān)于redis實(shí)現(xiàn)紅鎖的示例代碼的文章就介紹到這了,更多相關(guān)redis實(shí)現(xiàn)紅鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于redis樂觀鎖實(shí)現(xiàn)并發(fā)排隊(duì)
這篇文章主要介紹了基于redis樂觀鎖實(shí)現(xiàn)并發(fā)排隊(duì)的相關(guān)資料,需要的朋友可以參考下2022-12-12
redislive監(jiān)控redis服務(wù)的圖文教程_動(dòng)力節(jié)點(diǎn)Java 學(xué)院整理
這篇文章主要介紹了redislive監(jiān)控redis服務(wù)的圖文教程_動(dòng)力節(jié)點(diǎn)Java 學(xué)院整理,需要的朋友可以參考下2017-08-08
詳解redis數(shù)據(jù)結(jié)構(gòu)之sds
sds是Simple Dynamic String的縮寫,譯為簡(jiǎn)單動(dòng)態(tài)字符串,redis使用該結(jié)構(gòu)保存字符串,不同于c中的字符串,redis使用該結(jié)構(gòu)來更方便的進(jìn)行字符串的處理,需要的朋友可以參考下2017-05-05
生產(chǎn)redisson延時(shí)隊(duì)列不消費(fèi)問題排查解決
這篇文章主要為大家介紹了生產(chǎn)redisson延時(shí)隊(duì)列不消費(fèi)問題排查解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
Redis實(shí)現(xiàn)高并發(fā)計(jì)數(shù)器
這篇文章主要為大家詳細(xì)介紹了Redis實(shí)現(xiàn)高并發(fā)計(jì)數(shù)器,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10
Spring?Boot實(shí)戰(zhàn)解決高并發(fā)數(shù)據(jù)入庫(kù)之?Redis?緩存+MySQL?批量入庫(kù)問題
這篇文章主要介紹了Spring?Boot實(shí)戰(zhàn)解決高并發(fā)數(shù)據(jù)入庫(kù)之?Redis?緩存+MySQL?批量入庫(kù)問題,本文通過圖文實(shí)例相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02
redis?Template.opsForValue()中方法實(shí)例詳解
這篇文章主要介紹了redis?Template.opsForValue()中方法講解,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05

