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

Redisson分布式鎖實(shí)現(xiàn)原理示例詳解

 更新時(shí)間:2026年04月13日 08:29:35   作者:sirzero  
Redisson分布式鎖通過Lua腳本保證原子性操作,實(shí)現(xiàn)了分布式互斥、可重入、自動(dòng)續(xù)期等核心特性,其高級(jí)特性包括公平鎖、讀寫鎖和紅鎖算法,能夠滿足不同場(chǎng)景下的并發(fā)控制需求,這篇文章主要介紹了Redisson分布式鎖實(shí)現(xiàn)原理的相關(guān)資料,需要的朋友可以參考下

前言

說到redis的分布式鎖容易想到了setNx,好處是實(shí)現(xiàn)簡(jiǎn)單,但是會(huì)有一些問題比如誤刪鎖問題、鎖不可重入問題。所以Redisson并沒有通過setNx命令來實(shí)現(xiàn)加鎖,而是自己實(shí)現(xiàn)了一套完成的加鎖的邏輯

加鎖與解鎖

RLock繼承了Java的lock接口,RedissonLock繼承自RedissonBaseLock(抽象類),而RedissonBaseLock又實(shí)現(xiàn)了RLock。

//調(diào)用getLock時(shí)候就是new了一個(gè)RedissonLock
public RLock getLock(String name) {
    return new RedissonLock(this.commandExecutor, name);
}

我們重點(diǎn)看lock方法:

public void lock() {
    try {
        this.lock(-1L, (TimeUnit)null, false);
    } catch (InterruptedException var2) {
        throw new IllegalStateException();
    }
}
?
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    //調(diào)用了tryAcquire獲取鎖,這里傳入了-1L代表沒有指定鎖的釋放時(shí)間,正常情況下如果不釋放是永久持有。
    Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
    //以下代碼先忽略
    //.....
}
?
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    //這里調(diào)用get,等待future返回。
    return (Long)this.get(this.tryAcquireAsync0(waitTime, leaseTime, unit, threadId));
}
?
private RFuture<Long> tryAcquireAsync0(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    //使用了線程池去獲取鎖
    return this.getServiceManager().execute(() -> {
        return this.tryAcquireAsync(waitTime, leaseTime, unit, threadId);
    });
}
//重點(diǎn)方法來了
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture ttlRemainingFuture;
    if (leaseTime > 0L) {
        //如果這里leaseTime不為0說明用戶設(shè)置了鎖的租約時(shí)間直接傳入。
        ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        //如果這里leaseTime為0說明用戶沒有限制鎖的租約時(shí)間,但是這里仍然會(huì)傳30秒的持有時(shí)間
        ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
?
    CompletionStage<Long> s = this.handleNoSync(threadId, ttlRemainingFuture);
    RFuture<Long> ttlRemainingFuture = new CompletableFutureWrapper(s);
    CompletionStage<Long> f = ttlRemainingFuture.thenApply((ttlRemaining) -> {
        //如果為空說明lua獲取鎖腳本獲得了鎖
        if (ttlRemaining == null) {
            //判斷是否開啟看門狗機(jī)制。
            if (leaseTime > 0L) {
                this.internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                this.scheduleExpirationRenewal(threadId);
            }
        }
?
        return ttlRemaining;
    });
    return new CompletableFutureWrapper(f);
}
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    return this.evalWriteSyncedNoRetryAsync(this.getRawName(), LongCodec.INSTANCE, command, 
    "if ((redis.call('exists', KEYS[1]) == 0) or (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;"+
    //返回鎖的過期時(shí)間。
    "return redis.call('pttl', KEYS[1]);", 
    Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
}

獲取鎖的整個(gè)邏輯是:

首先,如果用戶調(diào)用獲取鎖時(shí)候沒有限制租約時(shí)間,redisson會(huì)自動(dòng)給tryLockInnerAsync加上一個(gè)30秒的租約時(shí)間,并調(diào)用scheduleExpirationRenewal進(jìn)行看門狗機(jī)制

