Redis 分布式鎖的五個坑,真是又大又深!
兄弟們~ 最近是不是又在搞分布式系統(tǒng)?不管是秒殺、庫存扣減,還是訂單防重復(fù)提交,只要涉及 “多實例搶同一資源”,十有八九會想到 Redis 分布式鎖。
畢竟 Redis 快啊、部署也方便,一行 setnx (現(xiàn)在是 set key value nx ex)就能搞個 “鎖” 出來,看起來門檻低得很。但我敢說,不少同學(xué)剛寫完代碼還沒來得及喝口咖啡,線上就炸了!要么死鎖了,要么鎖不住導(dǎo)致數(shù)據(jù)亂了,要么 debug 到半夜才發(fā)現(xiàn) “哦!原來這里踩坑了!”
今天就跟大家扒一扒 Redis 分布式鎖里那 5 個 “又大又深” 的坑,每個坑都給你講清楚 “怎么踩進去的”“為什么會炸”“怎么爬出來”,全程大白話,帶代碼,保證你看完直呼 “原來之前我栽在這了!”
坑 1:忘了加過期時間,鎖變成 “老賴” 占著茅坑不拉屎
這絕對是新手最常踩的第一個坑,沒有之一!
我見過不少同學(xué)寫分布式鎖,上來就這么搞:
// 偽代碼:加鎖(只做了nx,沒加過期)
Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent("order:lock:1001", "user123");
if (lockSuccess) {
    try {
        // 搞業(yè)務(wù):扣庫存、創(chuàng)建訂單
        doBusiness();
    } finally {
        // 解鎖
        redisTemplate.delete("order:lock:1001");
    }
}看起來沒毛病吧?加鎖、執(zhí)行業(yè)務(wù)、解鎖,邏輯很順。但問題就出在 “沒給鎖加過期時間” 上!你想?。喝绻?doBusiness() 執(zhí)行到一半,服務(wù)器突然宕機了、或者線程被 kill 了,那 finally 里的 delete 壓根沒機會執(zhí)行!這時候 Redis 里的 “order??1001” 這個 key 就永遠(yuǎn)躺在那了 —— 后面所有想搶這個鎖的請求,都會被 setIfAbsent 擋在外面,直接造成 “死鎖”!
我之前就遇到過一次:線上秒殺活動,剛開服 5 分鐘,就有用戶反饋 “下單按鈕點了沒反應(yīng)”。查日志發(fā)現(xiàn),大量請求卡在 “獲取分布式鎖” 那一步;再查 Redis,發(fā)現(xiàn)有個鎖 key 已經(jīng)存在 10 多分鐘了,對應(yīng)的服務(wù)實例早就因為內(nèi)存溢出掛了。最后只能手動刪 key 救急,那叫一個狼狽。
怎么爬坑?加過期時間!但別瞎加!
解決辦法很簡單:給鎖加個 “過期時間”,就算業(yè)務(wù)執(zhí)行崩了,Redis 也會自動刪掉鎖 key,避免死鎖。
現(xiàn)在 Redis 推薦用 set key value nx ex 過期時間 這個命令,因為它是 “原子操作”—— 要么加鎖成功且設(shè)置過期,要么失敗,不會出現(xiàn) “加鎖成功但過期沒設(shè)上” 的中間狀態(tài)(之前有人分開寫 setnx 再 expire,這兩步不是原子的,也會有坑)。
改成這樣就安全多了:
// 加鎖:key=訂單鎖,value=用戶標(biāo)識,nx=不存在才加鎖,ex=30秒過期
Boolean lockSuccess = redisTemplate.opsForValue()
        .setIfAbsent("order:lock:1001", "user123", 30, TimeUnit.SECONDS);
