Etcd分佈式鎖(二):支持公平鎖,避免某些場景下線程長期沒法獲取鎖

爲何須要公平鎖

公平鎖和非公平的鎖的區別主要仍是在於公平鎖保證先競爭的的線程會優先獲取鎖,而非公平鎖則沒法提供這個保障。redis

在ReentrantLock的實現中,二者的區別主要是公平鎖在獲取鎖以前,會先去判斷,若是有其餘線程持有鎖或者有其餘線程在等待隊列中等待這,那麼該線程將被放入到隊列中。而非公平鎖會先嚐試去獲取鎖,只有當鎖被某個線程持有時,該線程纔會被放入隊列中。這樣就意味着非公平鎖會有更多的機會去搶佔鎖。bash

在ReentrantLock的實現中,非公平鎖性能會高於公平鎖,由於公平鎖會增長不少線程切換的開銷,而公平鎖能避免某些場景下長時間等待依然沒法獲取鎖的狀況。由於兩種鎖各有優劣,咱們才須要根據不一樣場景去選擇不一樣的鎖。多線程

可是在基於etcd實現的分佈式鎖的場景內,由於Jetcd原生支持的就是公平鎖,咱們實現公平鎖和非公平鎖的代價是相差無幾的。因此在能實現公平鎖且性能相差很少的前提下,咱們天然得優先去選擇實現公平鎖。jvm

爲何以前沒實現公平鎖

看到這裏,你們必定很疑惑。既然etcd自然支持的就是公平鎖,爲何我在這裏還要強調鎖是否公平的問題。分佈式

在初版的實現中,咱們是按照redis分佈式鎖非公平鎖的思路去實現可重入的場景的,因此就致使了本來能夠實現公平鎖的cp分佈式鎖,由於可重入的問題,硬生生地變成了非公平鎖。post

final Boolean tryLock(long waitTime) {
    final long startTime = System.currentTimeMillis();
    final long endTime = startTime + waitTime * 1000;
    final long lockTime = LOCK_TIME;
    final Thread current = Thread.currentThread();
    try {
        do {
            int c = this.getState();
            if (c == 0) {
                LockLeaseData lockLeaseData = this.getLockLeaseData(lockName, lockTime);
                if (Objects.isNull(lockLeaseData)) {
                    return Boolean.FALSE;
                }
                Long leaseId = lockLeaseData.getLeaseId();
                if (lockEtcdClient.tryLock(leaseId, lockName, endTime - System.currentTimeMillis())) {
                    log.info("線程獲取重入鎖成功,cp鎖的名稱爲{}", lockName);
                    this.setLockOwnerThread(current);
                    this.setState(c + 1);
                    return Boolean.TRUE;
                }
            } else if (lockOwnerThread == Thread.currentThread()) {
                if (c + 1 <= 0) {
                    throw new Error("Maximum lock count exceeded");
                }
                this.setState(c + 1);
                log.info("線程重入鎖成功,cp鎖的名稱爲{},當前LockCount爲{}", lockName, state);
                return Boolean.TRUE;
            }
            int sleepTime = SLEEP_TIME_ONCE;
            if (waitTime > 0) {
                log.info("線程暫時沒法得到cp鎖,當前已等待{}ms,本次將再等待{}ms,cp鎖的名稱爲{}", System.currentTimeMillis() - startTime, sleepTime, lockName);
                try {
                    Thread.sleep(sleepTime);
                } catch (InterruptedException e) {
                    log.info("線程等待過程當中被中斷,cp鎖的名稱爲{}", lockName, e);
                }
            }
        } while (System.currentTimeMillis() <= endTime);
        if (waitTime == 0) {
            log.info("線程得到cp鎖失敗,將放棄獲取,cp鎖的名稱爲{}", lockName);
        } else {
            log.info("線程得到cp鎖失敗,以前共等待{}ms,將放棄等待獲取,cp鎖的名稱爲{}", System.currentTimeMillis() - startTime, lockName);
        }
        this.stopKeepAlive();
        return Boolean.FALSE;
    } catch (Exception e) {
        log.error("execute error", e);
        this.stopKeepAlive();
        return Boolean.FALSE;
    }
}
複製代碼

