使用Redisson實現可重入分布式鎖

前言

主流的分布式鎖一般有三種實現方式:

  1. 數據庫樂觀鎖

  2. 基于Redis的分布式鎖

  3. 基于ZooKeeper的分布式鎖

之前我在博客上寫過關于mysql和redis實現分布式鎖的具體方案: https://www.cnblogs.com/wang-meng/p/10226618.html 里面主要是從實現原理出發。

這次【分布式鎖】系列文章主要是深入redis客戶端reddision源碼和zk 這兩種分布式鎖的實現原理。

可靠性

首先,為了確保分布式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:

  1. 互斥性。在任意時刻,只有一個客戶端能持有鎖。

  2. 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證后續其他客戶端能加鎖。

  3. 具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。

  4. 解鈴還須系鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。

Redisson加鎖原理

redisson是一個非常強大的開源的redis客戶端框架, 官方地址: https://redisson.org/

使用起來很簡單,配置好maven和連接信息,這里直接看代碼實現:

RLock lock = redisson.getLock("anyLock");

lock.lock();
lock.unlock();

redisson具體的執行加鎖邏輯都是通過lua腳本來完成的,lua腳本能夠保證原子性。

先看下RLock初始化的代碼:

public class Redisson implements RedissonClient {

    @Override
    public RLock getLock(String name) {
        return new RedissonLock(connectionManager.getCommandExecutor(), name);
    }
}

public class RedissonLock extends RedissonExpirable implements RLock {
    public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
    super(commandExecutor, name);
    this.commandExecutor = commandExecutor;
    this.id = commandExecutor.getConnectionManager().getId();
    this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
    this.entryName = id + ":" + name;
}

首先看下RedissonLock 的id返回的是一個UUID對象,每個機器都對應一個自己的id屬性,id 值就類似于:"8743c9c0-0795-4907-87fd-6c719a6b4586"

接著往后看lock()的代碼實現:

public class RedissonLock extends RedissonExpirable implements RLock {
    @Override
    public void lock() {
        try {
            lockInterruptibly();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        lockInterruptibly(-1, null);
    }

    @Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) 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) {
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
    }

    <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));
    }
}

這里省略了一些中間代碼,這里主要看tryAcquire() 方法,這里傳遞的過期時間為-1,然后就是當前的線程id,接著就是核心的lua腳本執行流程,我們來一步步看看是如何執行的:

"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; " +

KEYS[1] 參數是:“anyLock”
ARGV[2] 是:“id + ":" + threadId”

首先用的exists 判斷redis中是否存在當前key,如果不存在就等于0,然后執行hset指令,將“anyLock id:threadId 1”存儲到redis中,最終redis存儲的數據類似于:

{
  "8743c9c0-0795-4907-87fd-6c719a6b4586:1":1
}

偷偷說一句,最后面的一個1 是為了后面可重入做的計數統計,后面會有講解到。

接著往下看,然后使用pexpire設置過期時間,默認使用internalLockLeaseTime為30s。最后返回為null,即時加鎖成功。

Redisson 可重入原理

我們看下鎖key存在的情況下,同一個機器同一個線程如何加鎖的?

"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]);",

ARGV[2] 是:“id + ":" + threadId”
如果同一個機器同一個線程再次來請求,這里就會是1,然后執行hincrby, hset設置的value+1 變成了2,然后繼續設置過期時間。

同理,一個線程重入后,解鎖時value - 1

Redisson watchDog原理

如果一個場景:現在有A,B在執行業務,A加了分布式鎖,但是生產環境是各種變化的,如果萬一A鎖超時了,但是A的業務還在跑。而這時由于A鎖超時釋放,B拿到鎖,B執行業務邏輯。這樣分布式鎖就失去了意義?

所以Redisson 引入了watch dog的概念,當A獲取到鎖執行后,如果鎖沒過期,有個后臺線程會自動延長鎖的過期時間,防止因為業務沒有執行完而鎖過期的情況。

我們接著來看看具體實現:

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final 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.addListener(new FutureListener<Long>() {
        @Override
        public void operationComplete(Future<Long> future) throws Exception {
            if (!future.isSuccess()) {
                return;
            }

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

當我們tryLockInnerAsync執行完之后,會添加一個監聽器,看看監聽器中的具體實現:

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));
}

這里面調度任務每隔10s鐘執行一次,lua腳本中是續約過期時間,使得當前線程持有的鎖不會因為過期時間到了而失效

image

Redisson 互斥性原理
還是看上面執行加鎖的lua腳本,最后會執行到:

"return redis.call('pttl', KEYS[1]);",

返回鎖還有多久時間過期,我們繼續接著看代碼:

@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // 返回ttl說明加鎖成功,不為空則是加鎖失敗
    if (ttl == null) {
        return;
    }

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

    try {
        // 死循環去嘗試獲取鎖
        while (true) {
            // 再次嘗試加鎖
            ttl = tryAcquire(leaseTime, unit, threadId);
            // 如果ttl=null說明搶占鎖成功
            if (ttl == null) {
                break;
            }

            // ttl 大于0,搶占鎖失敗,這個里面涉及到Semaphore,后續會講解
            if (ttl >= 0) {
                getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                getEntry(threadId).getLatch().acquire();
            }
        }
    } finally {
        unsubscribe(future, threadId);
    }
}

Redisson鎖釋放原理
直接看lua代碼:

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        // 判斷鎖key值是否存在
        "if (redis.call('exists', KEYS[1]) == 0) then " +
            "redis.call('publish', KEYS[2], ARGV[1]); " +
            "return 1; " +
        "end;" +
        // 判斷當前機器、當前線程id對應的key是否存在
        "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
            "return nil;" +
        "end; " +
        // 計數器數量-1 可重入鎖
        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
        // 如果計數器大于0,說明還在持有鎖
        "if (counter > 0) then " +
            "redis.call('pexpire', KEYS[1], ARGV[2]); " +
            "return 0; " +
        "else " +
            // 使用del指令刪除key
            "redis.call('del', KEYS[1]); " +
            "redis.call('publish', KEYS[2], ARGV[1]); " +
            "return 1; "+
        "end; " +
        "return nil;",
        Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}

總結
一圖總結:

image
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容