tryLockInnerAsync是執(zhí)行了一個(gè)lua腳本,首先redisson他采用的是hash來存放這個(gè)鎖,key是鎖的名字,field由UUID和線程id組成(UUID是區(qū)分不同客戶端,防止不同客戶端但是線程名恰好相同),value是鎖的重入次數(shù)。這樣就避免了鎖的誤刪,重入和死鎖問題了。lua的大致流程為:

  1. 先判斷當(dāng)前鎖是否被持有或者持有者是否是當(dāng)前線程,如果是的話重入次數(shù)加1,并設(shè)置/重置整個(gè)鎖 Key 的過期時(shí)間(防止死鎖)然后返回。
  2. 如果上面if不成立說明鎖被別人持有了,則返回當(dāng)前鎖剩余的存活時(shí)間(TTL)。客戶端拿到這個(gè)時(shí)間后,會(huì)等待這么久再重試。

回到剛剛我們忽略的代碼:

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
    //如果這里ttl不為空說明鎖被人占有了。
    if (ttl != null) {
        //此時(shí)使用pub/sub訂閱這個(gè)管道
        CompletableFuture<RedissonLockEntry> future = this.subscribe(threadId);
        this.pubSub.timeout(future);
        //內(nèi)部維護(hù)了一個(gè)Semaphore用于控制本地線程的阻塞和喚醒
        RedissonLockEntry entry;
        if (interruptibly) {
            entry = (RedissonLockEntry)this.commandExecutor.getInterrupted(future);
        } else {
            entry = (RedissonLockEntry)this.commandExecutor.get(future);
        }
?
        try {
            //死循環(huán)直到獲取鎖或者被中斷
            while(true) {
                //先樂觀查詢一次
                ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
                if (ttl == null) {
                    return;
                }
               //仍然沒有獲取到鎖則進(jìn)入阻塞階段
                if (ttl >= 0L) {
                    try {
                        //通過ttl時(shí)間判斷鎖還有多久釋放,從而判斷阻塞多久,避免cpu空轉(zhuǎn)帶來性能消耗,精準(zhǔn)在鎖釋放時(shí)候喚醒
                        //當(dāng)有人釋放鎖了,redisson監(jiān)聽到了之后調(diào)用entry.getLatch().release(),或者到達(dá)ttl了都會(huì)使線程被喚醒
                        entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException var14) {
                        InterruptedException e = var14;
                        if (interruptibly) {
                            throw e;
                        }
                        //出現(xiàn)異常了,重新?lián)屾i
                        entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else if (interruptibly) {
                    entry.getLatch().acquire();
                } else {
                    entry.getLatch().acquireUninterruptibly();
                }
            }
        } finally {
            //最后終止訂閱。
            this.unsubscribe(entry, threadId);
        }
    }
}

解鎖的邏輯:

跟加鎖邏輯一樣都是異步轉(zhuǎn)換同步。

public RFuture<Void> unlockAsync(long threadId) {
    String requestId = this.getServiceManager().generateId();
    return this.getServiceManager().execute(() -> {
        return this.unlockAsync0(threadId, requestId);
    });
}
?
private RFuture<Void> unlockAsync0(long threadId, String requestId) {
    CompletionStage<Boolean> future = this.unlockInnerAsync(threadId, requestId);
    //處理異常
    CompletionStage<Void> f = future.handle((res, e) -> {
        this.cancelExpirationRenewal(threadId, res);
        if (e != null) {
            if (e instanceof CompletionException) {
                throw (CompletionException)e;
            } else {
                throw new CompletionException(e);
            }
        } else if (res == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + threadId);
            throw new CompletionException(cause);
        } else {
            return null;
        }
    });
    return new CompletableFutureWrapper(f);
}
?
protected final RFuture<Boolean> unlockInnerAsync(long threadId, String requestId) {
    if (requestId == null) {
        requestId = this.getServiceManager().generateId();
    }
?
    MasterSlaveServersConfig config = this.getServiceManager().getConfig();
    long timeout = ((long)config.getTimeout() + config.getRetryDelay().calcDelay(config.getRetryAttempts()).toMillis()) * (long)config.getRetryAttempts();
    timeout = Math.max(timeout, 1L);
    //異步釋放鎖
    RFuture<Boolean> r = this.unlockInnerAsync(threadId, requestId, (int)timeout);
    CompletionStage<Boolean> ff = r.thenApply((v) -> {
        CommandAsyncExecutor ce = this.commandExecutor;
        if (ce instanceof CommandBatchService) {
            ce = new CommandBatchService(this.commandExecutor);
        }
?
        ((CommandAsyncExecutor)ce).writeAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.DEL, new Object[]{this.getUnlockLatchName(this.id)});
        if (ce instanceof CommandBatchService) {
            ((CommandBatchService)ce).executeAsync();
        }
?
        return v;
    });
    return new CompletableFutureWrapper(ff);
}
?
protected RFuture<Boolean> unlockInnerAsync(long threadId, String requestId, int timeout) {
    return this.evalWriteSyncedNoRetryAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
//防重檢查(冪等性)
"local val = redis.call('get', KEYS[3]);"+
"if val ~= false then "+
    "return tonumber(val);"+
"end;"+
//判斷是否持有鎖
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then "+
    "return nil;"+
"end; "+
//扣減重入次數(shù)
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); "+
//判斷是“重入釋放”還是“徹底釋放”
"if (counter > 0) then "+
    //重入次數(shù)減1
    "redis.call('pexpire', KEYS[1], ARGV[2]); "+
    "redis.call('set', KEYS[3], 0, 'px', ARGV[5]); "+