if (lockSuccess) {
    try {
        doBusiness(); // 業(yè)務(wù)邏輯,比如扣減庫存
    } finally {
        // 解鎖:后面會講這里還有坑,先這么寫著
        redisTemplate.delete("order:lock:1001");
    }
}這里要提醒一句:過期時間別瞎設(shè)!設(shè)短了不行(后面坑 2 會講),設(shè)太長也不行 —— 比如你設(shè)個 24 小時,萬一鎖沒正常釋放,那這個資源 24 小時內(nèi)都被鎖住,影響業(yè)務(wù)。一般建議根據(jù) “業(yè)務(wù)最大執(zhí)行時間” 來設(shè),比如你的 doBusiness() 最多跑 5 秒,那過期時間設(shè) 10-30 秒就夠了,留個緩沖。
坑 2:過期時間設(shè)短了,鎖 “提前跑路” 導(dǎo)致并發(fā)問題
剛解決了死鎖問題,又有同學(xué)踩進下一個坑:過期時間設(shè)太短,業(yè)務(wù)還沒執(zhí)行完,鎖就被 Redis 自動刪了!
舉個例子:你給鎖設(shè)了 5 秒過期,結(jié)果某次業(yè)務(wù)因為數(shù)據(jù)庫慢、或者調(diào)用的第三方接口卡了,doBusiness() 跑了 6 秒才結(jié)束。這時候?qū)擂瘟?—— 鎖在第 5 秒就被 Redis 刪了,而你的業(yè)務(wù)還在執(zhí)行;這時候另一個請求過來,發(fā)現(xiàn) “鎖沒了”,就直接加鎖成功,也開始執(zhí)行同樣的業(yè)務(wù)。
兩個請求同時操作同一資源,后果就是:庫存超賣、訂單重復(fù)創(chuàng)建、數(shù)據(jù)不一致…… 我之前見過最離譜的一次,某電商平臺因為這個問題,同一個訂單號被創(chuàng)建了 3 次,用戶收到 3 條發(fā)貨通知,客服電話被打爆。
怎么爬坑?給鎖 “續(xù)命”!或者用 “看門狗”
核心思路:讓鎖的過期時間 “跟著業(yè)務(wù)走”—— 只要業(yè)務(wù)還在執(zhí)行,就自動把鎖的過期時間延長,避免鎖提前失效。
有兩種常見方案:
方案 1:自己寫 “續(xù)命” 線程
在加鎖成功后,啟動一個后臺線程,每隔一段時間(比如過期時間的 1/3)就去檢查 “當(dāng)前鎖還是不是自己的”,如果是,就把過期時間重置為初始值。
比如你鎖過期時間是 30 秒,后臺線程每隔 10 秒檢查一次,只要業(yè)務(wù)沒結(jié)束,就執(zhí)行 expire "order??1001" 30,相當(dāng)于給鎖 “續(xù)杯”。
偽代碼大概長這樣:
Boolean lockSuccess = redisTemplate.opsForValue()
        .setIfAbsent("order:lock:1001", "user123", 30, TimeUnit.SECONDS);
