前言
主流的分布式鎖一般有三種實現方式:
數據庫樂觀鎖
基于Redis的分布式鎖
基于ZooKeeper的分布式鎖
之前我在博客上寫過關于mysql和redis實現分布式鎖的具體方案: https://www.cnblogs.com/wang-meng/p/10226618.html 里面主要是從實現原理出發。
這次【分布式鎖】系列文章主要是深入redis客戶端reddision源碼和zk 這兩種分布式鎖的實現原理。
可靠性
首先,為了確保分布式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:
互斥性。在任意時刻,只有一個客戶端能持有鎖。
不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證后續其他客戶端能加鎖。
具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
解鈴還須系鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
Redisson加鎖原理
redisson是一個非常強大的開源的redis客戶端框架, 官方地址: https://redisson.org/
使用起來很簡單,配置好maven和連接信息,這里直接看代碼實現:
RLock lock = redisson.getLock("anyLock");
lock.lock();
lock.unlock();
redisson具體的執行加鎖邏輯都是通過lua腳本來完成的,lua腳本能夠保證原子性。
先看下RLock初始化的代碼:
public class Redisson implements RedissonClient {
@Override
public RLock getLock(String name) {
return new RedissonLock(connectionManager.getCommandExecutor(), name);
}
}
public class RedissonLock extends RedissonExpirable implements RLock {
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.id = commandExecutor.getConnectionManager().getId();
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = id + ":" + name;
}
首先看下RedissonLock
的id返回的是一個UUID對象,每個機器都對應一個自己的id屬性,id
值就類似于:"8743c9c0-0795-4907-87fd-6c719a6b4586"
接著往后看lock()
的代碼實現:
public class RedissonLock extends RedissonExpirable implements RLock {
@Override
public void lock() {
try {
lockInterruptibly();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
lockInterruptibly(-1, null);
}
@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
// 獲取當前線程id
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
try {
while (true) {
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
if (ttl >= 0) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
unsubscribe(future, threadId);
}
}
<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));
}
}
這里省略了一些中間代碼,這里主要看tryAcquire()
方法,這里傳遞的過期時間為-1,然后就是當前的線程id,接著就是核心的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; " +
KEYS[1]
參數是:“anyLock”
ARGV[2]
是:“id + ":" + threadId”
首先用的exists
判斷redis中是否存在當前key,如果不存在就等于0,然后執行hset
指令,將“anyLock id:threadId 1”存儲到redis中,最終redis存儲的數據類似于:
{
"8743c9c0-0795-4907-87fd-6c719a6b4586:1":1
}
偷偷說一句,最后面的一個1 是為了后面可重入做的計數統計,后面會有講解到。
接著往下看,然后使用pexpire
設置過期時間,默認使用internalLockLeaseTime
為30s。最后返回為null,即時加鎖成功。
Redisson 可重入原理
我們看下鎖key存在的情況下,同一個機器同一個線程如何加鎖的?
"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]);",
ARGV[2]
是:“id + ":" + threadId”
如果同一個機器同一個線程再次來請求,這里就會是1,然后執行hincrby
, hset設置的value+1 變成了2,然后繼續設置過期時間。
同理,一個線程重入后,解鎖時value - 1
Redisson watchDog原理
如果一個場景:現在有A,B在執行業務,A加了分布式鎖,但是生產環境是各種變化的,如果萬一A鎖超時了,但是A的業務還在跑。而這時由于A鎖超時釋放,B拿到鎖,B執行業務邏輯。這樣分布式鎖就失去了意義?
所以Redisson 引入了watch dog的概念,當A獲取到鎖執行后,如果鎖沒過期,有個后臺線程會自動延長鎖的過期時間,防止因為業務沒有執行完而鎖過期的情況。
我們接著來看看具體實現:
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}
Long ttlRemaining = future.getNow();
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
當我們tryLockInnerAsync
執行完之后,會添加一個監聽器,看看監聽器中的具體實現:
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));
}
這里面調度任務每隔10s鐘執行一次,lua腳本中是續約過期時間,使得當前線程持有的鎖不會因為過期時間到了而失效
Redisson 互斥性原理
還是看上面執行加鎖的lua腳本,最后會執行到:
"return redis.call('pttl', KEYS[1]);",
返回鎖還有多久時間過期,我們繼續接著看代碼:
@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);
// 返回ttl說明加鎖成功,不為空則是加鎖失敗
if (ttl == null) {
return;
}
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
try {
// 死循環去嘗試獲取鎖
while (true) {
// 再次嘗試加鎖
ttl = tryAcquire(leaseTime, unit, threadId);
// 如果ttl=null說明搶占鎖成功
if (ttl == null) {
break;
}
// ttl 大于0,搶占鎖失敗,這個里面涉及到Semaphore,后續會講解
if (ttl >= 0) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
unsubscribe(future, threadId);
}
}
Redisson鎖釋放原理
直接看lua代碼:
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判斷鎖key值是否存在
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
// 判斷當前機器、當前線程id對應的key是否存在
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 計數器數量-1 可重入鎖
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 如果計數器大于0,說明還在持有鎖
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
// 使用del指令刪除key
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
總結
一圖總結: