redisson實現分佈式鎖,是通過一個hash結構存儲的,形式如下:
MY_LOCK 3444e697-8ab7-43ba-bfb5-28a38aeb1f02:1 1
MY_LOCK 是我獲取分佈式鎖的時候,通過redisson.getLock(“MY_LOCK”)定義的,它作爲hash結構的key
3444e697-8ab7-43ba-bfb5-28a38aeb1f02:1 作爲hash結構的一個field,冒號後面的1是線程id。
1 是field的值,作爲當前線程重入鎖的次數。
每次通過判斷所得key和當前線程對應的field是否存在來判斷是否可以獲取鎖。以上這些內容都可以在調試源碼的過程中看到具體細節。我是從org.redisson.RedissonLock#tryLock() 作爲入口,羅列了比較關鍵的代碼:
先整體看看加鎖和給鎖續期:
private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, long threadId) {
// ......
// 加鎖方法返回異步執行的結果,結果泛型爲Boolean,這個結果是通過RedisCommands.EVAL_NULL_BOOLEAN轉換的,如果結果爲null, 返回true,否則返回false.
RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
// 給加鎖返回的異步結果註冊執行完成時的回調方法onComplete,是否需要給鎖續期就是根據加鎖結果判斷的,
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
// 如果加鎖方法返回true, 需要通過scheduleExpirationRenewal(threadId)方法給鎖續期
if (ttlRemaining) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
加鎖方法內部實現
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"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]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
列一下我調試時這段lua的參數
- KEYS[1]是MY_LOCK, 自定義的鎖的key
- ARG[1] 是標識線程的哈希結構的field :81c39aea-b41f-4505-ae92-0f567fdc8651:1
- ARG[2] 是看門狗默認的超時時間
解釋下這段lua的三個分支都做了什麼事情:
如果MY_LOCK不存在,則創該建哈希結構,key爲MY_LOCK,field爲線程標記,value爲1,過期時間30秒, 返回null, 這是首次加鎖的情況。前面說到返回null的時候加鎖方法會轉爲true, 此時回調方法中會去設置定時給鎖續期的事情。
如果MY_LOCK這個key存在,並且MY_LOCK的線程field的存在,則給field增加1. 重新設置MY_LOCK的超時時間30s。返回null,這是鎖重入時的情況,每次重入給線程field的值加1. 此時的返回結果也轉爲true, 並去續期鎖。
否則,就是MY_LOCK存在,但是field不存在,表明當前線程已經不持有鎖了,返回key的剩餘生存時間. 此時結果不爲null, 返回的RFuture 結果爲false. 不會再去給鎖續期。
補充一些redis知識:
exists 檢查給定key是否存在
hexists 檢查希表key
中,給定域field
是否存在
pexpire 設置過期時間,單位毫秒
expire 單位秒
hincrby 對hash的field的value增加,只能操作數值型
pttl 返回給定key
的剩餘生存時間,單位毫秒
ttl 單位秒
EVAL script numkeys key [key …] arg [arg …]
- script: 參數是一段 Lua 5.1 腳本程序。腳本不必(也不應該)定義爲一個 Lua 函數。
- numkeys: 用於指定鍵名參數的個數。
- key [key …]: 從 EVAL 的第三個參數開始算起,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數可以在 Lua 中通過全局變量 KEYS 數組,用 1 爲基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。
- arg [arg …]: 附加參數,在 Lua 中通過全局變量 ARGV 數組訪問,訪問的形式和 KEYS 變量類似( ARGV[1] 、 ARGV[2] ,諸如此類)。
整體看看給鎖續期
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 創建Timeout,這裏創建的是HashedWheelTimeout,這是netty的HashedWheelTimer的一個內部類,HashedWheelTimer是一個優秀的定時任務實現方案,給鎖續期就是定時任務去判斷的。
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 這裏真正的去更新鎖的時間的操作
RFuture<Boolean> future = renewExpirationAsync(threadId);
// 根據更新鎖返回的結果決定是否需要繼續給鎖續期
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
// 如果給鎖續期返回成功,調用自己重新添加續期任務,因爲之前的任務可能會過期
if (res) {
// reschedule itself
renewExpiration();
}
});
}
// 定時任務的執行時機是設置的看門狗超時時間的三分之一,默認看門狗超時時間30s的話那任務就是10s執行一次。
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
看看給鎖續期方法內部
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.<Object>singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
如果MY_LOCK的線程field存在,說明當前線程還持有鎖,需要重新設置過期時間爲看門狗設置的時間,返回1。否則返回0.
整體看看解鎖
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
// 解鎖
RFuture<Boolean> future = unlockInnerAsync(threadId);
// 解鎖完成後回調
future.onComplete((opStatus, e) -> {
// 如果發生了異常,取消自動續期,並組裝失敗結果,結束
if (e != null) {
cancelExpirationRenewal(threadId);
result.tryFailure(e);
return;
}
// 如果解鎖返回null,也就是lua中,沒有線程對應的鎖存在,組裝帶有異常的失敗信息,結束
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
// 正常的解鎖流程,在這裏取消自動續期,返回成功結果
cancelExpirationRenewal(threadId);
result.trySuccess(null);
});
return result;
}
解鎖內部實現
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"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;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
我調試時,這段lua的參數是
KEYS:
- MY_LOCK
- redisson_lock__channel:{MY_LOCK}
ARGV:
- 0
- 30000
- 81c39aea-b41f-4505-ae92-0f567fdc8651:1
解釋下這段lua做了什麼事情:
如果MY_LOCK的線程field不存在,返回null,結束
如果MY_LOCK的線程field存在,給value 減一,也就是重入鎖退出一次。如果value還大於0,重新設置過期時間,返回0; 否則刪除MY_LOCK, 往redisson_lock__channel:{MY_LOCK}通道發佈一條消息0,返回1.