if (lockSuccess) {
    // 啟動續(xù)命線程
    Thread renewThread = new Thread(() -> {
        while (業(yè)務(wù)還在執(zhí)行中) {
            // 檢查鎖是不是自己的(value等于user123)
            String currentValue = redisTemplate.opsForValue().get("order:lock:1001");
            if ("user123".equals(currentValue)) {
                // 續(xù)命:重置為30秒過期
                redisTemplate.expire("order:lock:1001", 30, TimeUnit.SECONDS);
            }
            // 每隔10秒檢查一次
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    });
    renewThread.start();
    try {
        doBusiness(); // 執(zhí)行業(yè)務(wù)
    } finally {
        // 業(yè)務(wù)結(jié)束,停止續(xù)命線程,刪除鎖
        renewThread.interrupt();
        redisTemplate.delete("order:lock:1001");
    }
}方案 2:用成熟框架的 “看門狗”
自己寫續(xù)命線程容易有 bug(比如線程沒停干凈、檢查邏輯有問題),其實像 Redisson 這種 Redis 客戶端,已經(jīng)內(nèi)置了 “看門狗” 機制。
只要你用 Redisson 創(chuàng)建分布式鎖,它就會自動啟動一個看門狗線程,每隔 30 秒(默認(rèn))就給鎖續(xù)一次期,直到業(yè)務(wù)執(zhí)行完、手動釋放鎖。代碼也特別簡單:
// 獲取Redisson客戶端(提前配置好)
RedissonClient redissonClient = Redisson.create(config);
// 獲取分布式鎖
RLock lock = redissonClient.getLock("order:lock:1001");
try {
    // 加鎖:默認(rèn)30秒過期,看門狗自動續(xù)命
    lock.lock();
    doBusiness(); // 執(zhí)行業(yè)務(wù)
} finally {
    // 解鎖
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}是不是省了很多事?所以建議大家生產(chǎn)環(huán)境別自己瞎寫,優(yōu)先用 Redisson 這種成熟的框架,坑都幫你填好了。
坑 3:解鎖不校驗所有者,把別人的鎖刪了
這個坑也很隱蔽,很多同學(xué)覺得 “加鎖、執(zhí)行業(yè)務(wù)、解鎖” 流程對了就行,沒考慮過 “解鎖時,鎖可能已經(jīng)不是自己的了”。
比如這樣的代碼:
// 加鎖:30秒過期
Boolean lockSuccess = redisTemplate.opsForValue()
        .setIfAbsent("order:lock:1001", "user123", 30, TimeUnit.SECONDS);
if (lockSuccess) {
    try {
        doBusiness(); // 假設(shè)這里執(zhí)行了35秒,鎖已經(jīng)過期被自動刪了
    } finally {
        // 直接解鎖,沒校驗是不是自己的鎖!
        redisTemplate.delete("order:lock:1001");
    }
}你品,你細(xì)品:業(yè)務(wù)執(zhí)行了 35 秒,超過了鎖的 30 秒過期時間,Redis 已經(jīng)把這個鎖刪了。這時候另一個用戶(比如 user456)過來,成功加了新的鎖;結(jié)果你這邊業(yè)務(wù)執(zhí)行完,直接把 user456 的鎖給刪了!接下來就亂了:user456 以為自己還拿著鎖,在執(zhí)行業(yè)務(wù);而其他請求又能加鎖了,多個請求同時操作,數(shù)據(jù)直接就亂了。
這種情況 debug 起來特別費勁,因為你會發(fā)現(xiàn) “鎖加了、也解了”,但就是有并發(fā)問題,直到你盯著日志看時間線,才會發(fā)現(xiàn) “哦!原來我刪了別人的鎖!”
怎么爬坑?解鎖前先校驗 “鎖是不是自己的”
核心原則:誰加的鎖,誰才能解。所以解鎖前,必須先檢查 “當(dāng)前鎖的 value 是不是自己加鎖時設(shè)的值”,只有是自己的,才能刪。
但這里有個關(guān)鍵點:“檢查 value” 和 “刪除鎖” 這兩步,必須是原子操作!不能分開寫(先 get 再 delete),因為中間可能有時間差,還是會出問題。
比如你這么寫,還是有坑:
// 錯誤示例:先get再delete,非原子操作
String currentValue = redisTemplate.opsForValue().get("order:lock:1001");
if ("user123".equals(currentValue)) {
    // 這里有時間差!可能剛判斷完,鎖就過期被別人加了
    redisTemplate.delete("order:lock:1001");
}那怎么保證原子性呢?答案是用 Lua 腳本。因為 Redis 執(zhí)行 Lua 腳本時,會把腳本里的所有命令當(dāng)作一個整體執(zhí)行,中間不會被其他請求打斷,完美保證原子性。
用 Lua 腳本解鎖
我們可以寫一個這樣的 Lua 腳本:先判斷鎖的 value 是不是自己的,如果是,就刪除;如果不是,就不做任何操作。
Lua 腳本內(nèi)容:
-- 第一個參數(shù)是鎖的key,第二個參數(shù)是自己的標(biāo)識(value)
if redis.call('get', KEYS[1]) == ARGV[1] then
    -- 是自己的鎖,刪除
    return redis.call('del', KEYS[1])
else
    -- 不是自己的鎖,不操作
    return 0
end然后在 Java 代碼里調(diào)用這個腳本:
// 加鎖
String lockKey = "order:lock:1001";
String lockValue = "user123"; // 用唯一標(biāo)識,比如UUID+線程ID,避免同一臺機器不同線程沖突
Boolean lockSuccess = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
if (lockSuccess) {
    try {
        doBusiness();
    } finally {
        // 調(diào)用Lua腳本解鎖
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 執(zhí)行腳本:KEYS傳lockKey,ARGV傳lockValue
        redisTemplate.execute(
                new DefaultRedisScript<>(luaScript, Integer.class),
                Collections.singletonList(lockKey),
                lockValue
        );
    }
}這樣就安全了:不管鎖有沒有過期,只有當(dāng) value 是自己的標(biāo)識時,才會刪除,絕不會刪別人的鎖。順便說一句,Redisson 也幫你處理了這個問題 —— 它的 unlock() 方法會自動校驗當(dāng)前線程是不是鎖的持有者,不是的話會拋異常,避免誤刪。所以用框架真的能少踩很多坑。
坑 4:主從切換時,鎖 “丟了”
前面講的坑,都是基于 “單機 Redis” 的場景。但生產(chǎn)環(huán)境為了高可用,Redis 一般都是主從架構(gòu)(主節(jié)點寫,從節(jié)點讀,主掛了從節(jié)點頂上)。
可主從架構(gòu)里,藏著一個更隱蔽的坑:主從同步延遲導(dǎo)致鎖丟失。
流程是這樣的:
- 你在主節(jié)點上成功加了鎖(set key value nx ex),但主節(jié)點還沒來得及把這個 “加鎖命令” 同步到從節(jié)點;
 - 突然主節(jié)點宕機了(比如硬件故障、網(wǎng)絡(luò)斷了);
 - Redis 的哨兵(Sentinel)發(fā)現(xiàn)主節(jié)點掛了,就把某個從節(jié)點升級成新的主節(jié)點;
 - 新的主節(jié)點上,壓根沒有你之前加的那個鎖(因為沒同步過來);
 - 其他請求過來,就能在新主節(jié)點上成功加鎖,導(dǎo)致多個請求同時執(zhí)行業(yè)務(wù)。
 
這個坑有多坑?它不是代碼的問題,是 Redis 主從架構(gòu)的特性導(dǎo)致的,你代碼寫得再完美,遇到主從切換也可能中招。我之前幫朋友排查過一個問題,就是因為主從切換丟了鎖,導(dǎo)致秒殺活動中出現(xiàn)了超賣,最后只能走退款流程,損失了不少用戶信任。
怎么爬坑?方案有 3 種,各有優(yōu)劣
方案 1:接受風(fēng)險,用 “主從 + 哨兵”,配合業(yè)務(wù)補償
這是最常用的方案,因為實現(xiàn)簡單,性能也沒問題。核心思路是:
- Redis 用主從 + 哨兵架構(gòu),保證高可用;
 - 承認(rèn) “主從切換時可能丟鎖”,但通過 “業(yè)務(wù)補償” 來解決后果(比如超賣了,用定時任務(wù)對賬,發(fā)現(xiàn)超賣就退款、發(fā)通知);
 - 配合前面講的 “加過期時間、校驗解鎖者”,把丟鎖的概率降到最低。
 
這種方案適合 “對數(shù)據(jù)一致性要求不是 100% 嚴(yán)格,能接受少量異常并通過補償解決” 的場景(比如電商秒殺、普通訂單),畢竟主從切換不是高頻事件,丟鎖的概率很低。
方案 2:用 Redis Cluster+Redlock(紅鎖)
Redis 作者提出了一個 “紅鎖”(Redlock)方案,專門解決主從切換丟鎖的問題。原理很簡單:
- 部署多個獨立的 Redis 節(jié)點(至少 3 個,奇數(shù)個),這些節(jié)點之間沒有主從關(guān)系,都是獨立的;
 - 加鎖時,要在超過半數(shù)的節(jié)點上成功加鎖(比如 3 個節(jié)點,至少 2 個成功),才算整體加鎖成功;
 - 解鎖時,要在所有節(jié)點上都刪除鎖。
 
這樣即使某個節(jié)點宕機了,只要還有超過半數(shù)的節(jié)點正常,鎖依然有效。Redisson 也實現(xiàn)了紅鎖,代碼大概這樣:
// 創(chuàng)建多個Redis節(jié)點的客戶端
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.1.101:6379");
RedissonClient client1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.1.102:6379");
RedissonClient client2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.1.103:6379");
RedissonClient client3 = Redisson.create(config3);
// 構(gòu)造紅鎖
RLock lock1 = client1.getLock("order:lock:1001");
RLock lock2 = client2.getLock("order:lock:1001");
RLock lock3 = client3.getLock("order:lock:1001");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
    // 加鎖:在超過半數(shù)節(jié)點(至少2個)加鎖成功,才算成功
    redLock.lock(30, TimeUnit.SECONDS);
    doBusiness();
} finally {
    redLock.unlock();
}但紅鎖也有爭議:比如網(wǎng)絡(luò)分區(qū)時可能出現(xiàn) “腦裂”,而且需要部署多個獨立 Redis 節(jié)點,運維成本高、性能也比單機差(要連多個節(jié)點)。所以紅鎖適合 “對數(shù)據(jù)一致性要求極高,能接受運維成本和性能損耗” 的場景(比如金融交易)。
方案 3:不用 Redis,換 ZooKeeper 分布式鎖
如果實在擔(dān)心 Redis 主從切換的問題,也可以換個技術(shù)棧 —— 用 ZooKeeper 實現(xiàn)分布式鎖。
ZooKeeper 的特性是 “強一致性”:數(shù)據(jù)寫入時,會同步到所有節(jié)點,只有所有節(jié)點都寫成功,才算成功。所以不存在 Redis 主從同步延遲的問題,鎖的可靠性更高。
但 ZooKeeper 也有缺點:性能比 Redis 差(畢竟要同步所有節(jié)點),而且部署和維護更復(fù)雜(需要集群,至少 3 個節(jié)點)。所以要不要換,得根據(jù)你的業(yè)務(wù)場景權(quán)衡。
坑 5:沒考慮 “可重入”,自己把自己鎖死了
最后一個坑,是 “可重入” 問題。所謂 “可重入”,就是同一個線程可以多次獲取同一把鎖,不會自己擋住自己。
比如這樣的場景:你的業(yè)務(wù)代碼里有個遞歸調(diào)用,或者一個方法調(diào)用另一個方法,兩個方法都需要獲取同一把鎖:
// 方法A需要加鎖
public void methodA() {
    Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent("lock", "value", 30, TimeUnit.SECONDS);
    if (lockSuccess) {
        try {
            System.out.println("進入方法A");
            methodB(); // 調(diào)用方法B,方法B也需要這把鎖
        } finally {
            // 解鎖(這里省略校驗,方便舉例)
            redisTemplate.delete("lock");
        }
    }
}
// 方法B也需要加鎖
public void methodB() {
    Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent("lock", "value", 30, TimeUnit.SECONDS);
    if (lockSuccess) {
        try {
            System.out.println("進入方法B");
        } finally {
            redisTemplate.delete("lock");
        }
    } else {
        System.out.println("方法B獲取鎖失敗,被擋住了!");
    }
}當(dāng)線程執(zhí)行 methodA 時,成功加了鎖;然后調(diào)用 methodB,這時候 setIfAbsent 發(fā)現(xiàn)鎖已經(jīng)存在了,就返回 false,方法 B 獲取鎖失敗 —— 這就是 “自己把自己鎖死了”。這種情況在復(fù)雜業(yè)務(wù)里很常見,比如訂單處理流程中,“創(chuàng)建訂單” 和 “扣減庫存” 都需要同一把鎖,如果你沒考慮可重入,就會出現(xiàn)這種尷尬的情況。
怎么爬坑?實現(xiàn) “可重入鎖”
要實現(xiàn)可重入鎖,核心是要記錄 “哪個線程持有鎖” 以及 “持有了多少次”,解鎖時次數(shù)減 1,直到次數(shù)為 0 才真正刪除鎖。
具體怎么實現(xiàn)呢?可以用 Redis 的 Hash 數(shù)據(jù)結(jié)構(gòu),把鎖的 key 作為 Hash 的 key,然后在 Hash 里存兩個字段:
- threadId:持有鎖的線程 ID(比如 “12345”);
 - count:重入次數(shù)(比如 1、2、3)。
 
