redis實(shí)現(xiàn)紅鎖的示例代碼
舉個真實(shí)的例子:你的團(tuán)隊(duì)剛上線了一個秒殺系統(tǒng),用Redis鎖來防止超賣。測試環(huán)境明明跑得好好的,但大促當(dāng)晚卻出現(xiàn)了100件庫存賣出了120單的靈異事件。查看日志才發(fā)現(xiàn):就在用戶瘋狂點(diǎn)擊的瞬間,Redis主節(jié)點(diǎn)突然掛了,新的主節(jié)點(diǎn)還沒拿到鎖的信息,結(jié)果兩個用戶同時搶到了"同一把鎖"。
這就是很多開發(fā)者踩過的坑——你以為用了Redis分布式鎖就萬事大吉,其實(shí)這些情況隨時可能讓鎖失效:
- 主節(jié)點(diǎn)剛給你加完鎖就崩潰了,從節(jié)點(diǎn)接班時一臉懵:“什么鎖?我沒聽說過啊”
- 你的程序正處理到一半突然卡住了(比如GC停頓),等回過神來鎖早就過期了
- 網(wǎng)絡(luò)抽風(fēng)導(dǎo)致鎖信息沒傳到位,多個客戶端都覺得自己拿到了鎖
為了解決這些頭疼問題,Redis作者提出了**紅鎖(RedLock)**方案。簡單來說就是"不要把雞蛋放在一個籃子里":讓多個獨(dú)立的Redis節(jié)點(diǎn)投票決定鎖的歸屬,只有半數(shù)以上同意才算真正拿到鎖。
但這套方案也引發(fā)過激烈爭論,有人甚至說它"數(shù)學(xué)上就不安全"。本文將用最直白的語言:
- 先帶你看看傳統(tǒng)Redis鎖在集群環(huán)境為什么容易翻車
- 拆解紅鎖這個"少數(shù)服從多數(shù)"的解決方案
- 手把手教你用Java代碼實(shí)現(xiàn)紅鎖
- 揭秘Redisson框架如何簡化紅鎖的使用
讀完本文你會明白:沒有完美的分布式鎖,只有適合場景的選擇。下次設(shè)計(jì)系統(tǒng)時,至少能清楚知道手里的鎖到底有幾成把握。
集群鎖的缺陷與挑戰(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)升級為新主節(jié)點(diǎn)
由于Redis主從復(fù)制是異步的,鎖可能未同步到新主節(jié)點(diǎn)
客戶端B向新主節(jié)點(diǎn)申請相同資源的鎖,成功獲取導(dǎo)致數(shù)據(jù)競爭
# 主節(jié)點(diǎn)寫入鎖 SET resource_1 8a3e72 NX PX 10000 OK # 主節(jié)點(diǎn)宕機(jī),從節(jié)點(diǎn)晉升但未同步鎖數(shù)據(jù) # 新主節(jié)點(diǎn)處理客戶端B的請求 SET resource_1 5b9fd2 NX PX 10000 OK # 鎖被重復(fù)獲?。?
紅鎖(RedLock)的設(shè)計(jì)與實(shí)現(xiàn)
在N個獨(dú)立Redis節(jié)點(diǎn)(非Cluster模式)中,當(dāng)客戶端在半數(shù)以上節(jié)點(diǎn)成功獲取鎖,且總耗時小于鎖有效期時,才認(rèn)為鎖獲取成功。
實(shí)現(xiàn)步驟詳解
假設(shè)部署5個Redis節(jié)點(diǎn)(N=5):
獲取當(dāng)前時間:記錄開始時間
T1
(毫秒精度)依次向所有節(jié)點(diǎn)申請鎖:
SET lock_key valueNX PX $ttl
value
:全局唯一值(如UUID)ttl
:鎖自動釋放時間(如10秒)
- 計(jì)算鎖有效性:
客戶端計(jì)算獲取鎖總耗時
T_elapsed = T2 - T1
(T2為最后響應(yīng)時間)僅當(dāng)以下兩個條件滿足時,鎖才有效:
成功獲取鎖的節(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爭議問題
紅鎖算法自誕生起就伴隨著**N(網(wǎng)絡(luò)延遲)、P(進(jìn)程暫停)、C(時鐘漂移)**三個核心爭議,這些現(xiàn)實(shí)世界中的不確定因素,動搖了紅鎖在數(shù)學(xué)意義上的絕對安全性。
網(wǎng)絡(luò)延遲(Network Delay)的致命時間差
問題場景:
客戶端在節(jié)點(diǎn)A、B、C成功獲取鎖,總耗時48ms(小于TTL 50ms)
但由于跨機(jī)房網(wǎng)絡(luò)波動,實(shí)際鎖在節(jié)點(diǎn)上的有效時間存在差異:
- 節(jié)點(diǎn)A記錄的鎖過期時間:客戶端本地時間+50ms = T+50
- 節(jié)點(diǎn)B因網(wǎng)絡(luò)延遲,實(shí)際鎖過期時間為T+52
- 節(jié)點(diǎn)C因網(wǎng)絡(luò)擁塞,實(shí)際鎖過期時間僅T+48
在時間窗口
T+48
到T+50
之間,客戶端認(rèn)為鎖仍有效,但節(jié)點(diǎn)C的鎖已提前失效
后果:
其他客戶端可能在此期間獲取節(jié)點(diǎn)C的鎖,導(dǎo)致鎖狀態(tài)分裂,多個客戶端同時進(jìn)入臨界區(qū)。
進(jìn)程暫停(Process Pause)的「薛定諤鎖」
經(jīng)典案例:
// 偽代碼:獲取鎖后執(zhí)行業(yè)務(wù)邏輯 if (redLock.tryLock()) { // 觸發(fā)Full GC暫停300ms System.gc(); // 此時鎖已過期,但客戶端仍在寫數(shù)據(jù) updateInventory(); }
關(guān)鍵時間線:
- 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é)果錯亂。
時鐘漂移(Clock Drift)的時空扭曲
物理機(jī)時鐘偏移實(shí)驗(yàn)數(shù)據(jù):
節(jié)點(diǎn) | 時鐘誤差范圍 | 常見誘因 |
---|---|---|
節(jié)點(diǎn)A | ±200ms/分鐘 | 虛擬機(jī)時鐘不同步 |
節(jié)點(diǎn)B | ±500ms/天 | NTP服務(wù)異常 |
節(jié)點(diǎn)C | ±10秒/小時 | 宿主機(jī)硬件時鐘故障 |
連鎖反應(yīng):
- 客戶端計(jì)算鎖有效期基于本地時鐘(假設(shè)為T+100ms)
- 但節(jié)點(diǎn)B的時鐘比實(shí)際快30秒,導(dǎo)致其記錄的鎖過期時間為T-29000ms
- 鎖在客戶端認(rèn)為的有效期內(nèi)提前被節(jié)點(diǎn)B自動釋放
行業(yè)領(lǐng)袖的正面交鋒
Martin Kleppmann(《數(shù)據(jù)密集型應(yīng)用設(shè)計(jì)》作者):
“紅鎖依賴的假設(shè)——『客戶端能準(zhǔn)確感知鎖存活時間』,在異步分布式系統(tǒng)中根本無法保證。即使沒有節(jié)點(diǎn)故障,NPC問題也會導(dǎo)致鎖狀態(tài)的不確定性。”
Antirez(Redis作者)的反駁:
"工程實(shí)踐中可以通過以下手段控制風(fēng)險(xiǎn):
使用帶溫度補(bǔ)償?shù)脑隅娪布?/p>
禁用NTP服務(wù)的時鐘跳變調(diào)整
監(jiān)控進(jìn)程暫停(如GC日志分析)
為鎖TTL設(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ì)算獲取鎖耗時 long elapsedTime = System.currentTimeMillis() - startTime; // 驗(yàn)證:多數(shù)節(jié)點(diǎn)成功 且 耗時小于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),自動處理節(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)勢:
- 自動續(xù)期:通過WatchDog機(jī)制延長鎖有效期
- 簡化API:封裝底層細(xì)節(jié),支持異步/響應(yīng)式編程
- 故障容錯:自動跳過宕機(jī)節(jié)點(diǎn),保證半數(shù)以上成功即可
總結(jié)
紅鎖通過多節(jié)點(diǎn)投票機(jī)制,顯著提升了分布式鎖的可靠性,但需權(quán)衡其實(shí)現(xiàn)復(fù)雜度與運(yùn)維成本。建議在以下場景選擇紅鎖:
- 需要跨機(jī)房/地域部署
- 業(yè)務(wù)對數(shù)據(jù)一致性要求極高
- 已具備獨(dú)立Redis節(jié)點(diǎn)運(yùn)維能力
對于大多數(shù)場景,可優(yōu)先使用Redisson等成熟框架,避免重復(fù)造輪子。若對一致性有極致要求,可考慮ZooKeeper/etcd等基于共識算法的方案。
到此這篇關(guān)于redis實(shí)現(xiàn)紅鎖的示例代碼的文章就介紹到這了,更多相關(guān)redis實(shí)現(xiàn)紅鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
CentOS系統(tǒng)安裝Redis及Redis的PHP擴(kuò)展詳解
這篇文章主要介紹了CentOS系統(tǒng)下安裝Redis數(shù)據(jù)的教程,以及詳解了Redis數(shù)據(jù)庫的PHP擴(kuò)展,文中介紹的很詳細(xì),相信對大家的理解和學(xué)習(xí)具有一定的參考借鑒價值,有需要的朋友們可以參考借鑒,下面來一起看看吧。2016-12-12redis解決高并發(fā)看門狗策略的實(shí)現(xiàn)
本文主要介紹了redis解決高并發(fā)看門狗策略的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02Redis實(shí)現(xiàn)延遲任務(wù)的常見方案詳解
延遲任務(wù)(Delayed?Task)是指在未來的某個時間點(diǎn),執(zhí)行相應(yīng)的任務(wù),本文為大家整理了Redis實(shí)現(xiàn)延遲任務(wù)的幾個常見方案,希望對大家有所幫助2024-04-04Redis和Lua實(shí)現(xiàn)分布式限流器的方法詳解
這篇文章主要給大家介紹了關(guān)于Redis和Lua實(shí)現(xiàn)分布式限流器的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Redis和Lua具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06