最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Redisson分布式鎖原理深入分析

 更新時間:2026年04月26日 10:28:13   作者:風(fēng)吹迎面入袖涼  
文章介紹了Redisson分布式鎖的工作原理,并詳細解釋了加鎖、續(xù)期、解鎖的核心流程,Redisson利用Redis的單線程、自動續(xù)期和Lua腳本原子性等特點,避免了傳統(tǒng)鎖常見的死鎖、鎖爭用和誤刪等問題,此外,還總結(jié)了使用Redisson分布式鎖時的注意事項和避坑點

一. Redisson是什么?

先澄清一個誤區(qū):Redisson不是新的中間件,它就是一個操作Redis的Java工具(客戶端),基于Netty做的,速度很快。它的核心作用,就是把Redis里復(fù)雜的分布式鎖操作,封裝成了簡單的Java代碼——咱們不用記Redis的命令,不用寫復(fù)雜的邏輯,調(diào)用它的API就能實現(xiàn)分布式鎖,就像用本地的Lock一樣簡單。

Redisson里最核心的就是RLock這個接口,它和咱們本地用的Lock用法差不多,學(xué)起來特別簡單。但它背后的邏輯,比咱們想的要嚴謹,這也是它能避免很多坑的原因。

這里貼一段咱們實際開發(fā)中最常用的Redisson鎖代碼,先有個直觀感受:

// 1. 獲取Redisson客戶端(項目里一般是配置好的,直接注入)
RedissonClient redissonClient = Redisson.create();
// 2. 獲取一把分布式鎖(鎖的名字自己定義,比如“order:lock:123”,唯一就行)
RLock lock = redissonClient.getLock("order:lock:123");
try {
    // 3. 加鎖:默認30秒過期,也能自己設(shè)置,比如lock.lock(60, TimeUnit.SECONDS)
    lock.lock();
    // 4. 執(zhí)行業(yè)務(wù)邏輯(比如修改訂單狀態(tài)、扣減庫存,這部分是咱們自己的代碼)
    doBusiness();
} finally {
    // 5. 解鎖:必須放在finally里,防止業(yè)務(wù)報錯,鎖沒釋放
    lock.unlock();
}

就是這么簡單!一行l(wèi)ock()加鎖,一行unlock()解鎖,剩下的底層邏輯,Redisson全幫咱們搞定了。接下來,咱們就扒一扒這兩行代碼背后,Redisson到底做了什么。

二. Redisson鎖能干活,全靠Redis這3個本事

Redisson分布式鎖,本質(zhì)上是靠Redis實現(xiàn)的,沒有Redis,它也玩不轉(zhuǎn)。主要依賴Redis的3個核心能力:

  1. Redis是單線程干活:Redis同一時間只執(zhí)行一個命令,不會出現(xiàn)兩個命令同時執(zhí)行的情況。這就天然保證了,同一時刻只有一個線程能搶到鎖,不會出現(xiàn)“兩個人同時搶到鎖”的尷尬。
  2. 鎖能自動過期:給鎖設(shè)置一個過期時間(比如30秒),就算持有鎖的線程崩潰了、網(wǎng)絡(luò)斷了,過了這個時間,鎖會自動消失,不會一直占著資源,避免了“死鎖”(鎖一直沒人放,其他線程都搶不到)。
  3. 一堆命令能一次性執(zhí)行完:Redisson的加鎖、解鎖這些操作,都是用Lua腳本寫的。Lua腳本能把多個Redis命令打包,要么全部執(zhí)行成功,要么全部失敗,不會出現(xiàn)“執(zhí)行了一半卡住”的情況。比如“檢查鎖是否存在→創(chuàng)建鎖”,這兩步能一次性完成,避免了“兩個線程同時檢查到鎖不存在,同時創(chuàng)建鎖”的問題。

除此之外,Redisson還做了個優(yōu)化:把Lua腳本緩存起來,不用每次都傳輸完整腳本,能節(jié)省時間、提升速度,尤其是在多臺Redis組成的集群里,效果更明顯。

三. 核心流程:加鎖、續(xù)期、解鎖

