首頁  >  文章  >  Java  >  分散式鎖中的王者方案 - Redisson

分散式鎖中的王者方案 - Redisson

Java后端技术全栈
Java后端技术全栈轉載
2023-08-24 15:31:191164瀏覽

分散式鎖中的王者方案 - Redisson

網路上有各種關於Redis實作分散式鎖定的方案,但何為王者方案?

答案就: Redisson

我們先來看看 Redis 官網對分散式鎖定的說法:

分散式鎖中的王者方案 - Redisson

#而 Java 版的 分散式鎖定的框架就是 Redisson。

這篇實戰內容將會以我的開源專案 PassJava 為基礎來整合 Redisson。

我把後端前端小程式都上傳到同一個倉庫裡面了,大家可以透過Github碼雲存取。網址如下:

Github: https://github.com/Jackson0714/PassJava-Platform

碼雲#:https: //gitee.com/jayh2018/PassJava-Platform

配套教學:www.passjava.cn

在實戰前,我們先來看看使用Redisson 的原理。

一、Redisson 是什麼?

如果你之前是在用 Redis 的話,那使用 Redisson 的話將會事半功倍,Redisson 提供了使用 Redis的最簡單和最便捷的方法。

Redisson的宗旨是促進使用者對 Redis 的關注分離(Separation of Concern),從而讓使用者能夠將精力更集中地放在處理業務邏輯上。

Redisson 是一個在 Redis 的基礎上實作的 Java 駐記憶體資料網格(In-Memory Data Grid)。

分散式鎖中的王者方案 - Redisson
  • Netty 框架:Redisson採用了基於NIO的Netty框架,不僅能作為Redis底層驅動客戶端,具備提供對Redis各種組態形式的連接功能,對Redis命令能以同步發送、非同步形式發送、非同步流形式發送或管道形式發送的功能,LUA腳本執行處理,以及處理返回結果的功能

  • 基礎資料結構:將原生的Redis HashListSetStringGeoHyperLogLog等資料結構封裝為Java里大家最熟悉的映射(Map)列表(List)集(Set)通用物件桶(Object Bucket)地理空間物件桶(Geospatial Bucket)基數估計演算法(HyperLogLog)等結構,

  • #分散式資料結構:這基礎上也提供了分佈式的多值映射(Multimap),本地快取映射(LocalCachedMap),有序集(SortedSet),計分排序集(ScoredSortedSet),字典排序集(LexSortedSet),列隊(Queue),阻塞佇列(Blocking Queue),有界阻塞列隊(Bounded Blocking Queue),雙端隊列(Deque),阻塞雙端列隊(Blocking Deque),阻塞公平列隊(Blocking Fair Queue),延遲列隊(Delayed Queue),布隆過濾器(Bloom Filter) ,原子整長形(Ato​​micLong),原子雙精確度浮點數(AtomicDouble),BitSet等Redis原本沒有的分散式資料結構。

  • 分散式鎖定:Redisson也實作了Redis文件中提到像是分散式鎖定Lock這樣的更高階應用場景。事實上Redisson並沒有不只步於此,在分散式鎖的基礎上還提供了聯鎖(MultiLock)讀寫鎖(ReadWriteLock)公平鎖(Fair Lock)紅鎖(RedLock)信號量(Semaphore)可過期性訊號量(PermitExpirableSemaphore)閉鎖(CountDownLatch)這些實際當中對多執行緒高並發應用至關重要的基本元件。正是透過實現基於Redis的高階應用方案,使Redisson成為建構分散式系統的重要工具。

  • 節點:Redisson作為獨立節點可以用於獨立執行其他節點發佈到分散式執行服務分散式調度服務裡的遠端任務。

二、整合Redisson

#Spring Boot 整合Redisson 有兩種方案:

  • 程式化配置。
  • 檔案方式配置。

本篇介紹如何用程式化的方式整合 Redisson。

2.1 引入 Maven 依賴

在 passjava-question 微服務的 pom.xml 引入 redisson的 maven 依賴。

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.15.5</version>
</dependency>

2.2 自訂設定類別

下面的程式碼是單節點 Redis 的設定。