"return 0; "+
"else "+
    //徹底釋放鎖
    "redis.call('del', KEYS[1]); "+
    //發(fā)送隊(duì)列消息
    "redis.call(ARGV[4], KEYS[2], ARGV[1]); "+
    "redis.call('set', KEYS[3], 1, 'px', ARGV[5]); "+
    "return 1; "+
"end; ",
Arrays.asList(this.getRawName(), this.getChannelName(), this.getUnlockLatchName(requestId)), new Object[]{LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId), this.getSubscribeService().getPublishCommand(), timeout});
}

lua的流程為:

先去查一下 KEYS[3](查看解鎖請(qǐng)求的結(jié)果緩存)是否存在。如果存在,說明這個(gè)請(qǐng)求之前已經(jīng)處理過了(可能是網(wǎng)絡(luò)波動(dòng)導(dǎo)致客戶端以為超時(shí)了,重發(fā)了請(qǐng)求)。直接返回之前緩存的結(jié)果(0 或 1),不需要再執(zhí)行后面的邏輯。這保證了冪等性。

  1. 檢查 Hash KEYS[1] 中是否存在當(dāng)前線程 ARGV[3]。如果不存在(== 0),說明當(dāng)前線程根本沒有持有這把鎖。返回 nil(Java 客戶端會(huì)拋出 IllegalMonitorStateException)。
  2. 將該線程的加鎖計(jì)數(shù)器減 1,counter 是減完之后剩下的次數(shù)。
  3. 如果counter > 0,說明鎖重入了。pexpire:刷新鎖的過期時(shí)間(看門狗時(shí)間),只要鎖還沒徹底釋放,就得給它續(xù)命。set KEYS[3] 0:記錄本次請(qǐng)求結(jié)果為 0(表示未完全釋放)。返回 0,代表還未釋放鎖。
  4. 如果counter<=0,說明鎖此時(shí)可以釋放了,這里會(huì)先刪除這個(gè)鎖對(duì)應(yīng)的key,然后調(diào)用redis.call(ARGV[4], KEYS[2], ARGV[1]);這里ARGC[4]其實(shí)通常是publish發(fā)送消息,之所以不寫死是因?yàn)榧合驴梢杂胹publish加快性能。這里發(fā)送消息是為了前文提到的,告訴監(jiān)聽者鎖已經(jīng)釋放。set KEYS[3] 1:緩存本次請(qǐng)求結(jié)果為 1,并將1返回,代表鎖成功釋放了

這里之所以要多出一個(gè)KEY[3]是為了做冪等。KEY[3]命名一般為{鎖前綴}:{鎖名}:{requestId},每次請(qǐng)求時(shí)候都會(huì)帶上不同的requestId,vlaue是requestId請(qǐng)求的解鎖結(jié)果