Redisson分布式鎖的核心,就是“加鎖→續(xù)期→解鎖”這三步。

1. 加鎖:怎么保證只有一個線程能搶到鎖?

咱們平時調(diào)用的lock()方法,底層最終會調(diào)用RedissonLock類的lockInterruptibly()方法(核心加鎖方法),再往下走,會調(diào)用tryAcquire()方法,嘗試獲取鎖。這里貼tryAcquire()的核心源碼:

// 核心加鎖方法:嘗試獲取鎖,leaseTime是過期時間,unit是時間單位
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    // 1. 如果設(shè)置了過期時間,直接調(diào)用tryLockInnerAsync(真正執(zhí)行加鎖的方法)
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 2. 如果沒設(shè)置過期時間(用默認30秒),先嘗試加鎖
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    // 3. 加鎖成功后,啟動“看門狗”(續(xù)期用的),后面會講
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e == null) {
            if (ttlRemaining == null) {
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

// 真正執(zhí)行加鎖的方法:調(diào)用Lua腳本,和咱們之前講的Lua邏輯一致
private <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisCommand<T> command) {
    // 轉(zhuǎn)換過期時間為毫秒
    long ttl = unit.toMillis(leaseTime);
    // 返回Lua腳本的執(zhí)行結(jié)果,這里就是調(diào)用Redis執(zhí)行Lua腳本
    return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return nil; " +
            "end; " +
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return nil; " +
            "end; " +
            "return redis.call('pttl', KEYS[1]);",
            Collections.singletonList(getName()), ttl, getLockName(threadId));
}

逐行解讀源碼,不用懂復(fù)雜語法:

  • tryAcquireAsync方法:就是“嘗試獲取鎖”的核心,接收三個參數(shù)——過期時間(leaseTime)、時間單位(unit)、當(dāng)前線程ID(threadId)。

  • 第1步:如果咱們自己設(shè)置了過期時間(比如lock(60, TimeUnit.SECONDS)),就直接調(diào)用tryLockInnerAsync方法,真正去執(zhí)行加鎖。

  • 第2步:如果沒設(shè)置過期時間,就用Redisson默認的30秒過期時間(getLockWatchdogTimeout()就是默認30秒),先嘗試加鎖。

  • 第3步:加鎖成功后,啟動“看門狗”(scheduleExpirationRenewal方法),作用是“續(xù)期”——防止業(yè)務(wù)沒執(zhí)行完,鎖就過期了,后面會詳細講。

  • tryLockInnerAsync方法:真正執(zhí)行加鎖的邏輯,本質(zhì)就是調(diào)用Redis執(zhí)行咱們之前講的Lua腳本,參數(shù)對應(yīng)關(guān)系很簡單: - KEYS[1]:鎖的名字(getName()獲取,就是咱們自己定義的“order:lock:123”); - ARGV[1]:過期時間(ttl,轉(zhuǎn)換為毫秒); - ARGV[2]:線程身份證(getLockName(threadId),就是UUID+線程ID,比如“abc123:456”)。

這段源碼其實就是把咱們之前講的“加鎖邏輯”,用Java代碼實現(xiàn)了一遍,核心還是Lua腳本的原子性,保證只有一個線程能搶到鎖。這里再強調(diào)3個關(guān)鍵設(shè)計,徹底解決咱們自己寫鎖的坑:

  • 不會搶亂:因為Lua腳本是一次性執(zhí)行完的,不會出現(xiàn)“你檢查鎖不存在,正要創(chuàng)建,別人先創(chuàng)建了”的情況,保證了只有一個人能搶到鎖。

  • 自己能多次搶鎖(可重入):比如你的線程搶到鎖后,又需要調(diào)用另一個需要同一把鎖的方法,這時候不用等自己釋放鎖,直接再搶一次就行,計數(shù)會加1;解鎖的時候,計數(shù)減1,直到計數(shù)為0,才真正把鎖刪掉——和本地鎖的邏輯一樣,很靈活。

  • 不會刪別人的鎖:每個線程都有自己的“身份證”(UUID+線程ID),只有持有鎖的線程,才能操作這把鎖,別人就算想刪,也刪不了,避免了誤刪別人鎖的問題。

