上篇講解了如何用 Redis 實現分佈式鎖的五種方案,但咱們仍是有更優的王者方案,就是用 Redisson。html
緩存系列文章:前端
緩存實戰(一):20 圖 |6 千字|緩存實戰(上篇)java
緩存實戰(二):Redis 分佈式鎖|從青銅到鑽石的五種演進方案git
咱們先來看下 Redis 官網怎麼說,github
而 Java 版的 分佈式鎖的框架就是 Redisson。本篇實戰內容將會基於個人開源項目 PassJava 來整合 Redisson。redis
我把後端
、前端
、小程序
都上傳到同一個倉庫裏面了,你們能夠經過 Github
或 碼雲
訪問。地址以下:算法
Github: https://github.com/Jackson0714/PassJava-Platform小程序
碼雲:https://gitee.com/jayh2018/PassJava-Platform後端
配套教程:www.passjava.cnapi
在實戰以前,咱們先來看下使用 Redisson 的原理。
1、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做爲獨立節點能夠用於獨立執行其餘節點發布到
分佈式執行服務
和分佈式調度服務
裏的遠程任務。
2、整合 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、分佈式可重入鎖
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 秒,手動釋放鎖將會報錯,以下圖所示:
image-20210521102640573
因此咱們若是設置了鎖的自動過時時間,則執行業務的時間必定要小於鎖的自動過時時間,不然就會報錯。
4、王者方案
上一篇我講解了分佈式鎖的五種方案:《從青銅到鑽石的演進方案》,這一篇主要是講解如何用 Redisson 在 Spring Boot 項目中實現分佈式鎖的方案。
由於 Redisson 很是強大,實現分佈式鎖的方案很是簡潔,因此稱做王者方案
。
原理圖以下:
代碼以下所示:
// 1.設置分佈式鎖
RLock lock = redisson.getLock("lock");
// 2.佔用鎖
lock.lock();
// 3.執行業務
...
// 4.釋放鎖
lock.unlock();
和以前 Redis 的方案相比,簡潔不少。
5、分佈式讀寫鎖
基於 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();
6、分佈式信號量
基於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