分散式鎖Redission

炒燜煎糖板栗發表於2021-10-12

Redisson 作為分散式鎖

官方文件:https://github.com/redisson/redisson/wiki

  1. 引入依賴

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

    2.配置redission

    @Configuration
    public class MyRedissonConfig {
        /**
         * 所有對 Redisson 的使用都是通過 RedissonClient
         *
         * @return
         * @throws IOException
         */
        @Bean(destroyMethod = "shutdown")
        public RedissonClient redisson() throws IOException {
            // 1、建立配置
            Config config = new Config();
            // Redis url should start with redis:// or rediss://
            config.useSingleServer().setAddress("redis://192.168.163.131:6379");
    
            // 2、根據 Config 建立出 RedissonClient 例項
            return Redisson.create(config);
        }
    }
    

    3.測試

        @Autowired
        RedissonClient redissonClient;
    
        @Test
        public  void redission()
        {
            System.out.println(redissonClient);
        }
    

    4.使用

     @ResponseBody
        @GetMapping("/hello")
        public  String hello()
        {
            // 1. 獲取一把鎖
            RLock lock = redisson.getLock("my-lock");
            // 2. 加鎖, 阻塞式等待
            lock.lock();
            try {
                System.out.println("加鎖成功,執行業務...");
                Thread.sleep(15000);
            } catch (Exception e) {
            } finally {
                // 3. 解鎖 假設解鎖程式碼沒有執行,Redisson 會出現死鎖嗎?(不會)
                System.out.println("釋放鎖"+Thread.currentThread().getId());
                lock.unlock();
            }
            return "hello";
        }
    

假設解鎖程式碼沒有執行,Redisson 會出現死鎖嗎?

不會

  • 鎖的自動續期,如果業務時間很長,執行期間自動給鎖續期 30 s,不用擔心業務時間過長,鎖自動過期被刪掉;
  • 加鎖的業務只要執行完成,就不會給當前鎖續期,即使不手動續期,預設也會在 30 s 後解鎖

原始碼分析-Redission如何解決死鎖

Ctrl+Alt檢視方法實現

image-20211006120452959

這是一個加鎖方法,不傳過期時間

    public void lock() {
        try {
            //這裡過期時間自動賦值成-1
            this.lock(-1L, (TimeUnit)null, false);
        } catch (InterruptedException var2) {
            throw new IllegalStateException();
        }
    }

然後會呼叫 this.lock(-1L, (TimeUnit)null, false)方法

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        //得到執行緒ID
        long threadId = Thread.currentThread().getId();
       //通過執行緒ID獲取到鎖
        Long ttl = this.tryAcquire(leaseTime, unit, threadId);
        //如果沒有獲取到鎖
        if (ttl != null) {
            RFuture<RedissonLockEntry> future = this.subscribe(threadId);
            this.commandExecutor.syncSubscription(future);

            try {
                while(true) {
                    ttl = this.tryAcquire(leaseTime, unit, threadId);
                    if (ttl == null) {
                        return;
                    }

                    if (ttl >= 0L) {
                        try {
                            this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                        } catch (InterruptedException var13) {
                            if (interruptibly) {
                                throw var13;
                            }

                            this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                        }
                    } else if (interruptibly) {
                        this.getEntry(threadId).getLatch().acquire();
                    } else {
                        this.getEntry(threadId).getLatch().acquireUninterruptibly();
                    }
                }
            } finally {
                this.unsubscribe(future, threadId);
            }
        }
    }

獲取鎖方法

    private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
        return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
    }

裡面又呼叫了tryAcquireAsync

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    //如果傳了過期時間    
    if (leaseTime != -1L) {
            return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } 
    //沒有傳過期時間
    else {
            RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
            ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining == null) {
                        this.scheduleExpirationRenewal(threadId);
                    }

                }
            });
            return ttlRemainingFuture;
        }
    }

有指定過期時間走tryLockInnerAsync方法,嘗試用非同步加鎖

    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        //先把時間轉換成internalLockLeaseTime
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        //然後執行lua指令碼 發給redis執行
        return this.commandExecutor.evalWriteAsync(this.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.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }

沒有指定過期時間呼叫getLockWatchdogTimeout()方法,獲取鎖的預設看門狗時間,30秒

public long getLockWatchdogTimeout() {
    return this.lockWatchdogTimeout;
}
this.lockWatchdogTimeout = 30000L;

還是呼叫tryLockInnerAsyncredis傳送命令,佔鎖成功返回一個以不變非同步編排的RFuture<Long>物件,來進行監聽,裡面有兩個引數ttlRemaining, e

 ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining == null) {
                        this.scheduleExpirationRenewal(threadId);
                    }

                }
            });

