什麼是可重入鎖?詳解redis實現分散式重入鎖的方法

2022-01-28 10:00:17
什麼是可重入鎖?怎麼實現重入鎖?下面本篇文章就來帶大家深入聊聊redis實現分散式重入鎖的方法,希望對大家有所幫助!

什麼是不可重入鎖?

即若當前執行緒執行某個方法已經獲取了該鎖,那麼在方法中嘗試再次獲取鎖時,就會獲取不到而阻塞。

什麼是可重入鎖?

可重入鎖,也叫做遞迴鎖,指的是在同一執行緒內,外層函數獲得鎖之後,內層遞迴函數仍然可以獲取到該鎖。 就是同一個執行緒再次進入同樣程式碼時,可以再次拿到該鎖。

可重入鎖作用?

防止在同一執行緒中多次獲取鎖而導致死鎖發生。

注:在java的程式設計中synchronized 和 ReentrantLock都是可重入鎖。

基於synchronized的可重入鎖

步驟1:雙重加鎖邏輯

public class SynchronizedDemo {
    //模擬庫存100
    int count=100;
    public synchronized void operation(){
        log.info("第一層鎖:減庫存");
        //模擬減庫存
        count--;
        add();
        log.info("下訂單結束庫存剩餘:{}",count);
    }

    private synchronized void add(){
        log.info("第二層鎖:插入訂單");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

步驟2:加個測試類

public static void main(String[] args) {
    SynchronizedDemo synchronizedDemo=new SynchronizedDemo();
    for (int i = 0; i < 3; i++) {
        int finalI = i;
        new Thread(()->{
            log.info("-------使用者{}開始下單--------", finalI);
            synchronizedDemo.operation();
        }).start();
    }
}

步驟3:測試

20:44:04.013 [Thread-2] INFO com.agan.redis.controller.SynchronizedController - -------使用者2開始下單--------
20:44:04.013 [Thread-1] INFO com.agan.redis.controller.SynchronizedController - -------使用者1開始下單--------
20:44:04.013 [Thread-0] INFO com.agan.redis.controller.SynchronizedController - -------使用者0開始下單--------
20:44:04.016 [Thread-2] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第一層鎖:減庫存
20:44:04.016 [Thread-2] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第二層鎖:插入訂單
20:44:14.017 [Thread-2] INFO com.agan.redis.Reentrant.SynchronizedDemo - 下訂單結束庫存剩餘:99
20:44:14.017 [Thread-0] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第一層鎖:減庫存
20:44:14.017 [Thread-0] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第二層鎖:插入訂單
20:44:24.017 [Thread-0] INFO com.agan.redis.Reentrant.SynchronizedDemo - 下訂單結束庫存剩餘:98
20:44:24.017 [Thread-1] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第一層鎖:減庫存
20:44:24.017 [Thread-1] INFO com.agan.redis.Reentrant.SynchronizedDemo - 第二層鎖:插入訂單
20:44:34.017 [Thread-1] INFO com.agan.redis.Reentrant.SynchronizedDemo - 下訂單結束庫存剩餘:97
  • 由於synchronized關鍵字修飾的是方法,所有加鎖為範例物件:synchronizedDemo
  • 執行結果可以看出減庫存和插入訂單都是每個執行緒都完整執行兩個方法完畢,才能釋放鎖,其他執行緒才能拿鎖,即是一個執行緒多次可以拿到同一個鎖,可重入。所以synchronized也是可重入鎖。

基於ReentrantLock的可重入鎖

ReentrantLock,是一個可重入且獨佔式的鎖,是一種遞迴無阻塞的同步鎖。和synchronized關鍵字相比,它更靈活、更強大,增加了輪詢、超時、中斷等高階功能。

步驟1:雙重加鎖邏輯

public class ReentrantLockDemo {

    private Lock lock =  new ReentrantLock();

    public void doSomething(int n){
        try{
            //進入遞迴第一件事:加鎖
            lock.lock();
            log.info("--------遞迴{}次--------",n);
            if(n<=2){
                try {
                    Thread.sleep(1000*2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.doSomething(++n);
            }else{
                return;
            }
        }finally {
            lock.unlock();
        }
    }

}

步驟2:加個測試類

public static void main(String[] args) {
    ReentrantLockDemo reentrantLockDemo=new ReentrantLockDemo();
    for (int i = 0; i < 3; i++) {
        int finalI = i;
        new Thread(()->{
            log.info("-------使用者{}開始下單--------", finalI);
            reentrantLockDemo.doSomething(1);
        }).start();
    }
}

步驟3:測試

20:55:23.533 [Thread-1] INFO com.agan.redis.controller.ReentrantController - -------使用者1開始下單--------
20:55:23.533 [Thread-2] INFO com.agan.redis.controller.ReentrantController - -------使用者2開始下單--------
20:55:23.533 [Thread-0] INFO com.agan.redis.controller.ReentrantController - -------使用者0開始下單--------
20:55:23.536 [Thread-1] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------遞迴1次--------
20:55:25.537 [Thread-1] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------遞迴2次--------
20:55:27.538 [Thread-1] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------遞迴3次--------
20:55:27.538 [Thread-2] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------遞迴1次--------
20:55:29.538 [Thread-2] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------遞迴2次--------
20:55:31.539 [Thread-2] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------遞迴3次--------
20:55:31.539 [Thread-0] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------遞迴1次--------
20:55:33.539 [Thread-0] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------遞迴2次--------
20:55:35.540 [Thread-0] INFO com.agan.redis.Reentrant.ReentrantLockDemo - --------遞迴3次--------
  • 執行結果可以看出,每個執行緒都可以多次加鎖解鎖的,ReentrantLock是可重入的。

redis如何實現分散式重入鎖?

setnx雖然可以實現分散式鎖,但是不可重入,在一些複雜的業務場景,我們需要分散式重入鎖時, 對於redis的重入鎖業界還是有很多解決方案的,目前最流行的就是採用Redisson。【相關推薦:Redis視訊教學

什麼是Redisson?

  • Redisson是Redis官方推薦的Java版的Redis使用者端。
  • 基於Java實用工具包中常用介面,為使用者提供了一系列具有分散式特性的常用工具類。
  • 在網路通訊上是基於NIO的Netty框架,保證網路通訊的高效能。
  • 在分散式鎖的功能上,它提供了一系列的分散式鎖;如:
    • 可重入鎖(Reentrant Lock)
    • 公平鎖(Fair Lock)
    • 非公平鎖(unFair Lock)
    • 讀寫鎖(ReadWriteLock)
    • 聯鎖(MultiLock)
    • 紅鎖(RedLock)

案例實戰:體驗redis分散式重入鎖

步驟1:Redisson設定

Redisson設定的可以查考:redis分散式快取(三十四)一一 SpringBoot整合Redission - 掘金 (juejin.cn)

https://juejin.cn/post/7057132897819426824

步驟2:Redisson重入鎖測試類

public class RedisController {

    @Autowired
    RedissonClient redissonClient;

    @GetMapping(value = "/lock")
    public void get(String key) throws InterruptedException {
        this.getLock(key, 1);
    }

    private void getLock(String key, int n) throws InterruptedException {
        //模擬遞迴,3次遞迴後退出
        if (n > 3) {
            return;
        }
        //步驟1:獲取一個分散式可重入鎖RLock
        //分散式可重入鎖RLock :實現了java.util.concurrent.locks.Lock介面,同時還支援自動過期解鎖。
        RLock lock = redissonClient.getLock(key);
        //步驟2:嘗試拿鎖
        // 1. 預設的拿鎖
        //lock.tryLock();
        // 2. 支援過期解鎖功能,10秒鐘以後過期自動解鎖, 無需呼叫unlock方法手動解鎖
        //lock.tryLock(10, TimeUnit.SECONDS);
        // 3. 嘗試加鎖,最多等待3秒,上鎖以後10秒後過期自動解鎖
        // lock.tryLock(3, 10, TimeUnit.SECONDS);
        boolean bs = lock.tryLock(3, 10, TimeUnit.SECONDS);
        if (bs) {
            try {
                // 業務程式碼
                log.info("執行緒{}業務邏輯處理: {},遞迴{}" ,Thread.currentThread().getName(), key,n);
                //模擬處理業務
                Thread.sleep(1000 * 5);
                //模擬進入遞迴
                this.getLock(key, ++n);
            } catch (Exception e) {
                log.error(e.getLocalizedMessage());
            } finally {
                //步驟3:解鎖
                lock.unlock();
                log.info("執行緒{}解鎖退出",Thread.currentThread().getName());
            }
        } else {
            log.info("執行緒{}未取得鎖",Thread.currentThread().getName());
        }
    }
}

RLock三個加鎖動作:

    1. 預設的拿鎖
    • lock.tryLock();
    1. 支援過期解鎖功能,10秒鐘以後過期自動解鎖
    • lock.tryLock(10, TimeUnit.SECONDS);
    1. 嘗試加鎖,最多等待3秒,上鎖以後10秒後過期自動解鎖
    • lock.tryLock(3, 10, TimeUnit.SECONDS);

區別:

  • lock.lock():阻塞式等待。預設加的鎖都是30s
    • 鎖的自動續期,如果業務超長,執行期間自動鎖上新的30s。不用擔心業務時間長而導致鎖自動過期被刪掉(預設續期)
    • 加鎖的業務只要執行完成,就不會給當前鎖續期,即使不手動解鎖,鎖預設會在30s內自動過期,不會產生死鎖問題
    • lock()如果我們未指定鎖的超時時間,就使用【看門狗預設時間】: lockWatchdogTimeout = 30 * 1000
    • 原理:只要佔鎖成功,就會啟動一個定時任務【重新給鎖設定過期時間,新的過期時間就是看門狗的預設時間】,每隔10秒都會自動的再次續期,續成30秒
  • lock.lock(10,TimeUnit.SECONDS) :10秒鐘自動解鎖,自動解鎖時間一定要大於業務執行時間
    • 出現的問題:在鎖時間到了以後,不會自動續期
    • 原理:lock(10,TimeUnit.SECONDS)如果我們傳遞了鎖的超時時間,就傳送給redis執行指令碼,進行佔鎖,預設超時就是我們制定的時間

最佳實戰:

  • lock.lock(10,TimeUnit.SECONDS); 省掉看門狗續期操作,自動解鎖時間一定要大於業務執行時間,手動解鎖

步驟3:測試

存取3次:http://127.0.0.1:9090/lock?key=ljw

執行緒http-nio-9090-exec-1業務邏輯處理: ljw,遞迴1
執行緒http-nio-9090-exec-2未取得鎖
執行緒http-nio-9090-exec-1業務邏輯處理: ljw,遞迴2
執行緒http-nio-9090-exec-3未取得鎖
執行緒http-nio-9090-exec-1業務邏輯處理: ljw,遞迴3
執行緒http-nio-9090-exec-1解鎖退出
執行緒http-nio-9090-exec-1解鎖退出
執行緒http-nio-9090-exec-1解鎖退出

通過測試結果:

  • nio-9090-exec-1執行緒,在getLock方法遞迴了3次,即證明了lock.tryLock是可重入鎖
  • 只有當nio-9090-exec-1執行緒執行完後,io-9090-exec-2 nio-9090-exec-3 未取得鎖 因為lock.tryLock(3, 10, TimeUnit.SECONDS),嘗試加鎖,最多等待3秒,上鎖以後10秒後過期自動解鎖 所以等了3秒都等不到,就放棄了

總結

上面介紹了分散式重入鎖的相關知識,證明了Redisson工具能實現了可重入鎖的功能。其實Redisson工具包中還包含了讀寫鎖(ReadWriteLock)和 紅鎖(RedLock)等相關功能,我們下篇文章再詳細研究。

更多程式設計相關知識,請存取:!!

以上就是什麼是可重入鎖?詳解redis實現分散式重入鎖的方法的詳細內容,更多請關注TW511.COM其它相關文章!