網路上有各種關於Redis實作分散式鎖定的方案,但何為王者方案?
答案就: Redisson。
我們先來看看 Redis 官網對分散式鎖定的說法:
#而 Java 版的 分散式鎖定的框架就是 Redisson。
這篇實戰內容將會以我的開源專案 PassJava 為基礎來整合 Redisson。
我把後端
、前端
、小程式
都上傳到同一個倉庫裡面了,大家可以透過Github
或碼雲
存取。網址如下:
Github: https://github.com/Jackson0714/PassJava-Platform
碼雲#:https: //gitee.com/jayh2018/PassJava-Platform
配套教學:www.passjava.cn
在實戰前,我們先來看看使用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作為獨立節點可以用於獨立執行其他節點發佈到分散式執行服務
和分散式調度服務
裡的遠端任務。
#Spring Boot 整合Redisson 有兩種方案:
本篇介紹如何用程式化的方式整合 Redisson。
在 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>
下面的程式碼是單節點 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); } }
新建一個單元測試方法。
@Autowired RedissonClient redissonClient; @Test public void TestRedisson() { System.out.println(redissonClient); }
我們執行這個測試方法,印出redissonClient
org.redisson.Redisson@77f66138
基於Redis的Redisson分散式可重入鎖定RLock
Java 物件實作了java.util.concurrent.locks.Lock
介面。同時也提供了非同步(Async)、反射式(Reactive)和RxJava2標準的介面。
RLock lock = redisson.getLock("anyLock"); // 最常见的使用方法 lock.lock();
我們用passjava 這個開源專案測試下可重入鎖的兩個點:
為了驗證以上兩點,我寫了個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 秒后,释放锁。
画了一个流程图,帮助大家理解。如下图所示:
由此可以得出结论,Redisson 的可重入锁(lock)是阻塞其他线程的,需要等待其他线程释放的。
如果线程 A 在等待的过程中,服务突然停了,那么锁会释放吗?如果不释放的话,就会成为死锁,阻塞了其他线程获取锁。
我们先来看下线程 A 的获取锁后的,Redis 客户端查询到的结果,如下图所示:
WuKong-lock 有值,而且大家可以看到 TTL 在不断变小,说明 WuKong-lock 是自带过期时间的。
通过观察,经过 30 秒后,WuKong-lock 过期消失了。说明 Redisson 在停机后,占用的锁会自动释放。
那这又是什么原理呢?这里就要提一个概念了,看门狗
。
如果负责储存这个分布式锁的 Redisson 节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗
,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。
默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
如果我们未制定 lock 的超时时间,就使用 30 秒作为看门狗的默认时间。只要占锁成功,就会启动一个定时任务
:每隔 10 秒重新给锁设置过期的时间,过期时间为 30 秒。
如下图所示:
当服务器宕机后,因为锁的有效期是 30 秒,所以会在 30 秒内自动解锁。(30秒等于宕机之前的锁占用时间+后续锁占用的时间)。
如下图所示:
我们也可以通过给锁设置过期时间,让其自动解锁。
如下所示,设置锁 8 秒后自动过期。
lock.lock(8, TimeUnit.SECONDS);
如果业务执行时间超过 8 秒,手动释放锁将会报错,如下图所示:
所以我们如果设置了锁的自动过期时间,则执行业务的时间一定要小于锁的自动过期时间,否则就会报错。
上一篇我讲解了分布式锁的五种方案:Redis 分布式锁|从青铜到钻石的五种演进方案,这一篇主要是讲解如何用 Redisson 在 Spring Boot 项目中实现分布式锁的方案。
因为 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
接口。
写锁是一个排他锁(互斥锁),读锁是一个共享锁。
示例代码如下:
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)
還有其他分散式鎖定就不在本篇展開了,有興趣的同學可以查看官方文件。
以上是分散式鎖中的王者方案 - Redisson的詳細內容。更多資訊請關注PHP中文網其他相關文章!