Redisson - 可重入鎖ReentantLock的使用與原理


簡介

關於 Redisson 的具體介紹可點擊 這里,簡單來說就是將 JUC 和 Redis 結合起來,使其可以實現多機器多線程同步的功能,Redisson 有很多組件,這篇主要介紹可重入鎖 —— ReentantLock。

環境准備

添加 Maven 依賴

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.0</version>
</dependency>

添加配置類

@Configuration
public class MyRedissonConfig {
    @Bean(destroyMethod = "shutdown")
    RedissonClient redisson() throws IOException {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.56.10:6379");
        return Redisson.create(config);
    }
}

基本使用代碼如下:

@GetMapping("/hello")
@ResponseBody
public String hello() {
    //獲取Lock鎖,設置鎖的名稱
    RLock lock = redisson.getLock("my-lock");
    //開啟
    lock.lock();
    try {
        System.out.println("上鎖:" + Thread.currentThread().getId());
        //模擬業務處理20秒
        TimeUnit.SECONDS.sleep(20);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        System.out.println("解鎖:" + Thread.currentThread().getId());
        //釋放
        lock.unlock();
    }
    return "hello";
}

分析

當我們發送 /hello 請求后等待 20 秒得到響應結果,會在 Redis 中存儲鎖的信息(如下圖所示),期間,其它用戶發送 /hello 請求時會被阻塞,只有前一個請求結束后釋放鎖,當前請求才會進入。

思考1:如果在業務處理過程中程序突然終止,鎖沒有得到釋放,是否會一直阻塞下去?

經過實驗,在業務處理的20秒中,將服務手動停止,刷新 Redis 中 my-lock 的信息,發現 TTL 不斷的減小,直到失效,再發送其它請求能夠正常執行,這說明,即使不釋放鎖,鎖的有效時間到了也會自動釋放。源碼如下:

//獲取當前線程id
long threadId = Thread.currentThread().getId();
//獲取此線程的鎖
Long ttl = tryAcquire(leaseTime, unit, threadId);
//如果獲取不到,則說明鎖已經釋放了,直接返回
if (ttl == null) {
    return;
}
while (true) {
    ttl = tryAcquire(leaseTime, unit, threadId);
    //和上面一樣,判斷是否能獲取到鎖
    if (ttl == null) {
        break;
    }
    ...
}

思考2:過期時間是多少?如果我們的業務處理時間超過了過期時間,豈不是還沒處理完就把鎖的信息給刪了?

正常啟動服務訪問 /hello,刷新 my-lock 的信息,我們發現,TTL 每次減少到 20 就再次變為 30,直到業務處理完成,my-lock 被刪除。查找相關源代碼如下:

while (true) {
    //嘗試獲取鎖
    ttl = tryAcquire(leaseTime, unit, threadId);
    //如果獲取不到,說明執行該線程執行結束,就終止循環
    if (ttl == null) {
        break;
    }

    //如果獲取到了就繼續循環
    if (ttl >= 0) {
        try {
            future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            if (interruptibly) {
                throw e;
            }
            future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
        }
    } else {
        if (interruptibly) {
            future.getNow().getLatch().acquire();
        } else {
            future.getNow().getLatch().acquireUninterruptibly();
        }
    }
}

繼續深入源碼可以看到,如果不指定鎖的時間,就默認為 30 秒,它有一個好聽的名字:看門狗

private long lockWatchdogTimeout = 30 * 1000;

只要占領鎖,就會啟動一個定時任務:每隔一段時間重新給鎖設置過期時間

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));
    //internalLockLeaseTime就是看門狗的時間
}

每隔多長久刷新一下呢?

//獲取看門狗的時間,賦值給自己
this.internalLockLeaseTime = xxx.getLockWatchdogTimeout();
public long getLockWatchdogTimeout() {
    return lockWatchdogTimeout;
}

Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
    @Override
    public void run(Timeout timeout) throws Exception {
        ...
    }
    //使用的時候除3,也就是10秒刷新一次
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

思考三:如何自定義過期時間?

lock() 方法還有一個重載方法,可以傳入過期時間和單位

void lock(long leaseTime, TimeUnit unit);

我們將之前的代碼修改,設置為 15 秒,重啟服務再測試

lock.lock(15, TimeUnit.SECONDS);

訪問 /hello,刷新 Redis 中 my-lock 的信息會發現,TTL 從 15 減到 0,然后鎖信息過期,並不會出現之前的 10秒一刷新,查看源碼:

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    //如果傳入了過期時間,則直接執行tryLockInnerAsync里面的Lua腳本
    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;
        }
        if (ttlRemaining == null) {
            //刷新過期時間
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

總結

1、Reentrant Lock 對其它線程是阻塞的

2、為了解決死鎖的問題,Redisson 內部提供了一個監控鎖的看門狗,只要 Redisson 實例沒被關閉就不斷延長鎖的有效時間,默認情況下,看門狗的檢查鎖的超時時間是 30 秒,檢查時間是 10 秒(超時時間的三分之一),可以通過 setLockWatchdogTimeout 設置(只適用於未指定鎖的時間的情況)

3、如果指定鎖的時間,到達指定時間會自動解鎖,因此設置的時間必須大於業務正常執行的時間,否則,業務沒執行完,鎖就會被釋放

4、推薦使用指定時間的方式,省掉了續期操作,但需要合理設置過期時間,不能使鎖過早釋放


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM