Redisson實現Redis分散式鎖的N種姿勢
前幾天發的一篇文章《Redlock:Redis分散式鎖最牛逼的實現》,引起了一些同學的討論,也有一些同學提出了一些疑問,這是好事兒。本文在講解如何使用Redisson實現Redis普通分散式鎖,以及Redlock演算法分散式鎖的幾種方式的同時,也附帶解答這些同學的一些疑問。
Redis幾種架構
Redis發展到現在,幾種常見的部署架構有:
-
單機模式;
-
主從模式;
-
哨兵模式;
-
叢集模式;
我們首先基於這些架構講解Redisson普通分散式鎖實現,需要注意的是,只有充分了解普通分散式鎖是如何實現的,才能更好的瞭解Redlock分散式鎖的實現,因為Redlock分散式鎖的實現完全基於普通分散式鎖
普通分散式鎖
Redis普通分散式鎖原理這個大家基本上都瞭解,本文不打算再過多的介紹,上一篇文章《Redlock:Redis分散式鎖最牛逼的實現》也講的很細,並且也說到了幾個重要的注意點。如果你對Redis普通的分散式鎖還有一些疑問,可以再回顧一下這篇文章。
接下來直接show you the code,畢竟 talk is cheap。
redisson版本
本次測試選擇redisson 2.14.1版本。
單機模式
原始碼如下:
-
// 構造redisson實現分散式鎖必要的Config
-
Config config = new Config();
-
config.useSingleServer().setAddress(
-
// 構造RedissonClient
-
RedissonClient redissonClient = Redisson.create(config);
-
// 設定鎖定資源名稱
-
RLock disLock = redissonClient.getLock("DISLOCK");
-
boolean isLock;
-
try {
-
//嘗試獲取分散式鎖
-
isLock = disLock.tryLock(500, 15000, TimeUnit
-
if (isLock) {
-
//TODO if get lock success, do something;
-
Thread.sleep(15000);
-
}
-
} catch (Exception e) {
-
} finally {
-
// 無論如何, 最後都要解鎖
-
disLock.unlock();
-
}
通過程式碼可知,經過Redisson的封裝,實現Redis分散式鎖非常方便,我們再看一下Redis中的value是啥,和前文分析一樣,hash結構,key就是資源名稱,field就是UUID+threadId,value就是重入值,在分散式鎖時,這個值為1(Redisson還可以實現重入鎖,那麼這個值就取決於重入次數了):
-
172.29.1.180:5379> hgetall DISLOCK
-
1) "01a6d806-d282-4715-9bec-f51b9aa98110:1"
-
2) "1"
哨兵模式
即sentinel模式,實現程式碼和單機模式幾乎一樣,唯一的不同就是Config的構造:
-
Config config = new Config();
-
config.useSentinelServers().addSentinelAddress(
-
"redis://172.29.3.245:26378","redis://172.29.3.245:26379", "redis://172.29.3.245:26380")
-
.setMasterName("mymaster")
-
.setPassword("a123456").setDatabase(0);
叢集模式
叢集模式構造Config如下:
-
Config config = new Config();
-
config.useClusterServers().addNodeAddress(
-
"redis://172.29.3.245:6375","redis://172.29.3.245:6376", "redis://172.29.3.245:6377",
-
"redis://172.29.3.245:6378","redis://172.29.3.245:6379", "redis://172.29.3.245:6380")
-
.setPassword("a123456").setScanInterval(5000);
總結
普通分散式實現非常簡單,無論是那種架構,向Redis通過EVAL命令執行LUA指令碼即可。
Redlock分散式鎖
那麼Redlock分散式鎖如何實現呢?以單機模式Redis架構為例,直接看實現程式碼:
-
Config config1 = new Config();
-
config1.useSingleServer().setAddress("redis://172.29.1.180:5378")
-
.setPassword("a123456").setDatabase(0);
-
RedissonClient redissonClient1 = Redisson.create(config1);
-
Config config2 = new Config();
-
config2.useSingleServer().setAddress("redis://172.29.1.180:5379")
-
.setPassword("a123456").setDatabase(0);
-
RedissonClient redissonClient2 = Redisson.create(config2);
-
Config config3 = new Config();
-
config3.useSingleServer().setAddress("redis://172.29.1.180:5380")
-
.setPassword("a123456").setDatabase(0);
-
RedissonClient redissonClient3 = Redisson.create(config3);
-
String resourceName = "REDLOCK";
-
RLock lock1 = redissonClient1.getLock(resourceName);
-
RLock lock2 = redissonClient2.getLock(resourceName);
-
RLock lock3 = redissonClient3.getLock(resourceName);
-
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
-
boolean isLock;
-
try {
-
isLock = redLock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
-
System.out.println("isLock = "+isLock);
-
if (isLock) {
-
//TODO if get lock success, do something;
-
Thread.sleep(30000);
-
}
-
} catch (Exception e) {
-
} finally {
-
// 無論如何, 最後都要解鎖
-
System.out.println("");
-
redLock.unlock();
-
}
最核心的變化就是 RedissonRedLockredLock=newRedissonRedLock(lock1,lock2,lock3);
,因為我這裡是以三個節點為例。
那麼如果是哨兵模式呢?需要搭建3個,或者5個sentinel模式叢集(具體多少個,取決於你)。 那麼如果是叢集模式呢?需要搭建3個,或者5個cluster模式叢集(具體多少個,取決於你)。
實現原理
既然核心變化是使用了RedissonRedLock,那麼我們看一下它的原始碼有什麼不同。這個類是RedissonMultiLock的子類,所以呼叫tryLock方法時,事實上呼叫了RedissonMultiLock的tryLock方法,精簡原始碼如下:
-
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
-
// 實現要點之允許加鎖失敗節點限制(N-(N/2+1))
-
int failedLocksLimit = failedLocksLimit();
-
List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
-
// 實現要點之遍歷所有節點通過EVAL命令執行lua加鎖
-
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
-
RLock lock = iterator.next();
-
boolean lockAcquired;
-
try {
-
// 對節點嘗試加鎖
-
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
-
} catch (RedisConnectionClosedException|RedisResponseTimeoutException e) {
-
// 如果丟擲這類異常,為了防止加鎖成功,但是響應失敗,需要解鎖
-
unlockInner(Arrays.asList(lock));
-
lockAcquired = false;
-
} catch (Exception e) {
-
// 丟擲異常表示獲取鎖失敗
-
lockAcquired = false;
-
}
-
if (lockAcquired) {
-
// 成功獲取鎖集合
-
acquiredLocks.add(lock);
-
} else {
-
// 如果達到了允許加鎖失敗節點限制,那麼break,即此次Redlock加鎖失敗
-
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
-
break;
-
}
-
}
-
}
-
return true;
-
}
很明顯,這段原始碼就是上一篇文章《Redlock:Redis分散式鎖最牛逼的實現》提到的Redlock演算法的完全實現。
以sentinel模式架構為例,如下圖所示,有sentinel-1,sentinel-2,sentinel-3總計3個sentinel模式叢集,如果要獲取分散式鎖,那麼需要向這3個sentinel叢集通過EVAL命令執行LUA指令碼,需要3/2+1=2,即至少2個sentinel叢集響應成功,才算成功的以Redlock演算法獲取到分散式鎖:
問題合集
根據上面實現原理的分析,這位同學應該是對Redlock演算法實現有一點點誤解,假設我們用5個節點實現Redlock演算法的分散式鎖。那麼 要麼是5個redis單例項,要麼是5個sentinel叢集,要麼是5個cluster叢集。而不是一個有5個主節點的cluster叢集,然後向每個節點通過EVAL命令執行LUA指令碼嘗試獲取分散式鎖,如上圖所示。失效時間如何設定
這個問題的場景是,假設設定失效時間10秒,如果由於某些原因導致10秒還沒執行完任務,這時候鎖自動失效,導致其他執行緒也會拿到分散式鎖。
這確實是Redis分散式最大的問題,不管是普通分散式鎖,還是Redlock演算法分散式鎖,都沒有解決這個問題。也有一些文章提出了對失效時間續租,即延長失效時間,很明顯這又提升了分散式鎖的複雜度。另外就筆者瞭解,沒有現成的框架有實現,如果有哪位知道,可以告訴我,萬分感謝。
redis分散式鎖的高可用
關於Redis分散式鎖的安全性問題,在分散式系統專家Martin Kleppmann和Redis的作者antirez之間已經發生過一場爭論。有興趣的同學,搜尋"基於Redis的分散式鎖到底安全嗎"就能得到你想要的答案,需要注意的是,有上下兩篇(這應該就是傳說中的神仙打架吧,哈)。
zookeeper or redis
沒有絕對的好壞,只有更適合自己的業務。就效能而言,redis很明顯優於zookeeper;就分散式鎖實現的健壯性而言,zookeeper很明顯優於redis。如何選擇,取決於你的業務!
原文釋出時間為: 2018-12-03
本文作者:阿飛的部落格
本文來自雲棲社群合作伙伴“ Java技術驛站 ”,瞭解相關資訊可以關注“ Java技術驛站 ”。