Spring Boot Redis實(shí)現(xiàn)分布式鎖,真香?。?/h1>
之前看很多人手寫分布式鎖,其實(shí) Spring Boot 現(xiàn)在已經(jīng)做的足夠好了,開箱即用,支持主流的 Redis、Zookeeper 中間件,另外還支持 JDBC。
本篇棧長(zhǎng)以 Redis 為例(這也是用得最多的方案),教大家如何利用 Spring Boot 集成 Redis 實(shí)現(xiàn)緩存,如何簡(jiǎn)單、快速實(shí)現(xiàn) Redis 分布式鎖。
分布式鎖介紹
Spring Boot 實(shí)現(xiàn) Redis 分布式鎖在 spring-integration 這個(gè)項(xiàng)目中,參考:
首先來(lái)看下 LockRegistry 鎖注冊(cè)接口的所有實(shí)現(xiàn)類結(jié)構(gòu)圖:
DefaultLockRegistry 就是純單機(jī)的可重入鎖,PassThruLockRegistry 是一個(gè)空實(shí)現(xiàn)類,也都沒(méi)有什么利用價(jià)值。
Spring Integration 4.0 引入了基于 Redis 的分布式鎖:RedisLockRegistry,并且從 5.0 開始實(shí)現(xiàn)了 ExpirableLockRegistry 接口,用來(lái)移除超時(shí)且沒(méi)有用的鎖。
分布式鎖實(shí)戰(zhàn)
添加依賴
上面提到 Spring Boot 實(shí)現(xiàn) Redis 分布式鎖在 spring-integration 這個(gè)項(xiàng)目中,所以需要這三個(gè)依賴:
- 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 基礎(chǔ)知識(shí)就不介紹了,不熟悉的可以關(guān)注公眾號(hào)Java技術(shù)棧,在后臺(tái)回復(fù):boot,可以閱讀我寫的歷史實(shí)戰(zhàn)教程。
配置分布式鎖
- @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 是我封裝了的一個(gè) Redis 鎖服務(wù),代碼有點(diǎn)多,這里就不貼了,完整的代碼示例在 Github 上,大家可以 Star 一下:
https://github.com/javastacks/spring-boot-best-practice
測(cè)試:
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
可以看到每個(gè)線程需要等上一個(gè)線程休眠 3 秒后才能獲取到鎖。
源碼分析
集成完了,會(huì)使用了,還得研究下 RedisLockRegistry 的源碼,不然遇到什么坑還得再踩一篇。
RedisLockRegistry 有兩個(gè)類構(gòu)造器:
- connectionFactory:Redis 連接工廠
- registryKey:鎖前綴
- expireAfter:失效時(shí)間(非必須項(xiàng),默認(rèn)60秒)
所以,我們要注冊(cè) expireAfter 這個(gè)選項(xiàng),默認(rèn) 60 秒是否滿足業(yè)務(wù)需要,如果超過(guò)默認(rèn)的 60 少時(shí)間,否則將導(dǎo)致鎖失效。
還有兩個(gè)和 RedisLockRegistry 相關(guān)且很重要的成員變量:
- private final String clientId = UUID.randomUUID().toString();
- private final Map<String, RedisLock> locks = new ConcurrentHashMap<>();
- clientId:
首先用來(lái)標(biāo)識(shí)當(dāng)前 RedisLockRegistry 實(shí)例ID,并且在設(shè)置、移除鎖的時(shí)候都會(huì)要用到,用來(lái)判斷是不是當(dāng)前的鎖注冊(cè)實(shí)例。
- locks:
用來(lái)在內(nèi)存中緩存當(dāng)前鎖注冊(cè)實(shí)例所有鎖對(duì)象。
獲取鎖對(duì)象
如下面獲取鎖對(duì)象源碼所示:
每個(gè) key 在內(nèi)存中(ConcurrentHashMap)都對(duì)應(yīng)一個(gè)鎖對(duì)象,鎖對(duì)象有生成過(guò)就直接返回,沒(méi)有就生成再返回,有了這個(gè)鎖對(duì)象才能進(jìn)行上鎖和解鎖操作。
這個(gè)鎖對(duì)象(RedisLock)其實(shí)也是實(shí)現(xiàn)了 Java 中的 java.util.concurrent.locks.Lock 鎖接口:
鎖對(duì)象(RedisLock)也有兩個(gè)很重要的成員變量:
- private final ReentrantLock localLock = new ReentrantLock();
- private volatile long lockedAt;
- localLock:
localLock 是一個(gè)本地內(nèi)存可重入鎖,每次去 Redis 上鎖前,都要用本地 localLock 上鎖先,這樣能做到盡可能的少往 Redis 上鎖,也能從一方面提升鎖的性能。
- lockedAt:
lockedAt 上鎖時(shí)間,移除過(guò)時(shí)鎖會(huì)用到。
阻塞上鎖
RedisLock#lock():
每隔 100 毫秒嘗試獲取一次鎖,直到獲取鎖成功為止,不接受打斷異常,遇到其他異常會(huì)釋放本地鎖返回異常并結(jié)束。
主要看下設(shè)置 Redis 鎖的 Lua 腳本:
根據(jù) key 查詢其對(duì)應(yīng)的值:clientId,如果和當(dāng)前 clientId 一致則延長(zhǎng)失效時(shí)間,如果 clientId 不存在就直接上鎖,以上都不成立返回 false。
這樣做的好處是,可以將整個(gè) Redis Lua 腳本作為一個(gè)原子執(zhí)行,而不用考慮并發(fā)及事務(wù)影響。
好了,核心的源碼分析完了,其實(shí)也很簡(jiǎn)單,大家還不懂的或者有興趣的可以再研究下。
本文完整示例源代碼和上篇 Spring Boot 快速集成 Redis 的示例代碼一起上傳到了 Github,歡迎大家 Star 關(guān)注學(xué)習(xí)。
https://github.com/javastacks/spring-boot-best-practice
所以,你還在手寫分布式鎖嗎?趕快 Get 起來(lái)吧!