@Configuration
public class MyRedissonConfig {
    /**
     * 对 Redisson 的使用都是通过 RedissonClient 对象
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown") // 服务停止后调用 shutdown 方法。
    public RedissonClient redisson() throws IOException {
        // 1.创建配置
        Config config = new Config();
        // 集群模式
        // config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001");
        // 2.根据 Config 创建出 RedissonClient 示例。
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
}

2.3 測試設定類別

新建一個單元測試方法。

@Autowired
RedissonClient redissonClient;

@Test
public void TestRedisson() {
    System.out.println(redissonClient);
}

我們執行這個測試方法,印出redissonClient

org.redisson.Redisson@77f66138

三、分散式可重入鎖定

3.1 可重入鎖定測試

基於Redis的Redisson分散式可重入鎖定RLockJava 物件實作了java.util.concurrent.locks.Lock介面。同時也提供了非同步(Async)、反射式(Reactive)和RxJava2標準的介面。

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

我們用passjava 這個開源專案測試下可重入鎖的兩個點:

  • (1)多個執行緒搶佔鎖,後面鎖需要等待嗎?
  • (2)如果搶占到鎖的執行緒所在的服務停了,鎖會不會被釋放?

3.1.1 驗證一:可重入鎖定是阻塞的嗎?

為了驗證以上兩點,我寫了個demo 程式:程式碼的流程就是設定WuKong-lock鎖,然後加鎖,列印線程ID,等待10 秒後釋放鎖,最後回傳回應:「test lock ok」。

@ResponseBody
@GetMapping("test-lock")
public String TestLock() {
    // 1.获取锁,只要锁的名字一样,获取到的锁就是同一把锁。
    RLock lock = redisson.getLock("WuKong-lock");

    // 2.加锁
    lock.lock();
    try {
        System.out.println("加锁成功,执行后续代码。线程 ID:" + Thread.currentThread().getId());
        Thread.sleep(10000);
    } catch (Exception e) {
        //TODO
    } finally {
        lock.unlock();
        // 3.解锁
        System.out.println("Finally,释放锁成功。线程 ID:" + Thread.currentThread().getId());
    }

    return "test lock ok";
}

先驗證第一個點,用兩個 http 請求來測試搶佔鎖定。

請求的 URL:

http://localhost:11000/question/v1/redisson/test/test-lock
分散式鎖中的王者方案 - Redisson

第一个线程对应的线程 ID 为 86,10秒后,释放锁。在这期间,第二个线程需要等待锁释放。

第一个线程释放锁之后,第二个线程获取到了锁,10 秒后,释放锁。

画了一个流程图,帮助大家理解。如下图所示:

分散式鎖中的王者方案 - Redisson
  • 第一步:线程 A 在 0 秒时,抢占到锁,0.1 秒后,开始执行等待 10 s。
  • 第二步:线程 B 在 0.1 秒尝试抢占锁,未能抢到锁(被 A 抢占了)。
  • 第三步:线程 A 在 10.1 秒后,释放锁。
  • 第四步:线程 B 在 10.1 秒后抢占到锁,然后等待 10 秒后释放锁。

由此可以得出结论,Redisson 的可重入锁(lock)是阻塞其他线程的,需要等待其他线程释放的。

3.1.2 验证二:服务停了,锁会释放吗?

如果线程 A 在等待的过程中,服务突然停了,那么锁会释放吗?如果不释放的话,就会成为死锁,阻塞了其他线程获取锁。

我们先来看下线程 A 的获取锁后的,Redis 客户端查询到的结果,如下图所示:

分散式鎖中的王者方案 - Redisson

WuKong-lock 有值,而且大家可以看到 TTL 在不断变小,说明 WuKong-lock 是自带过期时间的。

通过观察,经过 30 秒后,WuKong-lock 过期消失了。说明 Redisson 在停机后,占用的锁会自动释放。

分散式鎖中的王者方案 - Redisson

那这又是什么原理呢?这里就要提一个概念了,看门狗

分散式鎖中的王者方案 - Redisson

3.2 看门狗原理

如果负责储存这个分布式锁的 Redisson 节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。

默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

如果我们未制定 lock 的超时时间,就使用 30 秒作为看门狗的默认时间。只要占锁成功,就会启动一个定时任务:每隔 10 秒重新给锁设置过期的时间,过期时间为 30 秒。

如下图所示:

分散式鎖中的王者方案 - Redisson
看门狗原理图-1

当服务器宕机后,因为锁的有效期是 30 秒,所以会在 30 秒内自动解锁。(30秒等于宕机之前的锁占用时间+后续锁占用的时间)。

如下图所示:

分散式鎖中的王者方案 - Redisson
看门狗原理图-2

3.3 设置锁过期时间

我们也可以通过给锁设置过期时间,让其自动解锁。

如下所示,设置锁 8 秒后自动过期。

lock.lock(8, TimeUnit.SECONDS);

如果业务执行时间超过 8 秒,手动释放锁将会报错,如下图所示:

分散式鎖中的王者方案 - Redisson

所以我们如果设置了锁的自动过期时间,则执行业务的时间一定要小于锁的自动过期时间,否则就会报错。

四、王者方案

上一篇我讲解了分布式锁的五种方案:Redis 分布式锁|从青铜到钻石的五种演进方案,这一篇主要是讲解如何用 Redisson 在 Spring Boot 项目中实现分布式锁的方案。

因为 Redisson 非常强大,实现分布式锁的方案非常简洁,所以称作王者方案

原理图如下:

分散式鎖中的王者方案 - Redisson

代码如下所示:

// 1.设置分布式锁
RLock lock = redisson.getLock("lock");
// 2.占用锁
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();

和之前 Redis 的方案相比,简洁很多。

下面讲解下 Redisson 的其他几种分布式锁,相信大家在以后的项目中也会用到。

五、分布式读写锁

基于 Redis 的 Redisson 分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了 RLock接口。

写锁是一个排他锁(互斥锁),读锁是一个共享锁。

  • 读锁 + 读锁:相当于没加锁,可以并发读。
  • 读锁 + 写锁:写锁需要等待读锁释放锁。
  • 写锁 + 写锁:互斥,需要等待对方的锁释放。
  • 写锁 + 读锁:读锁需要等待写锁释放。
分散式鎖中的王者方案 - Redisson

示例代码如下:

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

六、分布式信号量

基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

关于信号量的使用大家可以想象一下这个场景,有三个停车位,当三个停车位满了后,其他车就不停了。可以把车位比作信号,现在有三个信号,停一次车,用掉一个信号,车离开就是释放一个信号。

分散式鎖中的王者方案 - Redisson

我们用 Redisson 来演示上述停车位的场景。

先定义一个占用停车位的方法:

/**
* 停车,占用停车位
* 总共 3 个车位
*/
@ResponseBody
@RequestMapping("park")
public String park() throws InterruptedException {
  // 获取信号量(停车场)
  RSemaphore park = redisson.getSemaphore("park");
  // 获取一个信号(停车位)
  park.acquire();

  return "OK";
}

再定义一个离开车位的方法:

/**
 * 释放车位
 * 总共 3 个车位
 */
@ResponseBody
@RequestMapping("leave")
public String leave() throws InterruptedException {
    // 获取信号量(停车场)
    RSemaphore park = redisson.getSemaphore("park");
    // 释放一个信号(停车位)
    park.release();

    return "OK";
}

为了简便,我用 Redis 客户端添加了一个 key:“park”,值等于 3,代表信号量为 park,总共有三个值。

分散式鎖中的王者方案 - Redisson

然后用 postman 发送 park 请求占用一个停车位。

分散式鎖中的王者方案 - Redisson

然後在 redis 用戶端查看 park 的值,發現已經改為 2 了。繼續呼叫兩次,發現 park 的等於 0,當呼叫第四次的時候,會發現請求一直處於等待中,說明車位不夠了。如果想要不阻塞,可以用 tryAcquire 或 tryAcquireAsync。

我們再來呼叫離開車位的方法,park 的值變成 1,代表車位剩餘 1 個。

注意:多次執行釋放信號量操作,剩餘訊號量會一直增加,而不是到 3 後就封頂了。

其他分散式鎖定:

  • 公平鎖定(Fair Lock)

  • 聯鎖(MultiLock)

  • 紅鎖(RedLock)

  • 讀寫鎖定( ReadWriteLock)

  • 可過期性訊號量(PermitExpirableSemaphore)

  • #閉鎖(CountDownLatch)

分散式鎖中的王者方案 - Redisson

還有其他分散式鎖定就不在本篇展開了,有興趣的同學可以查看官方文件。

#

以上是分散式鎖中的王者方案 - Redisson的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:Java后端技术全栈。如有侵權,請聯絡admin@php.cn刪除