2. 續(xù)期:看門狗機制

很多人會有疑問:如果我的業(yè)務(wù)邏輯比較復(fù)雜,執(zhí)行時間超過了鎖的過期時間(比如默認30秒),怎么辦?這時候鎖會自動過期,當(dāng)鎖一過期的時候,就給了其他重試獲取鎖的線程可乘之機,它們會搶到鎖執(zhí)行自己的操作,導(dǎo)致數(shù)據(jù)錯亂。

Redisson早就想到了這個問題,自帶了“看門狗”機制(Watch Dog),核心作用就是:只要線程還持有鎖,就會每隔一段時間(默認10秒),把鎖的過期時間刷新回30秒,直到線程釋放鎖。

核心源碼:

// 啟動看門狗,續(xù)期用的
private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    // 把當(dāng)前線程的續(xù)期信息,存到本地緩存里
    EXPIRATION_RENEWAL_MAP.put(getEntryName(), entry);
    // 啟動一個定時任務(wù),每隔10秒執(zhí)行一次,刷新鎖的過期時間
    entry.task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            // 調(diào)用續(xù)期的Lua腳本,把鎖的過期時間刷新回30秒
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    // 續(xù)期失敗,移除本地緩存,停止續(xù)期
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }
                if (res) {
                    // 續(xù)期成功,繼續(xù)啟動下一個定時任務(wù),循環(huán)續(xù)期
                    scheduleExpirationRenewal(threadId);
                } else {
                    // 續(xù)期失敗,移除本地緩存
                    cancelExpirationRenewal(threadId);
                }
            });
        }
    }, getLockWatchdogTimeout() / 3, TimeUnit.MILLISECONDS);
}

// 續(xù)期的核心方法:調(diào)用Lua腳本,刷新過期時間
private RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return 1; " +
            "end; " +
            "return 0;",
            Collections.singletonList(getName()), getLockWatchdogTimeout(), getLockName(threadId));
}

大白話解讀看門狗機制:

  • 當(dāng)咱們調(diào)用lock()方法(不設(shè)置過期時間)時,Redisson會自動啟動看門狗,也就是scheduleExpirationRenewal方法。

  • 看門狗會啟動一個定時任務(wù),每隔10秒(默認30秒/3)執(zhí)行一次,調(diào)用renewExpirationAsync方法,執(zhí)行續(xù)期的Lua腳本。

  • 續(xù)期的Lua腳本邏輯很簡單:檢查當(dāng)前鎖是不是當(dāng)前線程持有,如果是,就把鎖的過期時間刷新回30秒,返回1(續(xù)期成功);如果不是,返回0(續(xù)期失敗)。

  • 只要續(xù)期成功,就會繼續(xù)啟動下一個定時任務(wù),循環(huán)續(xù)期;如果續(xù)期失?。ū热珂i已經(jīng)被釋放了),就停止續(xù)期,移除本地緩存。

  • 注意:如果咱們自己設(shè)置了過期時間(比如lock(60, TimeUnit.SECONDS)),Redisson不會啟動看門狗,鎖到期后會自動釋放——因為你已經(jīng)明確指定了鎖的存活時間,Redisson默認你能把控業(yè)務(wù)執(zhí)行時間。

3. 解鎖 :怎么正確釋放鎖?

解鎖的邏輯和加鎖對應(yīng),核心是“只有持有鎖的線程,才能釋放鎖”,而且要處理“可重入”的情況(計數(shù)減1,直到為0才刪除鎖)。咱們平時調(diào)用的unlock()方法,底層調(diào)用的是RedissonLock類的unlockAsync()方法:

// 核心解鎖方法
public RFuture<Void> unlockAsync(long threadId) {
    // 調(diào)用解鎖的Lua腳本,返回解鎖結(jié)果
    RFuture<Boolean> future = unlockInnerAsync(threadId);
    future.onComplete((opStatus, e) -> {
        // 解鎖成功后,停止看門狗續(xù)期
        cancelExpirationRenewal(threadId);
        if (e != null) {
            throw new CompletionException(e);
        }
        // 如果返回null,說明解鎖失敗(不是鎖的持有者)
        if (opStatus == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + getNodeId() + " thread-id: " + threadId);
        }
    });
    return future;
}