存在一種可能,客戶端重入了2次鎖,客戶端第一次調(diào)用unlock時(shí)候,redis正常執(zhí)行了解鎖邏輯,并扣減了1次鎖記錄,但是由于網(wǎng)絡(luò)波動(dòng),響應(yīng)丟失了,此時(shí)客戶端會(huì)重新發(fā)起一次請(qǐng)求,導(dǎo)致重復(fù)扣減。為了解決這個(gè)問題,就利用key[3],判斷一下相同的請(qǐng)求id是否有記錄如果是就代表確實(shí)發(fā)送了響應(yīng)丟失,直接將上次的數(shù)據(jù)返回,避免重復(fù)解鎖。

公平鎖

以上鎖是默認(rèn)非公平鎖,所有線程都去爭(zhēng)搶鎖,而公平鎖則是進(jìn)入隊(duì)列等待,防止饑餓問題。

當(dāng)我們getFairLock時(shí)候其實(shí)是new了一個(gè)RedissonFairLock

public RLock getFairLock(String name) {
    return new RedissonFairLock(this.commandExecutor, name);
}

RedissonFairLock繼承自RedissonLock也就是說加鎖的流程都是大致相同的。RedissonFairLock重寫了tryLockInnerAsync方法

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    long wait = this.threadWaitTime;
    if (waitTime > 0L) {
        wait = unit.toMillis(waitTime);
    }
?
    long currentTime = System.currentTimeMillis();
    if (command == RedisCommands.EVAL_NULL_BOOLEAN) {
        return this.commandExecutor.syncedEvalNoRetry(
            this.getRawName(),
            LongCodec.INSTANCE,
            command,
            // Lua腳本 - 用于獲取鎖
            "while true do " +
            "    local firstThreadId2 = redis.call('lindex', KEYS[2], 0); " +
            "    if firstThreadId2 == false then " +
            "        break; " +
            "    end; " +
            "    local timeout = redis.call('zscore', KEYS[3], firstThreadId2); " +
            "    if timeout ~= false and tonumber(timeout) <= tonumber(ARGV[3]) then " +
            "        redis.call('zrem', KEYS[3], firstThreadId2); " +
            "        redis.call('lpop', KEYS[2]); " +
            "    else " +
            "        break; " +
            "    end; " +
            "end; " +
            "if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
            "    redis.call('lpop', KEYS[2]); " +
            "    redis.call('zrem', KEYS[3], ARGV[2]); " +
            "    local keys = redis.call('zrange', KEYS[3], 0, -1); " +
            "    for i = 1, #keys, 1 do " +
            "        redis.call('zincrby', KEYS[3], -tonumber(ARGV[4]), keys[i]); " +
            "    end; " +
            "    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 1;",
            Arrays.asList(this.getRawName(), this.threadsQueueName, this.timeoutSetName),
            new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId), currentTime, wait}
        );
    } else if (command == RedisCommands.EVAL_LONG) {
        return this.commandExecutor.syncedEvalNoRetry(
            this.getRawName(),
            LongCodec.INSTANCE,
            command,
            // Lua腳本 - 用于嘗試獲取鎖并返回TTL
            "while true do " +
            "    local firstThreadId2 = redis.call('lindex', KEYS[2], 0); " +
            "    if firstThreadId2 == false then " +
            "        break; " +
            "    end; " +
            "    local timeout = redis.call('zscore', KEYS[3], firstThreadId2); " +
            "    if timeout ~= false and tonumber(timeout) <= tonumber(ARGV[4]) then " +
            "        redis.call('zrem', KEYS[3], firstThreadId2); " +
            "        redis.call('lpop', KEYS[2]); " +
            "    else " +
            "        break; " +
            "    end; " +
            "end; " +
            "if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +
            "    redis.call('lpop', KEYS[2]); " +
            "    redis.call('zrem', KEYS[3], ARGV[2]); " +
            "    local keys = redis.call('zrange', KEYS[3], 0, -1); " +
            "    for i = 1, #keys, 1 do " +
            "        redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]); " +
            "    end; " +
            "    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; " +
            "local timeout = redis.call('zscore', KEYS[3], ARGV[2]); " +
            "if timeout ~= false then " +
            "    local ttl = redis.call('pttl', KEYS[1]); " +
            "    return math.max(0, ttl); " +
            "end; " +
            "local lastThreadId = redis.call('lindex', KEYS[2], -1); " +
            "local ttl; " +
            "if lastThreadId ~= false and lastThreadId ~= ARGV[2] and redis.call('zscore', KEYS[3], lastThreadId) ~= false then " +
            "    ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]); " +
            "else " +
            "    ttl = redis.call('pttl', KEYS[1]); " +
            "end; " +
            "local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]); " +
            "if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +
            "    redis.call('rpush', KEYS[2], ARGV[2]); " +
            "end; " +
            "return ttl;",
            Arrays.asList(this.getRawName(), this.threadsQueueName, this.timeoutSetName),
            new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId), wait, currentTime}
        );
    } else {
        throw new IllegalArgumentException();
    }
}

