上篇講解了如何用 Redis 實現分散式鎖的五種方案,但我們還是有更優的王者方案,就是用 Redisson。
快取系列文章:
快取實戰(一):20 圖 |6 千字|快取實戰(上篇)
快取實戰(二):Redis 分散式鎖|從青銅到鑽石的五種演進方案
我們先來看下 Redis 官網怎麼說,
而 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)。
Netty 框架:Redisson採用了基於NIO的Netty框架,不僅能作為Redis底層驅動客戶端,具備提供對Redis各種組態形式的連線功能,對Redis命令能以同步傳送、非同步形式傳送、非同步流形式傳送或管道形式傳送的功能,LUA指令碼執行處理,以及處理返回結果的功能
基礎資料結構:將原生的Redis
Hash
,List
,Set
,String
,Geo
,HyperLogLog
等資料結構封裝為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)
,原子整長形(AtomicLong)
,原子雙精度浮點數(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分散式可重入鎖RLock
Java 物件實現了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
第一個執行緒對應的執行緒 ID 為 86,10秒後,釋放鎖。在這期間,第二個執行緒需要等待鎖釋放。
第一個執行緒釋放鎖之後,第二個執行緒獲取到了鎖,10 秒後,釋放鎖。
畫了一個流程圖,幫助大家理解。如下圖所示:
- 第一步:執行緒 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 客戶端查詢到的結果,如下圖所示:
WuKong-lock 有值,而且大家可以看到 TTL 在不斷變小,說明 WuKong-lock 是自帶過期時間的。
通過觀察,經過 30 秒後,WuKong-lock 過期消失了。說明 Redisson 在停機後,佔用的鎖會自動釋放。
那這又是什麼原理呢?這裡就要提一個概念了,看門狗
。
3.2 看門狗原理
如果負責儲存這個分散式鎖的 Redisson 節點宕機以後,而且這個鎖正好處於鎖住的狀態時,這個鎖會出現鎖死的狀態。為了避免這種情況的發生,Redisson內部提供了一個監控鎖的看門狗
,它的作用是在Redisson例項被關閉前,不斷的延長鎖的有效期。
預設情況下,看門狗的檢查鎖的超時時間是30秒鐘,也可以通過修改Config.lockWatchdogTimeout來另行指定。
如果我們未制定 lock 的超時時間,就使用 30 秒作為看門狗的預設時間。只要佔鎖成功,就會啟動一個定時任務
:每隔 10 秒重新給鎖設定過期的時間,過期時間為 30 秒。
如下圖所示:
當伺服器宕機後,因為鎖的有效期是 30 秒,所以會在 30 秒內自動解鎖。(30秒等於宕機之前的鎖佔用時間+後續鎖佔用的時間)。
如下圖所示:
3.3 設定鎖過期時間
我們也可以通過給鎖設定過期時間,讓其自動解鎖。
如下所示,設定鎖 8 秒後自動過期。
lock.lock(8, TimeUnit.SECONDS);
如果業務執行時間超過 8 秒,手動釋放鎖將會報錯,如下圖所示:
所以我們如果設定了鎖的自動過期時間,則執行業務的時間一定要小於鎖的自動過期時間,否則就會報錯。
四、王者方案
上一篇我講解了分散式鎖的五種方案:《從青銅到鑽石的演進方案》,這一篇主要是講解如何用 Redisson 在 Spring Boot 專案中實現分散式鎖的方案。
因為 Redisson 非常強大,實現分散式鎖的方案非常簡潔,所以稱作王者方案
。
原理圖如下:
程式碼如下所示:
// 1.設定分散式鎖
RLock lock = redisson.getLock("lock");
// 2.佔用鎖
lock.lock();
// 3.執行業務
...
// 4.釋放鎖
lock.unlock();
和之前 Redis 的方案相比,簡潔很多。
五、分散式讀寫鎖
基於 Redis 的 Redisson 分散式可重入讀寫鎖RReadWriteLock
Java物件實現了java.util.concurrent.locks.ReadWriteLock
介面。其中讀鎖和寫鎖都繼承了 RLock
介面。
寫鎖是一個拍他鎖(互斥鎖),讀鎖是一個共享鎖。
- 讀鎖 + 讀鎖:相當於沒加鎖,可以併發讀。
- 讀鎖 + 寫鎖:寫鎖需要等待讀鎖釋放鎖。
- 寫鎖 + 寫鎖:互斥,需要等待對方的鎖釋放。
- 寫鎖 + 讀鎖:讀鎖需要等待寫鎖釋放。
示例程式碼如下:
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 來演示上述停車位的場景。
先定義一個佔用停車位的方法:
/**
* 停車,佔用停車位
* 總共 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,總共有三個值。
然後用 postman 傳送 park 請求佔用一個停車位。
然後在 redis 客戶端檢視 park 的值,發現已經改為 2 了。繼續呼叫兩次,發現 park 的等於 0,當呼叫第四次的時候,會發現請求一直處於等待中
,說明車位不夠了。如果想要不阻塞,可以用 tryAcquire 或 tryAcquireAsync。
我們再呼叫離開車位的方法,park 的值變為了 1,代表車位剩餘 1 個。
注意:多次執行釋放訊號量操作,剩餘訊號量會一直增加,而不是到 3 後就封頂了。
其他分散式鎖:
公平鎖(Fair Lock)
聯鎖(MultiLock)
紅鎖(RedLock)
讀寫鎖(ReadWriteLock)
可過期性訊號量(PermitExpirableSemaphore)
閉鎖(CountDownLatch)
還有其他分散式鎖就不在本篇展開了,感興趣的同學可以檢視官方文件。
參考資料:
https://github.com/redisson/redisson