裡面有個scheduleExpirationRenewal方法

    private void scheduleExpirationRenewal(long threadId) {
        RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
        RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
        if (oldEntry != null) {
            oldEntry.addThreadId(threadId);
        } else {
            entry.addThreadId(threadId);
            //重新設定過期時間
            this.renewExpiration();
        }

    }

裡面的關鍵方法renewExpiration執行定時任務,

 private void renewExpiration() {
        RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
            //裡面會執行一個定時任務
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                            RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                            future.onComplete((res, e) -> {
                                if (e != null) {
                                    RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                                } else {
                                    if (res) {
                                        RedissonLock.this.renewExpiration();
                                    }

                                }
                            });
                        }
                    }
                }
                //看門狗時間/3 10秒鐘重試一次
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            ee.setTimeout(task);
        }
    }

主要是來執行renewExpirationAsync這個方法

 protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return this.commandExecutor.evalWriteAsync(this.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.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }

裡面傳入了一個internalLockLeaseTime時間引數

image-20211006163310509

又是獲取看門狗時間

總結

  • 如果傳了鎖的超時時間,就傳送給redis執行指令碼,進行佔鎖,預設超時就是我們指定的時間

  • 如果未指定鎖的超時時間,就是使用lockWatchdogTimeout的預設時間30秒,只要佔鎖成功就會啟動一個定時任務【重新給所設定時間,新的過期時間就是lockWatchdogTimeout的預設時間】

    最佳實踐使用自定義過期時間,省掉了自動續期時間,自動加鎖

讀寫鎖測試

@GetMapping("/write")
    @ResponseBody
    public  String writeValue()
    {
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        String s="";
        RLock rLock=readWriteLock.writeLock();
        try{
            //加寫鎖
            rLock.lock();
            s= UUID.randomUUID().toString();
            Thread.sleep(30000);
            redisTemplate.opsForValue().set("writeValue",s);
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            rLock.unlock();
        }
        return  s;
    }

    @GetMapping("/read")
    @ResponseBody
    public  String readValue()
    {
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        String s="";
        //加讀鎖
        RLock rLock=readWriteLock.readLock();
        rLock.lock();
        try{
            s=redisTemplate.opsForValue().get("writeValue");
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            rLock.unlock();
        }
        return  s;
    }

寫鎖沒釋放讀鎖就必須等待,沒有寫鎖讀鎖都可以讀

保證資料的一致性,寫鎖是一個排他鎖、互斥鎖,讀鎖是共享鎖。

讀讀共享、讀寫互斥、寫寫互斥、寫讀互斥,只要有寫的存在都必須等待

訊號量測試

像車庫停車,每進來一輛車,車庫減少一個車位,只有當車庫還有車位才可以停車

    @GetMapping("/park")
    @ResponseBody
    public  String park() throws InterruptedException {
        RSemaphore park = redisson.getSemaphore("park");
        //獲取一個訊號 佔一個值
        park.acquire();
        return  "ok";
    }

    @GetMapping("/go")
    @ResponseBody
    public  String go(){
        RSemaphore park = redisson.getSemaphore("park");
        //釋放一個車位
        park.release();
        return  "ok";
    }

訪問:

gulimall.com/park

gulimall.com/go

訊號量可以用作分散式的限流

閉鎖

只有等待所有活動都完成才發生,例如當所有班級放學走完才關閉學校大門

    @GetMapping("/lockdoor")
    @ResponseBody
    public  String lockDoor() throws InterruptedException {
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.trySetCount(5);
        door.await();//等待閉鎖都完成
        return  "放假啦....";
    }

    @GetMapping("/gogo/{id}")
    @ResponseBody
    public  String gogogo(@PathVariable("id") Long id) throws InterruptedException {
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.countDown();
        return  id+"班都走了";
    }

快取一致性解決

在我們讀快取的時候可能會有資料被修改過,為了讓我們能夠讀到最新的資料,有兩種處理方法:

雙寫模式

在把資料寫入資料庫的時候,同時寫入到快取中

問題:在寫的過程中,可能會在第一個執行緒快取還沒寫進,但是第二個查詢到快取又開始寫資料,讀到的最新資料有延遲,導致產生髒資料

失效模式

在把資料寫入資料更新的時候,把快取刪除,下次查詢沒有快取再新增快取

問題:線上程1更新資料的時候消耗大量時間,還沒刪快取,執行緒2進來也沒有快取,讀取到原來老的資料,然後更新快取

我們系統的一致性解決方案

1、快取的所有資料都有過期時間,資料過期下一次查詢觸發主動更新

2、讀寫資料的時候,加上分散式的讀寫鎖

3、遇到實時性、一致性要求高的資料,就應該查資料庫,即使慢點。

相關文章