這是最第一版的設計,咱們能夠看到在同一jvm內的不一樣線程和其餘jvm內的線程去競爭鎖的時候,可能會發生一個狀況:線程a已經獲取到鎖a,此時state>0且lockOwnerThread是線程a,而後線程b去競爭鎖a時,會直接跳過去競爭ectd鎖的步驟,再根據線程b的等待時間的設置決定是快速失敗仍是休眠後再循環。而後若是有其餘應用的線程c,也嘗試去競爭鎖a,而在線程c的應用內,鎖的state爲0,因此線程c會直接去競爭etcd鎖。不出意外的話,線程c會比線程b優先競爭到鎖a。這是不公平的。性能

如何實現可重入的公平鎖

以前咱們由於先判斷state是否爲0才致使的鎖非公平的問題。之因此這麼設計,是由於參照了ReentrantLock可重入的實現,而後在redis的非公平的分佈式鎖中這麼寫也徹底沒問題,因此就照搬照抄過來了。優化

咱們須要對鎖在可重入場景下的競爭邏輯作下修改,修改後的邏輯爲先去判斷是否當前線程已持有鎖,若是有,則直接重入,鎖的state+1。不然直接去競爭etcd鎖。這樣就避免了非公平競爭的發生。ui

此外還增長了一個快速失敗的狀況:在當前jvm內已有其餘線程持有鎖,若是當前線程競爭的是失敗後不重試的鎖,則直接快速失敗。由於是競爭失敗後再也不重試,因此在其餘線程已有持有的狀況下,無須再去競爭etcd鎖,這樣能減小一部分的開銷。this

之因此還保留do-while的實現方式,是爲了保證在某些狀況下若是waitTime沒超時,那麼即使出現tryLock失敗,該線程依然能夠繼續競爭鎖。

如下是do-while中的代碼實現:

int c = this.getState();
//當前線程已持有鎖
if (c > 0 && lockOwnerThread == Thread.currentThread()) {
    if (c + 1 <= 0) {
        throw new Error("Maximum lock count exceeded");
    }
    this.setState(c + 1);
    log.info("線程重入鎖成功,cp鎖的名稱爲{},當前LockCount爲{}", lockName, state);
    return Boolean.TRUE;
}
//其餘線程已持有鎖的狀況下,若是不等待獲取鎖,則直接快速失敗
if (c > 0 && waitTime == 0) {
    log.info("線程得到cp鎖失敗,將放棄獲取,cp鎖的名稱爲{}", lockName);
    return Boolean.FALSE;
}
LockLeaseData lockLeaseData = this.getLockLeaseData(lockName, lockTime);
if (Objects.isNull(lockLeaseData)) {
    return Boolean.FALSE;
}
//若是當前應用中沒有線程持有鎖,則去競爭獲取鎖
Long leaseId = lockLeaseData.getLeaseId();
if (lockEtcdClient.tryLock(leaseId, lockName, endTime - System.currentTimeMillis())) {
    log.info("線程獲取重入鎖成功,cp鎖的名稱爲{}", lockName);
    this.setLockOwnerThread(current);
    this.setState(c + 1);
    return Boolean.TRUE;
}
複製代碼

更新說明

本次更新,主要將非公平鎖改成公平鎖,此外還進行了如下幾處優化:
一、優化了在同jvm不一樣線程內須要休眠等待沒法及時競爭鎖的狀況
二、優化了租約被銷燬後依然能續期的問題
三、優化了快速失敗的場景。若是線程去tryLock時,同一jvm內已有其餘線程持有鎖,且該線程競爭的是失敗後不重試的鎖,則直接快速失敗

推薦閱讀

一、Etcd分佈式鎖:cp分佈式鎖的最佳實現
二、Redis分佈式鎖:基於AOP和Redis實現的簡易版分佈式鎖
三、Redis分佈式鎖(二):支持鎖的續期,避免鎖超時後致使多個線程得到鎖
四、Redis分佈式鎖(三):支持鎖可重入,避免鎖遞歸調用時死鎖

好了,咱們下一期再見,歡迎你們一塊兒留言討論。同時也歡迎點贊~

相關文章
相關標籤/搜索