使用Redisson實現redis的分布式鎖

Redisson簡介

Redisson在基于NIO的Netty框架上,充分的利用了Redis鍵值數據庫提供的一系列優勢,在Java實用工具包中常用接口的基礎上,為使用者提供了一系列具有分布式特性的常用工具類。使得原本作為協調單機多線程并發程序的工具包獲得了協調分布式多機多線程并發系統的能力,大大降低了設計和研發大規模分布式系統的難度。同時結合各富特色的分布式服務,更進一步簡化了分布式環境中程序相互之間的協作。

https://redisson.org/
https://redisson.org/Redisson.pdf
https://github.com/redisson/redisson/

Redisson實現的分布式鎖

org.redisson.api.RLock

Redisson獲取鎖的方式

org.redisson.api.RedissonClient

RedissonLock

加鎖

  • 使用redisson加鎖的代碼示例
    public void getlock() {

        Config config = new Config();
        config.useSingleServer()
                .setTimeout(1000000)
                .setAddress("redis://127.0.0.1:6379");
        RedissonClient redissonClient = Redisson.create(config);
        RLock testLock = redissonClient.getLock("test_lock");
       // 加鎖
        testLock.lock();
        try {
            Thread.sleep(100000L);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 釋放鎖
            testLock.unlock();
        }
    }
  • org.redisson.Redisson#getLock
    @Override
    public RLock getLock(String name) {
        return new RedissonLock(connectionManager.getCommandExecutor(), name);
    }
org.redisson.RedissonLock
  • RedissonLock獲取鎖的源碼部分

org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean)

    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        // 獲取當前線程Id
        long threadId = Thread.currentThread().getId();

        // 嘗試獲取鎖
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            // 獲取鎖失敗
            return;
        }

        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);

        try {
            while (true) {
                ttl = tryAcquire(leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }

                // waiting for message
                if (ttl >= 0) {
                    try {
                        getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    if (interruptibly) {
                        getEntry(threadId).getLatch().acquire();
                    } else {
                        getEntry(threadId).getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
    }

org.redisson.RedissonLock#tryAcquireAsync

    private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1) {
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // lock acquired
            if (ttlRemaining == null) {
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }

org.redisson.RedissonLock#tryLockInnerAsync

    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.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.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

  • 加鎖過程使用的lua腳本(其中KEYS[1]為鎖在redis中的key值,ARGV[1]為鎖的租期,對應redis中hash字段的value值,ARGV[2]為鎖在redis中的hash對象的字段名稱)
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]);
  • redis中鎖的結構


    鎖在redis中的結構

釋放鎖

org.redisson.RedissonLock#unlock

   @Override
    public void unlock() {
        try {
            get(unlockAsync(Thread.currentThread().getId()));
        } catch (RedisException e) {
            if (e.getCause() instanceof IllegalMonitorStateException) {
                throw (IllegalMonitorStateException) e.getCause();
            } else {
                throw e;
            }
        }
    }

org.redisson.RedissonLock#unlockAsync(long)

    @Override
    public RFuture<Void> unlockAsync(long threadId) {
        RPromise<Void> result = new RedissonPromise<Void>();
        RFuture<Boolean> future = unlockInnerAsync(threadId);

        future.onComplete((opStatus, e) -> {
            if (e != null) {
                // 取消分布式的watchDog續期
                cancelExpirationRenewal(threadId);
                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;
            }
            
            cancelExpirationRenewal(threadId);
            result.trySuccess(null);
        });

        return result;
    }

org.redisson.RedissonLock#unlockInnerAsync

    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                "end; " +
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                "else " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; "+
                "end; " +
                "return nil;",
                Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }
  • 釋放鎖執行的lua腳本( KEYS[1]:redis中hash對象的key值;KEYS[2]:釋放鎖的channelName;ARGV[1]:LockPubSub.UNLOCK_MESSAGE;ARGV[2]:鎖的過期時間;ARGV[3]:redis中hash對象的Field內容 )

先查詢redis中是否存在這個鎖
如果這個鎖不存在,則直接return;
如果鎖存在,則將鎖的占用-1
如果鎖的引用計數為0,則證明沒有線程/進程占用鎖,刪除鎖,并通知客戶端
如果鎖的引用計數大于0,則證明還有線程/進程在占用鎖,則重新設置鎖的過期時間

if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
    else
        redis.call('del', KEYS[1]); 
        redis.call('publish', KEYS[2], ARGV[1]);
        return 1;
end; 
    return nil;

鎖續期

如果同時有線程A和線程B在作業。
線程A先獲取到鎖,開始執行業務邏輯,但是線程A的業務邏輯因為種種原因導致在鎖的超時時間內沒有完成。如果沒有鎖續期機制,則會直接丟失鎖
這個時候,線程B獲取到鎖,線程B執行業務的過程中,線程A執行完畢,會嘗試釋放鎖。這個時候可能會將線程B持有的鎖釋放
為了解決這種問題,redisson引入了鎖續期機制

  • org.redisson.RedissonLock#renewExpirationAsync
    protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return commandExecutor.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.<Object>singletonList(getName()), 
            internalLockLeaseTime, getLockName(threadId));
    }
  • 鎖續期執行的lua腳本
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 1;
end;
return 0;

RedLock

上述加鎖、鎖續期、釋放鎖的過程應該可以滿足redis集群穩定的大部分場景的需求。

但是還有一種場景:獲取鎖之后,redis集群的master掛了,slaver變為新的master。由于redis的主從同步是異步的,無法保證slave節點的數據跟master的數據是完全一致的。
這個時候,如果某個線程加鎖成功,鎖的信息保存在master上,還未來得及同步到slave上。這個時候master掛了,slave變為新的master。就會出現鎖丟失的問題。等到老的master的恢復之后,會存在鎖被重復獲取的問題。

  • 為了解決上述問題,我們可以選擇RedLock的方案.Redisson正好就為我們實現的RedLock的整個過程。
    org.redisson.RedissonRedLock
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容