Redisson分布式鎖使用即源碼解讀

Redisson 提供的分布式鎖

image.png

使用實例

private void redissonDoc() throws InterruptedException {
    //1. 普通的可重入鎖
    RLock lock = redissonClient.getLock("generalLock");

    // 拿鎖失敗時會不停的重試
    // 具有Watch Dog 自動延期機制 默認(rèn)續(xù)30s 每隔30/3=10 秒續(xù)到30s
    lock.lock();

    // 嘗試拿鎖10s后停止重試,返回false
    // 具有Watch Dog 自動延期機制 默認(rèn)續(xù)30s
    boolean res1 = lock.tryLock(10, TimeUnit.SECONDS);

    // 拿鎖失敗時會不停的重試
    // 沒有Watch Dog 废酷,10s后自動釋放
    lock.lock(10, TimeUnit.SECONDS);

    // 嘗試拿鎖100s后停止重試,返回false
    // 沒有Watch Dog 遂黍,10s后自動釋放
    boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);

    //2. 公平鎖 保證 Redisson 客戶端線程將以其請求的順序獲得鎖
    RLock fairLock = redissonClient.getFairLock("fairLock");

    //3. 讀寫鎖 沒錯與JDK中ReentrantLock的讀寫鎖效果一樣
    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");
    readWriteLock.readLock().lock();
    readWriteLock.writeLock().lock();
}

如果拿到分布式鎖的節(jié)點宕機,且這個鎖正好處于鎖住的狀態(tài)時安接,會出現(xiàn)鎖死的狀態(tài)翔忽,為了避免這種情況的發(fā)生,鎖都會設(shè)置一個過期時間盏檐。這樣也存在一個問題歇式,加入一個線程拿到了鎖設(shè)置了30s超時,在30s后這個線程還沒有執(zhí)行完畢糯笙,鎖超時釋放了贬丛,就會導(dǎo)致問題,Redisson給出了自己的答案给涕,就是 watch dog 自動延期機制。

Redisson提供了一個監(jiān)控鎖的看門狗额获,它的作用是在Redisson實例被關(guān)閉前够庙,不斷的延長鎖的有效期,也就是說抄邀,如果一個拿到鎖的線程一直沒有完成邏輯耘眨,那么看門狗會幫助線程不斷的延長鎖超時時間,鎖不會因為超時而被釋放境肾。

默認(rèn)情況下剔难,看門狗的續(xù)期時間是30s,也可以通過修改Config.lockWatchdogTimeout來另行指定奥喻。

另外Redisson 還提供了可以指定leaseTime參數(shù)的加鎖方法來指定加鎖的時間偶宫。超過這個時間后鎖便自動解開了,不會延長鎖的有效期

watch dog 核心源碼解讀

// 直接使用lock無參數(shù)方法
public void lock() {
    try {
        lock(-1, null, false);
    } catch (InterruptedException e) {
        throw new IllegalStateException();
    }
}

// 進入該方法 其中l(wèi)easeTime = -1
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return;
    }

   //...
}

// 進入 tryAcquire(-1, leaseTime, unit, threadId)
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}

// 進入 tryAcquireAsync
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    //當(dāng)leaseTime = -1 時 啟動 watch dog機制
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                            commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                            TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    //執(zhí)行完lua腳本后的回調(diào)
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }

        if (ttlRemaining == null) {
            // watch dog 
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

scheduleExpirationRenewal 方法開啟監(jiān)控

private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    //將線程放入緩存中
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    //第二次獲得鎖后 不會進行延期操作
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        
        // 第一次獲得鎖 延期操作
        renewExpiration();
    }
}

// 進入 renewExpiration()
private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    //如果緩存不存在环鲤,那不再鎖續(xù)期
    if (ee == null) {
        return;
    }
    
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            
            //執(zhí)行l(wèi)ua 進行續(xù)期
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                if (res) {
                    //延期成功纯趋,繼續(xù)循環(huán)操作
                    renewExpiration();
                }
            });
        }
        //每隔internalLockLeaseTime/3=10秒檢查一次
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

//lua腳本 執(zhí)行包裝好的lua腳本進行key續(xù)期
protected 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()),
            internalLockLeaseTime, getLockName(threadId));
}

關(guān)鍵結(jié)論