加鎖和解鎖的邏輯如下:
加鎖邏輯(Lua 腳本實現(xiàn)原子性)
- 檢查 Hash 里的 threadId 是不是當(dāng)前線程 ID:
 
- 如果是,說明是重入,把 count 加 1,同時重置鎖的過期時間;
 - 如果不是,檢查 Hash 是否存在:不存在的話,創(chuàng)建 Hash,設(shè)置 threadId 為當(dāng)前線程,count=1,并設(shè)置過期時間;存在的話,加鎖失敗。
 
Lua 腳本:
-- KEYS[1] = 鎖的key,ARGV[1] = 線程ID,ARGV[2] = 過期時間(秒)
if redis.call('hexists', KEYS[1], 'threadId') == 1then
    -- 重入:count+1,重置過期時間
    redis.call('hincrby', KEYS[1], 'count', 1);
    redis.call('expire', KEYS[1], ARGV[2]);
    return1; -- 加鎖成功
else
    -- 不是當(dāng)前線程的鎖,檢查是否存在
    if redis.call('exists', KEYS[1]) == 0then
        -- 不存在,創(chuàng)建Hash,設(shè)置threadId和count
        redis.call('hset', KEYS[1], 'threadId', ARGV[1]);
        redis.call('hset', KEYS[1], 'count', 1);
        redis.call('expire', KEYS[1], ARGV[2]);
        return1; -- 加鎖成功
    else
        return0; -- 加鎖失敗
    end
