1. 前言
之前寫過一篇《Redis分散式鎖的實現》的文章,主要介紹的Redis分散式鎖的原始性實現,核心是基於setnx
來加鎖,以及使用lua
保障事務的原子性等。但畢竟比較原始,需要根據不同的應用場景做不同的程式碼實現,也容易考慮不周。當時文章中就有提到 Redisson
框架,剛好最近工作中又用的比較多,這次就著重介紹。
Redisson 是架設在 Redis基礎上的一個Java開發框架,底層基於 Netty框架,為使用者提供了一系列具有分散式特性的常用工具類。Redisson的功能非常豐富,具體可參考 github中文wiki,但本文只介紹 Redisson分散式鎖的功能。
2. 普通可重入鎖
2.1. 使用示例
在SpringBoot專案通過Redisson來加鎖非常容易,不需要像之前文章中一樣寫一大堆程式碼,框架遮蔽掉了很多細節。如下例:
Config config = new Config();
config.useSingleServer().setAddress("redis://ip:port").setPassword("password").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
RLock lock = redissonClient.getLock("LOCK_KEY");
long waitTime=500L;
long leaseTime=15000L;
boolean isLock;
try {
isLock = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
if (isLock) {
// do something ...
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
注意程式碼中 Config 並無限制,示例中是Redis單節點連線,但實際上可以是哨兵模式、叢集模式、主從模式等。
2.2. 原始碼講解
前面例子中加鎖用到了RLock
介面,這裡貼一下原始碼:
org.redisson.api.RLock.java
public interface RLock extends Lock, RLockAsync {
String getName();
void lockInterruptibly(long var1, TimeUnit var3) throws InterruptedException;
boolean tryLock(long var1, long var3, TimeUnit var5) throws InterruptedException;
void lock(long var1, TimeUnit var3);
boolean forceUnlock();
boolean isLocked();
boolean isHeldByThread(long var1);
boolean isHeldByCurrentThread();
int getHoldCount();
long remainTimeToLive();
}
對於可重入鎖,介面對應的實現方法在org.redisson.RedissonLock
類裡面,原始碼就不貼了,可以看到落到Redis時,實際的“加鎖”和“解鎖”過程也是一段lua指令碼。
1、加鎖
lua
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end
return redis.call('pttl', KEYS[1]);
引數解釋:
KEYS[1]
:被鎖資源名。ARGV[1]
:過期時間。ARGV[2]
:當前程式標識(UUID + 當前threadId)。
加鎖的邏輯,是在redis中存入一個Set型別值。資源一旦被鎖,初次設定Value為1,也只有當前程式可重複加鎖,即Value往上加1。
2、解鎖
lua
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end
return nil;
引數解釋:
KEYS[1]
:被鎖資源名。KEYS[2]
:解鎖時廣播通道名。ARGV[1]
:解鎖時廣播通道訊息(值為0L)。ARGV[2]
:過期時間。ARGV[3]
:當前程式標識(UUID + 當前threadId)。
解鎖的邏輯,是先判斷被鎖資源名是否存在,如果存在則給Value減1,當Value為0時,則刪除Key,並向指定通道廣播訊息。
廣播通道的設計很有亮點,當多個執行緒同時競爭鎖時,未搶到鎖的執行緒無需無效輪詢,只需訂閱一個通道。當鎖釋放時,在通道中廣播訊息,通知那些等待獲取鎖的執行緒現在可以獲得鎖了,那些執行緒再去競爭鎖,避免效能資源的浪費。
3、看門狗機制
如果拿到分散式鎖的節點當機,且這個鎖正好處於鎖住的狀態時,會出現鎖死的狀態,為了避免這種情況的發生,鎖都會設定一個過期時間。即前面在加鎖時傳入的leaseTime
。某些應用場景中,如果在指定時間中,我們尚未完成業務,此時就需要給鎖“續期”。如果整個過程完全可控,可以在程式中手動給鎖續期。但如果希望能自動續期,就可以用到Redisson的Wath Dog(看門狗)機制。
Redisson提供了一個監控鎖的看門狗,它的作用是在Redisson例項被關閉前,不斷的延長鎖的有效期,也就是說,如果一個拿到鎖的執行緒一直沒有完成邏輯,那麼看門狗會幫助執行緒不斷的延長鎖超時時間,鎖不會因為超時而被釋放。預設情況下,看門狗的續期時間是30s,也可以通過修改Config.lockWatchdogTimeout來另行指定。
下面就是加鎖的原始碼,注意,在呼叫加鎖方法時,如果想用看門狗,則傳leaseTime
值為-1L
。如果給leaseTime設定了有效值,那麼看門狗就不會生效,鎖不會自動續期,而是在你指定的時間後自動解鎖。
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1L) {
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining == null) {
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
}
3. RedLock 紅鎖
3.1. 概念說明
也是在之前的那一篇文章中,也提到了RedLock,中文直譯“紅鎖”。其實那篇文章已經介紹過了,這裡再介紹一下。前面用redis實現分散式鎖時存在漏洞,具體場景:
客戶端A在Redis master節點申請鎖。但master在將儲存的key同步到slave上之前崩潰了,然後slave晉升為master。而客戶端B申請一個客戶端A已經持有的資源的鎖。然後呢?然後呢?出問題啦,客戶端A和B都能申請到同一個鎖。
RedLock是Redis官方提出的演算法,具體流程包括:
- 獲取當前時間。
- 依次N個節點獲取鎖,並設定響應超時時間,防止單節點獲取鎖時間過長。
- 鎖有效時間=鎖過期時間-獲取鎖耗費時間,如果第2步驟中獲取成功的節點數大於
N/2+1,且鎖有效時間大於0,則獲得鎖成功。 - 若獲得鎖失敗,則向所有節點釋放鎖。
簡單點說,就是在鎖過期時間內,如果半數以上的節點成功獲取到了鎖,則說明獲取鎖成功。這個有點像ZooKeeper的選舉機制。這裡講講Redisson中的實現方法。
3.2. 使用示例
Redisson關於RedLock的使用程式碼上及其簡單,只是將幾個鎖組合成一個“大鎖”,然後再正常使用“大鎖”的加鎖/解鎖。
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://ip1:port1")
.setPassword("password1").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://ip2:port2")
.setPassword("password2").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://ip3:port3")
.setPassword("password3").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);
String lockKey = "REDLOCK_KEY";
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
long waitTime=500L;
long leaseTime=15000L;
try {
isLock = redLock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
if (isLock) {
// do something ...
}
} catch (Exception e) {
... ...
} finally {
redLock.unlock();
}
注意程式碼中 Config 並無限制,示例中是Redis單節點連線,但實際上可以是哨兵模式、叢集模式、主從模式等。
3.3. 原始碼講解
在講Redisson的 RedLock
(紅鎖)之前,先講 MultiLock
(聯鎖),原因先看 RedissonRedLock
原始碼,完全是繼承 RedissonMultiLock
的所有功能。
RedissonRedLock.java
public class RedissonRedLock extends RedissonMultiLock {
public RedissonRedLock(RLock... locks) {
super(locks);
}
protected int failedLocksLimit() {
return this.locks.size() - this.minLocksAmount(this.locks);
}
protected int minLocksAmount(List<RLock> locks) {
return locks.size() / 2 + 1;
}
protected long calcLockWaitTime(long remainTime) {
return Math.max(remainTime / (long)this.locks.size(), 1L);
}
public void unlock() {
this.unlockInner(this.locks);
}
}
RedissonMultiLock.java核心程式碼
// 加鎖
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long newLeaseTime = -1L;
if (leaseTime != -1L) {
if (waitTime == -1L) {
newLeaseTime = unit.toMillis(leaseTime);
} else {
newLeaseTime = unit.toMillis(waitTime) * 2L;
}
}
long time = System.currentTimeMillis();
long remainTime = -1L;
if (waitTime != -1L) {
remainTime = unit.toMillis(waitTime);
}
long lockWaitTime = this.calcLockWaitTime(remainTime);
int failedLocksLimit = this.failedLocksLimit();
List<RLock> acquiredLocks = new ArrayList(this.locks.size());
ListIterator iterator = this.locks.listIterator();
while(iterator.hasNext()) {
RLock lock = (RLock)iterator.next();
boolean lockAcquired;
try {
if (waitTime == -1L && leaseTime == -1L) {
lockAcquired = lock.tryLock();
} else {
long awaitTime = Math.min(lockWaitTime, remainTime);
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
} catch (RedisResponseTimeoutException var21) {
this.unlockInner(Arrays.asList(lock));
lockAcquired = false;
} catch (Exception var22) {
lockAcquired = false;
}
if (lockAcquired) {
acquiredLocks.add(lock);
} else {
if (this.locks.size() - acquiredLocks.size() == this.failedLocksLimit()) {
break;
}
if (failedLocksLimit == 0) {
this.unlockInner(acquiredLocks);
if (waitTime == -1L) {
return false;
}
failedLocksLimit = this.failedLocksLimit();
acquiredLocks.clear();
while(iterator.hasPrevious()) {
iterator.previous();
}
} else {
--failedLocksLimit;
}
}
if (remainTime != -1L) {
remainTime -= System.currentTimeMillis() - time;
time = System.currentTimeMillis();
if (remainTime <= 0L) {
this.unlockInner(acquiredLocks);
return false;
}
}
}
if (leaseTime != -1L) {
List<RFuture<Boolean>> futures = new ArrayList(acquiredLocks.size());
Iterator var24 = acquiredLocks.iterator();
while(var24.hasNext()) {
RLock rLock = (RLock)var24.next();
RFuture<Boolean> future = ((RedissonLock)rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
futures.add(future);
}
var24 = futures.iterator();
while(var24.hasNext()) {
RFuture<Boolean> rFuture = (RFuture)var24.next();
rFuture.syncUninterruptibly();
}
}
return true;
}
// 解鎖
public void unlock() {
List<RFuture<Void>> futures = new ArrayList(this.locks.size());
Iterator var2 = this.locks.iterator();
while(var2.hasNext()) {
RLock lock = (RLock)var2.next();
futures.add(lock.unlockAsync());
}
var2 = futures.iterator();
while(var2.hasNext()) {
RFuture<Void> future = (RFuture)var2.next();
future.syncUninterruptibly();
}
}
RedissonRedLock.java
中重寫了 RedissonMultiLock.java
裡的幾個方法:
- failedLocksLimit:MultiLock中返回
0
,RedLock中返回locks.size() / 2 - 1
。 - calcLockWaitTime:MultiLock中返回
remainTime
,RedLock中返回Math.max(remainTime / (long)this.locks.size(), 1L)
。
通過原始碼容易看到,Redisson中的 RedLock演算法完全是基於 MultiLock實現的。Redisson 支援這種“聯合鎖”的概念,將多個 RLock鎖放入一個 ArrayList中,然後開始遍歷加鎖。只不過 MultiLock的要求比較苛刻,List中的所有的 RLock加鎖時,不能存在任何加鎖失敗的,即 failedLocksLimit=0。而 RedLock要求放鬆一點,只要過半加鎖成功即可,即 failedLocksLimit = locks.size() / 2 - 1。但解鎖時,要求將整個 ArrayList 中的鎖都解一遍。