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