這里有倆段lua腳本,第一段對(duì)應(yīng)的是無參的tryLock()方法,進(jìn)行一次快速嘗試,如果獲取不到鎖直接返回。第二段對(duì)應(yīng)的是tryLock(waitTime)/lock這倆個(gè)阻塞等鎖的方法。這里我們重點(diǎn)看第二段lua腳本,第一個(gè)lua腳本和第二個(gè)類似:

--循環(huán)的判斷是否有waitTime已經(jīng)超過的節(jié)點(diǎn),如果有就剔除掉,防止占用著隊(duì)列
while true do 
    --查詢等待隊(duì)列頭節(jié)點(diǎn)是什么
    local firstThreadId2 = redis.call('lindex', KEYS[2], 0); 
    --如果等待隊(duì)列為空則跳出循環(huán)
    if firstThreadId2 == false then 
        break; 
    end; 
    --不為空,判斷一下是否超過了超時(shí)時(shí)間,如果是就刪除掉,沒有就跳出循環(huán)
    local timeout = redis.call('zscore', KEYS[3], firstThreadId2); 
    if timeout ~= false and tonumber(timeout) <= tonumber(ARGV[4]) then 
        redis.call('zrem', KEYS[3], firstThreadId2); 
        redis.call('lpop', KEYS[2]); 
    else 
        break; 
    end; 
end; 
--判斷一下鎖是否沒有被其他線程持有并且等待隊(duì)列不存在(等待隊(duì)列為空)或者對(duì)頭是此線程,如果是則進(jìn)入搶鎖。
if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then 
    --將自己從隊(duì)列移除
    redis.call('lpop', KEYS[2]); 
    redis.call('zrem', KEYS[3], ARGV[2]); 
    local keys = redis.call('zrange', KEYS[3], 0, -1); 
    --循環(huán)整個(gè)隊(duì)列,更新所有節(jié)點(diǎn)的超時(shí)時(shí)間(減少等待預(yù)算)
    for i = 1, #keys, 1 do 
        redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]); 
    end; 
    --真正的加鎖邏輯,加鎖后直接返回。
    redis.call('hset', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    --返回nil說明搶鎖成功
    return nil; 
end; 
--前面判斷為false,說明鎖被持有了,判斷一下鎖是否被本線程持有,如果是重入加1并返回
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; 
--走到這里說明鎖被其他人持有了,此時(shí)判斷一下本線程是否已經(jīng)在排隊(duì)了
local timeout = redis.call('zscore', KEYS[3], ARGV[2]); 
if timeout ~= false then 
    --如果在排隊(duì)了,則返回鎖的剩余持有時(shí)間
    local ttl = redis.call('pttl', KEYS[1]); 
    return math.max(0, ttl); 
end; 
--走到這里說明沒有入隊(duì),先查看一下隊(duì)尾節(jié)點(diǎn)
local lastThreadId = redis.call('lindex', KEYS[2], -1); 
local ttl; 
--如果隊(duì)尾節(jié)點(diǎn)存在(鎖被持有,有線程等待搶鎖)則ttl是錢一個(gè)人的超時(shí)時(shí)間-當(dāng)前時(shí)間。
if lastThreadId ~= false and lastThreadId ~= ARGV[2] and redis.call('zscore', KEYS[3], lastThreadId) ~= false then 
    ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]); 
