Java中Redis鎖的實(shí)現(xiàn)

由于具體業(yè)務(wù)場景的需求,需要保證數(shù)據(jù)在分布式環(huán)境下的正確更新,所以研究了一下Java中分布式鎖的實(shí)現(xiàn)。

Java分布式鎖的實(shí)現(xiàn)方式主要有以下三種:

  1. 數(shù)據(jù)庫實(shí)現(xiàn)的樂觀鎖
  2. Redis實(shí)現(xiàn)的分布式鎖
  3. Zookeeper實(shí)現(xiàn)的分布式鎖

其中,較常用的是前兩種方式,但是數(shù)據(jù)庫實(shí)現(xiàn)方式需要較多的數(shù)據(jù)庫操作,所以最終選擇的是用Redis實(shí)現(xiàn)分布式鎖。

最初考慮分布式鎖的數(shù)據(jù)安全性的時候,只考慮到兩點(diǎn)。第一,Redis鎖需要有一個超時時間,這樣即便某個持有鎖的節(jié)點(diǎn)掛了,也不到導(dǎo)致其他節(jié)點(diǎn)死鎖,保證每個鎖有一個UniqueId;第二,每個鎖需要有一個UniqueId,確保當(dāng)一個線程執(zhí)行完一個任務(wù)去釋放鎖的時候釋放的一定是自己的鎖,否則可能存在一種場景,就是一個線程釋放鎖的時候,它的鎖可能已經(jīng)超時被釋放了,而因?yàn)槿鄙僖粋€UniqueId,它卻釋放了另一個線程的鎖

基于以上兩點(diǎn)的考慮,分別設(shè)計了獲取鎖和釋放鎖的api。

public interface DistributionLockService {
    /**
     * @param lockName the name of the lock
     * @param uniqueCode uniqueCode for the lock
     * @param expireTime expire time of lock(MILLISECONDS)
     * @return the result of get lock
     * */
    boolean getLock(String lockName, String uniqueCode, int expireTime);

    /**
     * @param lockName the name of the lock
     * @param uniqueCode uniqueCode for the lock
     * */
    void releaseLock(String lockName, String uniqueCode);

}

具體的實(shí)現(xiàn)代碼如下:

   /**
     * @param lockName the name of the lock
     * @param uniqueCode uniqueCode for the lock
     * @param expireTime expire time of lock(MILLISECONDS)
     * @return the result of get lock
     * */
    @Override
    public boolean getLock(String lockName, String uniqueCode, int expireTime) {

        boolean isLock = false;
        try {
            Long result = jedis.setnx(lockName, uniqueCode);
            isLock = result == 1 ? true : false;
            if (isLock) {
                jedis.expire(lockName, expireTime);
            }
        } catch (Exception e){
            logger.error("DistributionLockService/getLock", e);
        }
        return isLock;
    }

    /**
     * @param lockName the name of the lock
     * @param uniqueCode uniqueCode for the lock
     * */
    @Override
    public void releaseLock(String lockName, String uniqueCode) {
        try {
            String tag = jedis.get(getKey(lockName));
            if (tag != null && tag.equals(uniqueCode))
                jedis.del(getKey(lockName));
        } catch (Exception e) {
            logger.error("DistributionLockService/releaseLock", e);
        }
    }

上述的代碼用setnx+expire實(shí)現(xiàn)分布式鎖。調(diào)用setnx,當(dāng)傳入的key未被占用時,就在redis中插入一條該key的記錄,返回值為1,此時為其設(shè)置超時時間。而當(dāng)這個key在redis中已有記錄時,則不會重新插入記錄,這樣的話,便可以實(shí)現(xiàn)分布式鎖的基本功能。且為其設(shè)置過期時間,并加入UniqueId的check,避免了上述提及的兩個問題。

但是,上述代碼仍然存在問題,就是忽略了操作的原子性。獲取鎖的時候,調(diào)sexnx方法與設(shè)置超時時間expire不是原子操作,如果在sexnx方法執(zhí)行成功后,節(jié)點(diǎn)突然down掉,沒有執(zhí)行expire方法,而之后的釋放鎖操作也沒有執(zhí)行,那么這個節(jié)點(diǎn)便會長期持有鎖,盡管這種可能性很小,但是依然存在死鎖的風(fēng)險。為了避免這種風(fēng)險,修正代碼如下:

   private static final String SET_IF_NOT_EXIST = "NX";
   private static final String SET_WITH_EXPIRE_TIME = "PX";
   private static final String IS_LOCKED = "OK";

    /**
     * @param lockName the name of the lock
     * @param uniqueCode uniqueCode for the lock
     * @param expireTime expire time of lock
     * @return the result of get lock
     * */
    @Override
    public boolean getLock(String lockName, String uniqueCode, int expireTime) {

        boolean isLock = false;
        try {
            String result = jedis.set(lockName, uniqueCode, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

            isLock = IS_LOCKED.equalsIgnoreCase(result) ? true : false;

        } catch (Exception e){
            logger.error("DistributionLockService/getLock", e);
        }
        return isLock;
    }

Redis較高的版本中,有一個有五個參數(shù)的set方法,其中前兩個參數(shù)就是key和value,最后一個參數(shù)是過期時間,中間兩個參數(shù)表示setnx和setex,實(shí)際上就是一個可以設(shè)置過期時間的setnx方法。這個方法可以保證加鎖和設(shè)置過期時間兩者是作為一個請求傳送到Redis服務(wù)器的,所以不會出現(xiàn)上述的死鎖場景。

加鎖的問題解決了,解鎖的問題依然在。上述的解鎖代碼中,在解鎖之前先驗(yàn)證了UniqueId,然后采用del方法來釋放鎖,但是由于get和del是兩次請求,而不是一個原子操作,所以這之間仍存在并發(fā)的問題。若做check的時候,檢查得到確實(shí)是這個鎖的UniqueId,但是在執(zhí)行del方法之前,這個鎖已經(jīng)超時,然后新的線程也已經(jīng)獲取到鎖了,那么del刪掉的鎖,便不是自己的鎖,而是下一個線程的鎖。
Redis中沒有直接的api處理這個問題。解決這個問題,需要使用lua腳本,來確保整個操作的原子性。代碼如下:

    /**
     * @param lockName the name of the lock
     * @param uniqueCode uniqueCode for the lock
     * */
    @Override
    public void releaseLock(String lockName, String uniqueCode) {
        try {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                            "then return redis.call('del', KEYS[1]) " +
                            "else return 0 end";
            jedis.eval(script, Arrays.asList(lockName), Arrays.asList(uniqueCode));

        } catch (Exception e) {
            logger.error("DistributionLockService/releaseLock", e);
        }
    }

jedis的eval方法支持執(zhí)行l(wèi)ua腳本方法,所以便可利用這個方法來實(shí)現(xiàn)釋放鎖的原子操作,具體邏輯和之前的代碼其實(shí)是一致的,但是由于是原子操作,所以可以避免上文中存在的問題。

至此,簡單Redis鎖的實(shí)現(xiàn)便算是成功了。但是其中依然存在許多問題,如果Redis不是單機(jī)的,而是集群分布的,那么其中的數(shù)據(jù)同步該怎么做?在有些較看重數(shù)據(jù)的正確性的場景中,即使Redis鎖超時,只要檢測到機(jī)器仍在正常運(yùn)行Redis鎖就不應(yīng)該被釋放,而應(yīng)該被續(xù)期,這些,都是redis鎖在更復(fù)雜的場景中所需要考慮的。留待以后繼續(xù)研究。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容