// 真正執(zhí)行解鎖的方法:調(diào)用Lua腳本
private RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then " +
                "return nil; " +  // 不是鎖的持有者,返回null,解鎖失敗
            "end; " +
            "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +  // 重入計數(shù)減1
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +  // 計數(shù)還大于0,刷新過期時間
                "return 1; " +  // 解鎖成功(只是計數(shù)減1,沒刪除鎖)
            "else " +
                "redis.call('del', KEYS[1]); " +  // 計數(shù)為0,刪除鎖
                "return 1; " +  // 解鎖成功(刪除鎖)
            "end;",
            Collections.singletonList(getName()), getLockWatchdogTimeout(), getLockName(threadId));
}

解鎖源碼:

  • unlockAsync方法:核心是調(diào)用unlockInnerAsync方法,執(zhí)行解鎖的Lua腳本,然后停止看門狗續(xù)期(cancelExpirationRenewal方法)。

  • 解鎖的Lua腳本邏輯,分3步: 1. 先檢查當(dāng)前線程是不是鎖的持有者(hexists判斷),如果不是,返回null,解鎖失敗,拋出異常(比如你試圖解鎖別人的鎖); 2. 如果是持有者,就把重入計數(shù)減1(hincrby -1); 3. 如果計數(shù)減1后還大于0(說明線程還在重入,沒執(zhí)行完所有業(yè)務(wù)),就刷新鎖的過期時間,返回1(解鎖成功,但沒刪除鎖);如果計數(shù)為0(說明線程所有業(yè)務(wù)都執(zhí)行完了),就刪除鎖(del命令),返回1(解鎖成功,刪除鎖)。

  • 這里有個坑:一定要在finally里調(diào)用unlock()!如果業(yè)務(wù)邏輯報錯,沒執(zhí)行到unlock(),鎖就會一直被持有(雖然有看門狗續(xù)期,但線程崩潰后,看門狗也會停止,鎖會過期釋放,但會有延遲),可能導(dǎo)致其他線程一直搶不到鎖。

四. Redisson分布式鎖的避坑點

  • 必須在finally里解鎖:不管業(yè)務(wù)邏輯有沒有報錯,都要釋放鎖,避免鎖泄露(鎖一直被持有,其他線程搶不到)。

  • 不要混用普通鎖和紅鎖:如果用了紅鎖,就全程用紅鎖的API,不要和普通鎖混用,否則會導(dǎo)致鎖失效。

  • 設(shè)置合理的過期時間:如果自己設(shè)置過期時間,一定要比業(yè)務(wù)執(zhí)行時間長,避免業(yè)務(wù)沒執(zhí)行完,鎖就過期了;如果業(yè)務(wù)執(zhí)行時間不確定,就用默認的30秒,依賴看門狗續(xù)期。

  • 避免鎖的粒度太大:比如不要給整個“訂單模塊”加一把鎖,應(yīng)該給每個訂單加一把鎖(比如“order:lock:123”,123是訂單ID),這樣不同訂單的線程可以同時執(zhí)行,提升并發(fā)性能。

  • Redis集群要保證高可用:Redisson鎖依賴Redis,所以Redis集群一定要做好高可用(比如主從復(fù)制、哨兵模式),避免Redis掛了,整個分布式鎖失效。

五. 總結(jié)