else 
    --如果隊(duì)尾節(jié)點(diǎn)不存在(鎖被持有,且沒有線程等待搶鎖)則ttl就是鎖的過期時(shí)間
    ttl = redis.call('pttl', KEYS[1]); 
end; 
local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]); 
--然后入隊(duì),將計(jì)算好的超時(shí)時(shí)間放入Zset,并將本線程放入隊(duì)尾。
if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then 
    redis.call('rpush', KEYS[2], ARGV[2]); 
end; 
--返回告訴客戶端,還需要時(shí)間為ttl,使用Semaphore去阻塞。(喚醒過程和前文提到過的一樣)
return ttl;

此過程中用到了Zset,List,我們直到List用于存放資源爭(zhēng)搶者,那Zset又是干嘛的?

List 雖然能完美實(shí)現(xiàn) FIFO(先進(jìn)先出),但它有2個(gè)致命弱點(diǎn):

  • 不好判斷過期時(shí)間:List 只能告訴你誰排第一,但不能告訴你他還是不是活的(有沒有超時(shí)),如果非要利用List存儲(chǔ)過期時(shí)間就得通過value去分割,不僅需要占用cpu資源而且不好判斷如何分割,萬一用戶命名不規(guī)范導(dǎo)致分割錯(cuò)誤,此時(shí)需要Zset,它存儲(chǔ)了每個(gè)人的“死亡時(shí)間”,用來在每次操作前清理 List 里的僵尸節(jié)點(diǎn)。member是uuid+線程id,score是過期時(shí)間
  • 不好判斷是否當(dāng)前線程在隊(duì)列中:List需要O(n)判斷,時(shí)間慢,通過Zset的dict數(shù)據(jù)結(jié)構(gòu)O(1)判斷。

此外,由于每一個(gè)節(jié)點(diǎn)有可能是lock()這種無等待時(shí)間,會(huì)阻塞到一直獲取鎖,在Zset中難道我們要將Zset的過期分?jǐn)?shù)設(shè)置很大或者設(shè)置為-1代表沒有過期時(shí)間嗎?那如果客戶端宕機(jī)了,該節(jié)點(diǎn)不就占用這個(gè)Zset和list的節(jié)點(diǎn),且誰也清除不掉,導(dǎo)致內(nèi)存泄露。且輪到它作為頭節(jié)點(diǎn)時(shí)候,又不會(huì)搶鎖,導(dǎo)致全部都在死等。

舊版本的redisson是默認(rèn)給5秒過期時(shí)間,每次5秒后就刷新一下,如果客戶端宕機(jī)了就不會(huì)刷新,這個(gè)節(jié)點(diǎn)會(huì)被清除掉。但是當(dāng)競(jìng)爭(zhēng)激烈的時(shí)候5秒獲取不到鎖時(shí)候大量的線程醒過來同時(shí)去更新過期時(shí)間,這個(gè)驚群效應(yīng)會(huì)導(dǎo)致性能急劇下降,后續(xù)新版本改為了5分鐘。

如圖,比起非公平鎖多了倆個(gè)數(shù)據(jù)結(jié)構(gòu)

總結(jié)