上述源碼讀過來我們可以記住幾個關(guān)鍵情報:

  • watch dog 在當(dāng)前節(jié)點存活時每10s給分布式鎖的key續(xù)期 30s;
  • watch dog 機制啟動,且代碼中沒有釋放鎖操作時吵冒,watch dog 會不斷的給鎖續(xù)期纯命;
  • 從可2得出,如果程序釋放鎖操作時因為異常沒有被執(zhí)行痹栖,那么鎖無法被釋放亿汞,所以釋放鎖操作一定要放到 finally {} 中;
    看到3的時候揪阿,可能會有人有疑問留夜,如果釋放鎖操作本身異常了,watch dog 還會不停的續(xù)期嗎图甜?下面看一下釋放鎖的源碼碍粥,找找答案
// 鎖釋放
public void unlock() {
    try {
        get(unlockAsync(Thread.currentThread().getId()));
    } catch (RedisException e) {
        if (e.getCause() instanceof IllegalMonitorStateException) {
            throw (IllegalMonitorStateException) e.getCause();
        } else {
            throw e;
        }
    }
}

// 進入 unlockAsync(Thread.currentThread().getId()) 方法 入?yún)⑹钱?dāng)前線程的id
public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<Void>();
    //執(zhí)行l(wèi)ua腳本 刪除key
    RFuture<Boolean> future = unlockInnerAsync(threadId);

    future.onComplete((opStatus, e) -> {
        // 無論執(zhí)行l(wèi)ua腳本是否成功 執(zhí)行cancelExpirationRenewal(threadId) 方法來刪除EXPIRATION_RENEWAL_MAP中的緩存
        cancelExpirationRenewal(threadId);

        if (e != null) {
            result.tryFailure(e);
            return;
        }

        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            result.tryFailure(cause);
            return;
        }

        result.trySuccess(null);
    });

    return result;
}

// 此方法會停止 watch dog 機制
void cancelExpirationRenewal(Long threadId) {
    ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (task == null) {
        return;
    }
    
    if (threadId != null) {
        task.removeThreadId(threadId);
    }

    if (threadId == null || task.hasNoThreads()) {
        Timeout timeout = task.getTimeout();
        if (timeout != null) {
            timeout.cancel();
        }
        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
    }
}

釋放鎖的操作中 有一步操作是從 EXPIRATION_RENEWAL_MAP 中獲取 ExpirationEntry 對象,然后將其remove黑毅,結(jié)合watch dog中的續(xù)期前的判斷:

EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
    return;
}

如果釋放鎖操作本身異常了嚼摩,watch dog 還會不停的續(xù)期嗎?不會矿瘦,因為無論釋放鎖操作是否成功枕面,EXPIRATION_RENEWAL_MAP中的目標(biāo) ExpirationEntry 對象已經(jīng)被移除了,watch dog 通過判斷后就不會繼續(xù)給鎖續(xù)期了缚去。


Redisson實現(xiàn)分布式鎖(1)---原理

Redisson 官方文檔

談?wù)劵赗edis分布式鎖(下)- Redisson源碼解析

https://www.cnblogs.com/keeya/p/14332131.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末潮秘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子易结,更是在濱河造成了極大的恐慌枕荞,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搞动,死亡現(xiàn)場離奇詭異躏精,居然都是意外死亡,警方通過查閱死者的電腦和手機鹦肿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門矗烛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人箩溃,你說我怎么就攤上這事瞭吃。” “怎么了涣旨?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵歪架,是天一觀的道長。 經(jīng)常有香客問我开泽,道長牡拇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮惠呼,結(jié)果婚禮上导俘,老公的妹妹穿的比我還像新娘。我一直安慰自己剔蹋,他們只是感情好旅薄,可當(dāng)我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著泣崩,像睡著了一般少梁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上矫付,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天凯沪,我揣著相機與錄音,去河邊找鬼买优。 笑死妨马,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的杀赢。 我是一名探鬼主播烘跺,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼脂崔!你這毒婦竟也來了滤淳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤砌左,失蹤者是張志新(化名)和其女友劉穎脖咐,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體绊困,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡文搂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了秤朗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡笔喉,死狀恐怖取视,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情常挚,我是刑警寧澤作谭,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站奄毡,受9級特大地震影響折欠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一锐秦、第九天 我趴在偏房一處隱蔽的房頂上張望咪奖。 院中可真熱鬧,春花似錦酱床、人聲如沸羊赵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昧捷。三九已至,卻和暖如春罐寨,著一層夾襖步出監(jiān)牢的瞬間靡挥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工鸯绿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留跋破,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓楞慈,卻偏偏與公主長得像幔烛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子囊蓝,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容