Spring Boot Redis 實現分散式鎖,真香!!

Java技術棧發表於2020-07-15

之前看很多人手寫分散式鎖,其實 Spring Boot 現在已經做的足夠好了,開箱即用,支援主流的 Redis、Zookeeper 中介軟體,另外還支援 JDBC。

本篇棧長以 Redis 為例(這也是用得最多的方案),教大家如何利用 Spring Boot 整合 Redis 實現快取,如何簡單、快速實現 Redis 分散式鎖。

分散式鎖介紹

Spring Boot 實現 Redis 分散式鎖在 spring-integration 這個專案中,參考:

https://docs.spring.io/spring-integration/docs/5.3.1.RELEASE/reference/html/redis.html#redis-lock-registry

首先來看下 LockRegistry 鎖註冊介面的所有實現類結構圖:

DefaultLockRegistry 就是純單機的可重入鎖,PassThruLockRegistry 是一個空實現類,也都沒有什麼利用價值。

Spring Integration 4.0 引入了基於 Redis 的分散式鎖:RedisLockRegistry,並且從 5.0 開始實現了 ExpirableLockRegistry 介面,用來移除超時且沒有用的鎖。

分散式鎖實戰

新增依賴

上面提到 Spring Boot 實現 Redis 分散式鎖在 spring-integration 這個專案中,所以需要這三個依賴:

  • spring-boot-starter-data-redis
  • spring-boot-starter-integration
  • spring-integration-redis
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.integration</groupId>
	<artifactId>spring-integration-redis</artifactId>
</dependency>

Spring Boot 基礎知識就不介紹了,不熟悉的可以關注公眾號Java技術棧,在後臺回覆:boot,可以閱讀我寫的歷史實戰教程。

配置分散式鎖

@Bean(destroyMethod = "destroy")
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
    return new RedisLockRegistry(redisConnectionFactory, "lock");
}

使用示例

@GetMapping("/redis/lock")
public String lock(@RequestParam("key") String key) {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            redisLockService.lock(key);
            try {
                Thread.sleep(3000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
            redisLockService.unlock(key);
        }
        ).start();
    }
    return "OK";
}

RedisLockService 是我封裝了的一個 Redis 鎖服務,程式碼有點多,這裡就不貼了,完整的程式碼示例在 Github 上,大家可以 Star 一下:

https://github.com/javastacks/spring-boot-best-practice

測試:

http://localhost:8080/redis/lock?key=yeah

輸出:

2020-06-23 11:15:34
2020-06-23 11:15:37
2020-06-23 11:15:40
2020-06-23 11:15:43
2020-06-23 11:15:46
2020-06-23 11:15:49
2020-06-23 11:15:52
2020-06-23 11:15:55
2020-06-23 11:15:58
2020-06-23 11:16:01

可以看到每個執行緒需要等上一個執行緒休眠 3 秒後才能獲取到鎖。

原始碼分析

整合完了,會使用了,還得研究下 RedisLockRegistry 的原始碼,不然遇到什麼坑還得再踩一篇。

RedisLockRegistry 有兩個類構造器:

  • connectionFactory:Redis 連線工廠
  • registryKey:鎖字首
  • expireAfter:失效時間(非必須項,預設60秒)

所以,我們要註冊 expireAfter 這個選項,預設 60 秒是否滿足業務需要,如果超過預設的 60 少時間,否則將導致鎖失效。

還有兩個和 RedisLockRegistry 相關且很重要的成員變數:

private final String clientId = UUID.randomUUID().toString();

private final Map<String, RedisLock> locks = new ConcurrentHashMap<>();
  • clientId

首先用來標識當前 RedisLockRegistry 例項ID,並且在設定、移除鎖的時候都會要用到,用來判斷是不是當前的鎖註冊例項。

  • locks

用來在記憶體中快取當前鎖註冊例項所有鎖物件。

獲取鎖物件

如下面獲取鎖物件原始碼所示:

每個 key 在記憶體中(ConcurrentHashMap)都對應一個鎖物件,鎖物件有生成過就直接返回,沒有就生成再返回,有了這個鎖物件才能進行上鎖和解鎖操作。

這個鎖物件(RedisLock)其實也是實現了 Java 中的 java.util.concurrent.locks.Lock 鎖介面:

鎖物件(RedisLock)也有兩個很重要的成員變數:

private final ReentrantLock localLock = new ReentrantLock();

private volatile long lockedAt;
  • localLock

localLock 是一個本地記憶體可重入鎖,每次去 Redis 上鎖前,都要用本地 localLock 上鎖先,這樣能做到儘可能的少往 Redis 上鎖,也能從一方面提升鎖的效能。

  • lockedAt

lockedAt 上鎖時間,移除過時鎖會用到。

阻塞上鎖

RedisLock#lock():

每隔 100 毫秒嘗試獲取一次鎖,直到獲取鎖成功為止,不接受打斷異常,遇到其他異常會釋放本地鎖返回異常並結束。

主要看下設定 Redis 鎖的 Lua 指令碼:

根據 key 查詢其對應的值:clientId,如果和當前 clientId 一致則延長失效時間,如果 clientId 不存在就直接上鎖,以上都不成立返回 false。

這樣做的好處是,可以將整個 Redis Lua 指令碼作為一個原子執行,而不用考慮併發及事務影響。

好了,核心的原始碼分析完了,其實也很簡單,大家還不懂的或者有興趣的可以再研究下。

本文完整示例原始碼和上篇 Spring Boot 快速整合 Redis 的示例程式碼一起上傳到了 Github,歡迎大家 Star 關注學習。

https://github.com/javastacks/spring-boot-best-practice

所以,你還在手寫分散式鎖嗎?趕快 Get 起來吧!

相關文章