要實現分佈式鎖,Redis官網介紹了三個必需要保證的特性:html
咱們分析一個場景:redis
1.客戶端A在Redis master節點申請鎖算法
2.master在將存儲的key同步到slave上以前崩潰了安全
3.slave晉升爲mastermarkdown
4.客戶端B申請一個客戶端A已經持有的資源的鎖網絡
哇,問題出現了。分佈式
爲了解決基於故障轉移實現的分佈式鎖的問題,Redis做者提出了Redlock算法。ide
假設有N(N≥5)個Redis的master實例,這些節點之間都是相互獨立的,所以咱們不用副本或其餘隱式的協調系統。爲了獲取鎖,客戶端須要進行如下操做(假設N=5):函數
1.獲取當前時間oop
2.按順序嘗試在5個Redis節點上獲取鎖,使用相同的key 做爲鍵,隨機數做爲值(隨機值在5個節點上是同樣的)。在嘗試在每一個Redis節點上獲取鎖時,設置一個超時時間,這個超時時間須要比總的鎖的自動超時時間小。例如,自動釋放時間爲10秒,那麼鏈接超時的時間能夠設置爲5-50毫秒。這樣能夠防止客戶端長時間與處於故障狀態的Redis節點通訊時保持阻塞狀態:若是一個Redis節點處於故障狀態,咱們須要儘快與下一個節點進行通訊。
3.客戶端計算獲取鎖時消耗的時間,用當前時間,減去在第1步中獲得的時間。只用當客戶端能夠在多數節點上可以獲取到鎖,而且獲取鎖消耗的總時間小於鎖的有效時間,那麼這個鎖被認爲是獲取成功了。
4.若是鎖獲取成功了,鎖的有效時間是初始的有效時間減掉申請鎖消耗的總時間。
5.若是客戶端申請鎖失敗了(例如不知足多數節點或第4步中獲取到的鎖有效期爲負數),客戶端須要在全部節點上解除鎖(即便是認爲已經沒法提供服務的節點)。
當一個客戶端沒法獲取鎖時,它應該在隨機延遲後再次嘗試,以便讓多個客戶端在同一時間嘗試獲取相同資源的鎖(可能會致使腦裂的狀況),並且,在大多數Redis實例中,客戶端嘗試獲取鎖定的速度越快,出現裂腦狀況的窗口就越小,所以,理想狀況下,客戶端應嘗試使用多路複用將SET命令發送到N個實例。
值得強調的是,對於未能獲取大多數鎖的客戶端,儘快釋放(部分)獲取的鎖,這是多麼重要,所以,無需等待key到期便可再次獲取鎖(可是,若是發生網絡分區,而且客戶端再也不可以與Redis實例通訊,則在等待key到期時須要支付可用性損失)。
有細心的同窗可能會有疑惑,在每一個節點上設置的鎖時間是同樣的,可是在每一個節點執行的時間可能不同,那鎖失效的時間也有早有晚啊,那這個算法夠安全嗎?咱們一塊兒來分析一下不一樣場景下的狀況:
首先假設客戶端在每一個節點上都能獲取到鎖,每一個節點上都有相同的存活時間的key。可是,key在每一個節點上設置的時間點是不同的,那麼key的過時時間就不同,假設第一個節點上設置鎖的時間點爲T1,最後一個節點設置鎖的時間點爲T2,那麼第一個鎖的有效期至少爲
MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT(每一個計算機都有一個本地時間,而每一個計算機可能都存在很是小的時間偏差,這個時間偏差即爲CLOCK_DRIFT),其餘節點上的key都會在這個時間以後纔會到期,因此咱們能夠肯定至少在這一時間內,全部的key是都生效的。
在一個客戶端已經在多數節點上設置了key的這段時間內,其餘的客戶端是沒法再在多數節點上(N/2+1)獲取到鎖的。
咱們還須要保證多個客戶端在同一時間不能獲取到同一個鎖。
若是一個客戶端在多數節點上獲取鎖所消耗的時間接近或者大於鎖的有效期(TTL),咱們認爲這個鎖沒有申請成功,並會在全部節點上進行解鎖操做,因此咱們只要考慮加鎖消耗時間小於鎖有效期的狀況便可,在這種狀況下,對於上面已經說明的參數——MIN_VALIDITY,沒有客戶端可以從新獲取鎖。因此只有在在多數節點上(N/2+1)加鎖耗時大於TTL的狀況下,纔會有多個客戶端可能會同時獲取到鎖,這種狀況直接將鎖認定爲獲取失敗便可。
這套算法的可用性是基於如下三個特性:
1.自動釋放鎖——鎖能夠自動失效,失效後能夠從新上鎖
2.一般狀況下,客戶端一般會在未得到鎖或得到鎖且工做終止時刪除鎖,這使得咱們沒必要等待key過時便可從新得到鎖。
3.當客戶端須要重試獲取鎖時,它等待的時間要比獲取大多數節點鎖定所需的時間長得多,以便機率地使資源爭用期間的腦裂狀況變得不可能。
然而,咱們付出了時間爲TTL的網絡分區可用性的代價,若是發生了連續性的分區,那咱們也要付出無限的分區可用性代價(CAP理論,一致性、可用性、分區容錯性,分區一致性是前提)。每當客戶端得到一個鎖並在可以刪除鎖以前被分區時,都會發生這種狀況。(以下圖所示,有三個節點的分區不可用的狀況下,client A 和client B同時申請一個鎖,這時兩個客戶端都沒法獲取到多數節點,那麼一直到網絡恢復或者鎖的超時時間,競爭關係解除,從新再競爭...)
以上就是Redis的做者給出的Redlock的算法模型,那麼在Java的Redis客戶端中,Redisson實現了Redlock,咱們來分析一下它的具體實現代碼。
Redisson是一個在Redis的基礎上實現的Java駐內存數據網格(In-Memory Data Grid)。它不只提供了一系列的分佈式的Java經常使用對象,還提供了許多分佈式服務。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最簡單和最便捷的方法。Redisson的宗旨是促進使用者對Redis的關注分離(Separation of Concern),從而讓使用者可以將精力更集中地放在處理業務邏輯上。
按照慣例,咱們先來看一下源碼
/**
* RedLock locking algorithm implementation for multiple locks.
* It manages all locks as one.
*
* @see <a href="http://redis.io/topics/distlock">http://redis.io/topics/distlock</a>
*
* @author Nikita Koksharov
*
*/
public class RedissonRedLock extends RedissonMultiLock {
/**
* Creates instance with multiple {@link RLock} objects.
* Each RLock object could be created by own Redisson instance.
*
* @param locks - array of locks
*/
public RedissonRedLock(RLock... locks) {
super(locks);
}
@Override
protected int failedLocksLimit() {
return locks.size() - minLocksAmount(locks);
}
protected int minLocksAmount(final List<RLock> locks) {
return locks.size()/2 + 1;
}
@Override
protected long calcLockWaitTime(long remainTime) {
return Math.max(remainTime / locks.size(), 1);
}
@Override
public void unlock() {
unlockInner(locks);
}
}
複製代碼
經過構造函數咱們能夠看出,要構造出一個RedissonRedLock,須要至少一個RLock實例,具體實現須要到父類RedissonMultiLock中查看,父類實際上是將構造函數傳入的RLock添加到了一個List的列表中。至於什麼是RLock呢,這是Redisson中對鎖的最高層的抽象,它的實現類包括RedissonWriteLock、RedissonReadLock、RedissonFairLock,固然咱們正在分析的RedissonMultiLock也是它的實現類。
假設咱們有三個Redis節點,
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://localhost:5378")
.setPassword("").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://localhost:5379")
.setPassword("").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://localhost:5380")
.setPassword("").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);
String lockKey = "REDLOCK";
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
if (redLock.tryLock(10, 5, TimeUnit.SECONDS)) {
//TODO if get lock success, do something;
}
}catch(Exception e){
}
複製代碼
RedissonRedLock徹底的按照上文咱們介紹的Redlock的算法來實現的,經過在三個不一樣節點上分別獲取鎖,來構造一個Redlock,咱們再來分析一下具體的tryLock的實現,這個方法是在RedissonRedLock的父類RedissonMultiLock實現的:
/**
*
* @param waitTime the maximum time to acquire the lock
* @param leaseTime lease time
* @param unit time unit
* @return
* @throws InterruptedException
*/
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long newLeaseTime = -1;
if (leaseTime != -1) {
if (waitTime == -1) {
newLeaseTime = unit.toMillis(leaseTime);
} else {
//等待時間和加鎖時間都不爲-1時,newLeaseTime爲waitTime時間的兩倍
newLeaseTime = unit.toMillis(waitTime)*2;
}
}
long time = System.currentTimeMillis();
long remainTime = -1;
if (waitTime != -1) {
remainTime = unit.toMillis(waitTime);
}
//子類重寫的方法,Math.max(remainTime / locks.size(), 1),waitTime除以節點的個數,與1取較大值
//Math.max(等待時間的毫秒數/節點個數,1)
long lockWaitTime = calcLockWaitTime(remainTime);
//調用子類重寫方法,locks.size() - minLocksAmount(locks)
//minLocksAmount(locks) => locks.size()/2 + 1
//failedLocksLimit = 鎖的個數 -(鎖的個數/2 + 1)
//即爲Redis節點個數的少數(N/2-1),獲取鎖容許失敗個數的最大閥值爲N/2-1,超過這個值,就認定Redlock加鎖失敗
int failedLocksLimit = failedLocksLimit();
List<RLock> acquiredLocks = new ArrayList<>(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 {
//加鎖嘗試的等待時間爲:等待時長的毫秒數 與 Math.max(等待時長的毫秒數/節點個數,1)之間的較小值
long awaitTime = Math.min(lockWaitTime, remainTime);
//挨個嘗試加鎖,鎖的有效期爲等待時長的2倍
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
} catch (RedisResponseTimeoutException e) {
//申請鎖超時就嘗試解鎖
unlockInner(Arrays.asList(lock));
lockAcquired = false;
} catch (Exception e) {
lockAcquired = false;
}
if (lockAcquired) {
//加鎖成功的節點就放到acquiredLocks這個list中
acquiredLocks.add(lock);
} else {
//加鎖失敗,須要判斷失敗的個數是否已經達到了N/2-1個,達到了的話,再來一個失敗的,那麼這個
//redlock就加鎖失敗了,後面的就能夠不用再試了
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
break;
}
if (failedLocksLimit == 0) {
unlockInner(acquiredLocks);
if (waitTime == -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;
}
}
}
//redLock申請成功,爲每一個節點上的鎖設置過時時間
if (leaseTime != -1) {
List<RFuture<Boolean>> futures = new ArrayList<>(acquiredLocks.size());
for (RLock rLock : acquiredLocks) {
RFuture<Boolean> future = ((RedissonLock) rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
futures.add(future);
}
for (RFuture<Boolean> rFuture : futures) {
rFuture.syncUninterruptibly();
}
}
return true;
}
複製代碼
加鎖的代碼你們能夠經過看註釋,根據上文的算法原理解析,應該能夠輕鬆的看懂,接下來再看一下解鎖:
protected RFuture<Void> unlockInnerAsync(Collection<RLock> locks, long threadId) {
if (locks.isEmpty()) {
return RedissonPromise.newSucceededFuture(null);
}
RPromise<Void> result = new RedissonPromise<Void>();
AtomicInteger counter = new AtomicInteger(locks.size());
for (RLock lock : locks) {
lock.unlockAsync(threadId).onComplete((res, e) -> {
if (e != null) {
result.tryFailure(e);
return;
}
//在全部Redis節點上都完成解鎖動做後
if (counter.decrementAndGet() == 0) {
result.trySuccess(null);
}
});
}
return result;
}
複製代碼
Redlock雖然是Redis做者踢出的一種實現分佈式鎖的算法,可是,它也並非一個實現分佈式鎖的完美算法,若是對Redlock的缺點有興趣,你們能夠看一下Martin Kleppmann批判Redlock的文章(叫Martin的好像都挺牛,Martin Fowler) martin.kleppmann.com/2016/02/08/…
不得不佩服大師,系統各類狀況、場景都考慮的很是全面,向大師致敬,咱們在學習知識的同時,也須要本身多思考,多總結。