Redisson分布式鎖的核心邏輯:基于Redis的單線程、過期機制和Lua腳本原子性,封裝了加鎖、續(xù)期、解鎖的邏輯,還提供了可重入、看門狗、紅鎖、公平鎖、讀寫鎖等實用功能,讓我們能“開箱即用”。

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • java使用elasticsearch分組進行聚合查詢過程解析

    java使用elasticsearch分組進行聚合查詢過程解析

    這篇文章主要介紹了java使用elasticsearch分組進行聚合查詢過程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-02-02
  • 詳解Spring Boot中MyBatis的使用方法

    詳解Spring Boot中MyBatis的使用方法

    mybatis初期使用比較麻煩,需要各種配置文件、實體類、dao層映射關(guān)聯(lián)、還有一大推其它配置。當(dāng)然mybatis也發(fā)現(xiàn)了這種弊端。下面通過本文給大家詳細介紹Spring Boot中MyBatis的使用方法,感興趣的朋友一起看看吧
    2017-07-07
  • Spring實現(xiàn)泛型注入的示例詳解

    Spring實現(xiàn)泛型注入的示例詳解

    Spring?4.0版本中更新了很多新功能,其中比較重要的一個就是對帶泛型的Bean進行依賴注入的支持。本文將通過實例詳細講講Spring如何實現(xiàn)泛型注入,需要的可以參考一下
    2022-07-07
  • IntelliJ IDEA 配置與插件全部遷移到其他盤徹底釋放C盤空間

    IntelliJ IDEA 配置與插件全部遷移到其他盤徹底釋放C盤空間

    這篇文章給大家介紹IntelliJ IDEA 配置與插件全部遷移到其他盤徹底釋放C盤空間,本文將詳細解釋這一問題的根源,并提供兩種實用的遷移方案,幫你把 IDEA 的所有用戶數(shù)據(jù)安全地移到其他盤,徹底釋放 C 盤空間,感興趣的朋友跟隨小編一起看看吧
    2026-03-03
  • Java面試必備之ArrayList陷阱解析

    Java面試必備之ArrayList陷阱解析

    昨天小楓接到了一個公司的面試電話,其中一道面試題覺得有點意思,在這里和大家一起分享下。面試題是ArrayList如何刪除指定元素。乍聽很簡單的問題,但是如果沒有實際踩過坑很容易掉進面試官的陷阱中,我們一起來分析下吧
    2022-02-02
  • redis客戶端Jedis使用小結(jié)

    redis客戶端Jedis使用小結(jié)

    Jedis是Redis的一款Java語言的開源客戶端連接工具,本文主要介紹了redis客戶端Jedis使用,具有一定的參考價值,感興趣的可以了解一下
    2023-11-11
  • java循環(huán)遍歷無規(guī)則json的方式:gson、fastjson、orgjson

    java循環(huán)遍歷無規(guī)則json的方式:gson、fastjson、orgjson

    這篇文章主要介紹了java循環(huán)遍歷無規(guī)則json的方式:gson、fastjson、orgjson,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • java數(shù)據(jù)結(jié)構(gòu)基礎(chǔ):稀疏數(shù)組

    java數(shù)據(jù)結(jié)構(gòu)基礎(chǔ):稀疏數(shù)組

    今天帶大家了解一下Java稀疏數(shù)組的相關(guān)知識,文中有非常詳細的介紹及代碼示例,對正在學(xué)習(xí)java的小伙伴們有很好地幫助,需要的朋友可以參考下
    2021-08-08
  • 詳細說一說Java序列化的幾種方式對比

    詳細說一說Java序列化的幾種方式對比

    在某種情況下需要考慮一些安全問題和數(shù)據(jù)對象的使用問題,這時候就可以用序列化的技術(shù)來進行數(shù)據(jù)存儲,這篇文章主要介紹了Java序列化幾種方式對比的相關(guān)資料,需要的朋友可以參考下
    2025-07-07
  • java基礎(chǔ)理論Stream的Filter與謂詞邏輯

    java基礎(chǔ)理論Stream的Filter與謂詞邏輯

    這篇文章主要為大家介紹了java基礎(chǔ)理論Stream的Filter與謂詞邏輯,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-03-03

最新評論

探索| 额济纳旗| 青海省| 彭泽县| 澜沧| 延长县| 贵定县| 随州市| 弋阳县| 鄄城县| 平湖市| 贺州市| 永州市| 兴安盟| 乡城县| 中宁县| 武汉市| 阿合奇县| 体育| 东乡县| 托里县| 夏邑县| 金寨县| 稻城县| 固安县| 岢岚县| 徐汇区| 耒阳市| 正宁县| 正阳县| 泰兴市| 桦川县| 嘉定区| 武强县| 阿巴嘎旗| 普陀区| 平潭县| 库车县| 房山区| 广宁县| 宁强县|