end解鎖邏輯(Lua 腳本)
檢查 Hash 里的 threadId 是不是當(dāng)前線程 ID:
- 如果不是,直接返回 0(不是自己的鎖,不解鎖);
 - 如果是,把 count 減 1:如果減到 0,就刪除整個 Hash(釋放鎖);如果沒到 0,就重置過期時間。
 
Lua 腳本:
-- KEYS[1] = 鎖的key,ARGV[1] = 線程ID,ARGV[2] = 過期時間(秒)
if redis.call('hexists', KEYS[1], 'threadId') ~= 1then
    return0; -- 不是自己的鎖,不解鎖
end
-- count減1
local count = redis.call('hincrby', KEYS[1], 'count', -1);
if count == 0then
    -- count到0,刪除鎖
    redis.call('del', KEYS[1]);
    return1;
else
    -- count沒到0,重置過期時間
    redis.call('expire', KEYS[1], ARGV[2]);
    return1;
endJava 代碼調(diào)用
把上面的 Lua 腳本集成到 Java 代碼里,就能實現(xiàn)可重入鎖了:
// 加鎖方法
privateboolean reentrantLock(String lockKey, String threadId, int expireSeconds) {
    String luaScript = "if redis.call('hexists', KEYS[1], 'threadId') == 1 then redis.call('hincrby', KEYS[1], 'count', 1); redis.call('expire', KEYS[1], ARGV[2]); return 1; else if redis.call('exists', KEYS[1]) == 0 then redis.call('hset', KEYS[1], 'threadId', ARGV[1]); redis.call('hset', KEYS[1], 'count', 1); redis.call('expire', KEYS[1], ARGV[2]); return 1; else return 0; end";
    Integer result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Integer.class),
            Collections.singletonList(lockKey),
            threadId, String.valueOf(expireSeconds)
    );
    return result != null && result == 1;
}
// 解鎖方法
privateboolean reentrantUnlock(String lockKey, String threadId, int expireSeconds) {
    String luaScript = "if redis.call('hexists', KEYS[1], 'threadId') ~= 1 then return 0; end local count = redis.call('hincrby', KEYS[1], 'count', -1); if count == 0 then redis.call('del', KEYS[1]); return 1; else redis.call('expire', KEYS[1], ARGV[2]); return 1; end";
    Integer result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Integer.class),
            Collections.singletonList(lockKey),
            threadId, String.valueOf(expireSeconds)
    );
    return result != null && result == 1;
}
// 測試可重入
publicvoid testReentrant() {
    String lockKey = "reentrant:lock";
    String threadId = Thread.currentThread().getId() + ""; // 當(dāng)前線程ID
    int expireSeconds = 30;
    // 第一次加鎖
    if (reentrantLock(lockKey, threadId, expireSeconds)) {
        try {
            System.out.println("第一次加鎖成功");
            // 第二次加鎖(重入)
            if (reentrantLock(lockKey, threadId, expireSeconds)) {
                try {
                    System.out.println("第二次加鎖成功(重入)");
                } finally {
                    reentrantUnlock(lockKey, threadId, expireSeconds);
                    System.out.println("第二次解鎖");
                }
            }
        } finally {
            reentrantUnlock(lockKey, threadId, expireSeconds);
            System.out.println("第一次解鎖");
        }
    }
}運行這段代碼,會輸出:
第一次加鎖成功
第二次加鎖成功(重入)
第二次解鎖
第一次解鎖完美實現(xiàn)了可重入!當(dāng)然,如果你用 Redisson,它也內(nèi)置了可重入鎖,不用自己寫這么多代碼 ——RLock 本身就是可重入的,之前的例子里已經(jīng)體現(xiàn)了。
總結(jié):Redis 分布式鎖避坑指南
講完了 5 個坑,最后給大家總結(jié)一下避坑要點,方便你收藏備用:
坑 1:忘加過期時間→死鎖
避坑:加鎖時必須用 set key value nx ex 原子命令,設(shè)置合理過期時間(比業(yè)務(wù)最大執(zhí)行時間長)。
坑 2:過期時間太短→鎖提前釋放
避坑:用 “續(xù)命線程” 或 Redisson 看門狗,業(yè)務(wù)沒結(jié)束就自動續(xù)期。
坑 3:解鎖不校驗所有者→刪別人的鎖
避坑:解鎖前用 Lua 腳本校驗鎖的 value(比如線程 ID),原子性刪除。
坑 4:主從切換→鎖丟失
避坑:普通場景用 “主從 + 哨兵 + 業(yè)務(wù)補償”;高一致性場景用 Redlock 或 ZooKeeper。
坑 5:不支持可重入→自己鎖自己
避坑:用 Hash 結(jié)構(gòu)記錄線程 ID 和重入次數(shù),或直接用 Redisson 可重入鎖。
最后再啰嗦一句:Redis 分布式鎖不是 “銀彈”,沒有完美的方案,只有適合自己業(yè)務(wù)的方案。生產(chǎn)環(huán)境優(yōu)先用 Redisson 這種成熟框架,別自己造輪子,除非你對底層原理吃得很透,不然很容易踩坑。















 
 
 













 
 
 
 