到此這篇關(guān)于Redisson分布式鎖實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Redisson分布式鎖實(shí)現(xiàn)原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 在Spring中基于Java類進(jìn)行配置的完整步驟

    在Spring中基于Java類進(jìn)行配置的完整步驟

    基于Java配置選項(xiàng),可以編寫大多數(shù)的Spring不用配置XML,下面這篇文章主要給大家介紹了關(guān)于在Spring中基于Java類進(jìn)行配置的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2018-05-05
  • springboot不掃描@repository的問題及解決

    springboot不掃描@repository的問題及解決

    這篇文章主要介紹了springboot不掃描@repository的問題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-05-05
  • Java高效實(shí)現(xiàn)CSV轉(zhuǎn)Excel的三種方法詳解

    Java高效實(shí)現(xiàn)CSV轉(zhuǎn)Excel的三種方法詳解

    ?在日常開發(fā)工作中,CSV 文件作為一種簡(jiǎn)單、輕便的數(shù)據(jù)存儲(chǔ)格式,常被用于數(shù)據(jù)交換和存儲(chǔ),本篇文章將分享幾種基于 Java 的 CSV 轉(zhuǎn) Excel 方法,希望對(duì)大家有所幫助
    2026-02-02
  • 堆排序?qū)嵗?Java數(shù)組實(shí)現(xiàn))

    堆排序?qū)嵗?Java數(shù)組實(shí)現(xiàn))

    下面小編就為大家分享一篇使用Java數(shù)組實(shí)現(xiàn)堆排序的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2017-12-12
  • 深入理解hibernate的三種狀態(tài)

    深入理解hibernate的三種狀態(tài)

    本篇文章主要介紹了深入理解hibernate的三種狀態(tài) ,主要包括了transient(瞬時(shí)狀態(tài)),persistent(持久化狀態(tài))以及detached(離線狀態(tài)),有興趣的同學(xué)可以了解一下
    2017-05-05
  • 如何徹底刪除SVN中的文件和文件夾(附恢復(fù)方法)

    如何徹底刪除SVN中的文件和文件夾(附恢復(fù)方法)

    在SVN中如果刪除某個(gè)文件或文件夾也可以在歷史記錄中進(jìn)行找回,有的時(shí)候需要徹底刪除某些文件,即不希望通過歷史記錄進(jìn)行恢復(fù),需要在服務(wù)器上對(duì)SVN的數(shù)據(jù)進(jìn)行重新整理
    2014-08-08
  • SpringBoot 指標(biāo)監(jiān)控actuator的專題

    SpringBoot 指標(biāo)監(jiān)控actuator的專題

    未來每一個(gè)微服務(wù)在云上部署以后,我們都需要對(duì)其進(jìn)行監(jiān)控、追蹤、審計(jì)、控制等。SpringBoot就抽取了Actuator場(chǎng)景,使得我們每個(gè)微服務(wù)快速引用即可獲得生產(chǎn)級(jí)別的應(yīng)用監(jiān)控、審計(jì)等功能,通讀本篇對(duì)大家的學(xué)習(xí)或工作具有一定的價(jià)值,需要的朋友可以參考下
    2021-11-11
  • Java如何處理延遲任務(wù)過程解析

    Java如何處理延遲任務(wù)過程解析

    這篇文章主要介紹了Java如何處理延遲任務(wù)過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-10-10
  • JAVA String轉(zhuǎn)化成java.sql.date和java.sql.time方法示例

    JAVA String轉(zhuǎn)化成java.sql.date和java.sql.time方法示例

    這篇文章主要給大家分享了關(guān)于JAVA String轉(zhuǎn)化成java.sql.date和java.sql.time的方法,文中給出了詳細(xì)的示例代碼,相信對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來一起看看吧。
    2017-03-03
  • java編程隊(duì)列數(shù)據(jù)結(jié)構(gòu)代碼示例

    java編程隊(duì)列數(shù)據(jù)結(jié)構(gòu)代碼示例

    這篇文章主要介紹了java編程隊(duì)列數(shù)據(jù)結(jié)構(gòu)代碼示例,簡(jiǎn)單介紹了隊(duì)列的相關(guān)基礎(chǔ)知識(shí),然后通過實(shí)例向大家展示其實(shí)現(xiàn)方法,具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-11-11

最新評(píng)論

松溪县| 安乡县| 栾川县| 乌拉特中旗| 营山县| 石嘴山市| 凯里市| 邯郸县| 惠水县| 岳池县| 西昌市| 赣州市| 南丰县| 台东市| 岳阳县| 大同县| 房产| 泸水县| 班戈县| 柯坪县| 库尔勒市| 石门县| 通许县| 尉氏县| 清水河县| 晋江市| 老河口市| 英山县| 福鼎市| 清新县| 双辽市| 长汀县| 仲巴县| 盐山县| 望奎县| 海城市| 肃宁县| 平昌县| 乾安县| 星座| 灵石县|