CAP
在引出RedLock之前,先介紹一下分佈式系統中CAP理論:
C(Consistency):一致性,在同一時間點,所有節點的數據都是完全一致的。
A(Availability):可用性,應該能夠在正常時間內對請求進行響應。
P(Partition-tolerance):分區容忍性,在分佈式環境中,多個節點組成的網絡應該是互相連通的,當由於網絡故障等原因造成網絡分區,要求仍然能夠對外提供服務。
CAP理論告訴我們,任何分佈式系統只能滿足三個中的兩個,不可能全部都滿足。
參考分佈式系統那點事
redis分佈式鎖存在的問題
網上對於採用redis實現分佈式鎖有很多種方案,比較完善的方案應該是用setNx + lua進行實現。簡單實現如下:
- java代碼-加鎖,相當於setnx lock_key_name unique_value
set lock_key_name unique_value NX PX 5000;
- lua腳本-解鎖,原子性操作
if redis.call("get", KEYS[1] == ARGV[1]) then
return redis.call("del", KEYS[1])
else
return 0
end
注意:
- value需要具有唯一性,可以採用時間戳、uuid或者自增id實現;
- 客戶端在解鎖時,需要比較本地內存中的value和redis中的value是否一致,防止誤解鎖;(case:clientA獲取鎖lock1,由於clientA執行的時間比較久,導致key=lock1已經過期,redis實例會移除該key;clientB獲取相同的鎖lock1,clientB正在佔有鎖並執行業務,此時clientA業務已經執行完畢,準備釋放鎖;如果沒有比較value的邏輯,那麼clientA會把clientB持有的鎖釋放掉,這個顯然不行的,由於value值不同,那麼clientA釋放鎖的時候只會釋放自己加的鎖,不會誤釋放別的客戶端加的鎖)
在分佈式系統中,爲了避免單點故障,提高可靠性,redis都會採用主從架構,當主節點掛了後,從節點會作爲主繼續提供服務。該種方案能夠滿足大多數的業務場景,但是對於要求強一致性的場景如交易,該種方案還是有漏洞的,原因如下:
redis主從架構採用的是異步複製,當master節點拿到了鎖,但是鎖還未同步到slave節點,此時master節點掛了,發生故障轉移,slave節點被選舉爲master節點,丟失了鎖。這樣其他線程就能夠獲取到該鎖,顯然是有問題的。
因此,上述基於redis實現的分佈式鎖只是滿足了AP,並沒有滿足C。
RedLock
正是因爲上述redis分佈式鎖存在的一致性問題,redis作者提出了一個更加高級的基於redis實現的分佈式鎖——RedLock。原文可參考 Distributed locks with Redis
RedLock是什麼?
RedLock是基於redis實現的分佈式鎖,它能夠保證以下特性:
- 互斥性:在任何時候,只能有一個客戶端能夠持有鎖;
- 避免死鎖:當客戶端拿到鎖後,即使發生了網絡分區或者客戶端宕機,也不會發生死鎖;(利用key的存活時間)
- 容錯性:只要多數節點的redis實例正常運行,就能夠對外提供服務,加鎖或者釋放鎖;
而非redLock是無法滿足互斥性的,上面已經闡述過了原因。
RedLock算法
假設有N個redis的master節點,這些節點是相互獨立的(不需要主從或者其他協調的系統)。N推薦爲奇數~
客戶端在獲取鎖時,需要做以下操作:
- 獲取當前時間戳,以微妙爲單爲。
- 使用相同的lockName和lockValue,嘗試從N個節點獲取鎖。(在獲取鎖時,要求等待獲取鎖的時間遠小於鎖的釋放時間,如鎖的lease_time爲10s,那麼wait_time應該爲5-50毫秒;避免因爲redis實例掛掉,客戶端需要等待更長的時間才能返回,即需要讓客戶端能夠fast_fail;如果一個redis實例不可用,那麼需要繼續從下個redis實例獲取鎖)
- 當從N個節點獲取鎖結束後,如果客戶端能夠從多數節點(N/2 + 1)中成功獲取鎖,且獲取鎖的時間小於失效時間,那麼可認爲,客戶端成功獲得了鎖。(獲取鎖的時間=當前時間戳 - 步驟1的時間戳)
- 客戶端成功獲得鎖後,那麼鎖的實際有效時間 = 設置鎖的有效時間 - 獲取鎖的時間。
- 客戶端獲取鎖失敗後,N個節點的redis實例都會釋放鎖,即使未能加鎖成功。
爲什麼N推薦爲奇數呢?
原因1:本着最大容錯的情況下,佔用服務資源最少的原則,2N+1和2N+2的容災能力是一樣的,所以採用2N+1;比如,5臺服務器允許2臺宕機,容錯性爲2,6臺服務器也只能允許2臺宕機,容錯性也是2,因爲要求超過半數節點存活才OK。
原因2:假設有6個redis節點,client1和client2同時向redis實例獲取同一個鎖資源,那麼可能發生的結果是——client1獲得了3把鎖,client2獲得了3把鎖,由於都沒有超過半數,那麼client1和client2獲取鎖都失敗,對於奇數節點是不會存在這個問題。
失敗時重試
當客戶端無法獲取到鎖時,應該隨機延時後進行重試,防止多個客戶端在同一時間搶奪同一資源的鎖(會導致腦裂,最終都不能獲取到鎖)。客戶端獲得超過半數節點的鎖花費的時間越短,那麼腦裂的概率就越低。所以,理想的情況下,客戶端最好能夠同時(併發)向所有redis發出set命令。
當客戶端從多數節點獲取鎖失敗時,應該儘快釋放已經成功獲取的鎖,這樣其他客戶端不需要等待鎖過期後再獲取。(如果存在網絡分區,客戶端已經無法和redis進行通信,那麼此時只能等待鎖過期後自動釋放)
不明白爲什麼會發生腦裂???
釋放鎖
向所有redis實例發送釋放鎖命令即可,不需要關心redis實例有沒有成功上鎖。
redisson在加鎖的時候,key=lockName, value=uuid + threadID, 採用set結構存儲,幷包含了上鎖的次數 (支持可重入);解鎖的時候通過hexists判斷key和value是否存在,存在則解鎖;這裏不會出現誤解鎖
性能、 崩潰恢復和redis同步
如何提升分佈式鎖的性能?以每分鐘執行多少次acquire/release操作作爲性能指標,一方面通過增加redis實例可用降低響應延遲,另一方面,使用非阻塞模型,一次發送所有的命令,然後異步讀取響應結果,這裏假設客戶端和redis之間的RTT差不多。
如果redis沒用使用備份,redis重啓後,那麼會丟失鎖,導致多個客戶端都能獲取到鎖。通過AOF持久化可以緩解這個問題。redis key過期是unix時間戳,即便是redis重啓,那麼時間依然是前進的。但是,如果是斷電呢?redis在啓動後,可能就會丟失這個key(在寫入或者還未寫入磁盤時斷電了,取決於fsync的配置),如果採用fsync=always,那麼會極大影響性能。如何解決這個問題呢?可以讓redis節點重啓後,在一個TTL時間段內,對客戶端不可用即可。
針對redlock的爭議
後續會對該部分內容進行更新。
[參考鏈接](http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
Redisson
redisson是在redis基礎上實現的一套開源解決方案,不僅提供了一系列的分佈式的java常用對象,還提供了許多分佈式服務,宗旨是促進使用者對redis的關注分離,更多的關注業務邏輯的處理上。
redisson也對redlock做了一套實現,詳細如下:
使用案例
public static void main() {
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://xxxx1:xxx1")
.setPassword("xxxx1")
.setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer()
.setAddress("redis://xxxx2:xxx2")
.setPassword("xxxx2")
.setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().
setAddress("redis://xxxx3:xxx3")
.setPassword("xxxx3")
.setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);
String lockName = "redlock-test";
RLock lock1 = redissonClient1.getLock(lockName);
RLock lock2 = redissonClient2.getLock(lockName);
RLock lock3 = redissonClient3.getLock(lockName);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
isLock = redLock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
System.out.println("isLock = " + isLock);
if (isLock) {
// lock success, do something;
Thread.sleep(30000);
}
} catch (Exception e) {
} finally {
// 無論如何, 最後都要解鎖
redLock.unlock();
System.out.println("unlock success");
}
}
源碼
tryLock():redisson對redlock的實現方式基本和上述描述的類似,有一點區別在於,redisson在獲取鎖成功後,會對key的失效時間重新。
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long newLeaseTime = -1;
if (leaseTime != -1) {
newLeaseTime = unit.toMillis(waitTime)*2;
}
long time = System.currentTimeMillis();
long remainTime = -1;
if (waitTime != -1) {
remainTime = unit.toMillis(waitTime);
}
long lockWaitTime = calcLockWaitTime(remainTime);
int failedLocksLimit = failedLocksLimit();
List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
RLock lock = iterator.next();
boolean lockAcquired;
try {
if (waitTime == -1 && leaseTime == -1) {
lockAcquired = lock.tryLock();
} else {
long awaitTime = Math.min(lockWaitTime, remainTime);
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
} catch (RedisResponseTimeoutException e) {
unlockInner(Arrays.asList(lock));
lockAcquired = false;
} catch (Exception e) {
lockAcquired = false;
}
if (lockAcquired) {
acquiredLocks.add(lock);
} else {
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
break;
}
if (failedLocksLimit == 0) {
unlockInner(acquiredLocks);
if (waitTime == -1 && leaseTime == -1) {
return false;
}
failedLocksLimit = failedLocksLimit();
acquiredLocks.clear();
// reset iterator
while (iterator.hasPrevious()) {
iterator.previous();
}
} else {
failedLocksLimit--;
}
}
if (remainTime != -1) {
remainTime -= (System.currentTimeMillis() - time);
time = System.currentTimeMillis();
if (remainTime <= 0) {
unlockInner(acquiredLocks);
return false;
}
}
}
if (leaseTime != -1) {
List<RFuture<Boolean>> futures = new ArrayList<RFuture<Boolean>>(acquiredLocks.size());
for (RLock rLock : acquiredLocks) {
RFuture<Boolean> future = rLock.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
futures.add(future);
}
for (RFuture<Boolean> rFuture : futures) {
rFuture.syncUninterruptibly();
}
}
return true;
}