前言
在我們日常開發中,難免會遇到要加鎖的情景。例如扣除產品庫存,首先要從資料庫中取出庫存,進行庫存判斷,再減去庫存。這一波操作明顯不符合原子性,如果程式碼塊不加鎖,很容易因為併發導致超賣問題。我們們的系統如果是單體架構,那我們使用本地鎖就可以解決問題。如果是分散式架構,就需要使用分散式鎖。
方案
使用 SETNX 和 EXPIRE 命令
if (setnx("item_1_lock", 1)) {
expire("item_1_lock", 30);
try {
... 邏輯
} catch {
...
} finally {
del("item_1_lock");
}
}
這種方法看起來可以解決問題,但是有一定的風險,因為 SETNX
和 EXPIRE
這波操作是非原子性的,如果 SETNX
成功之後,出現錯誤,導致 EXPIRE
沒有執行,導致鎖沒有設定超時時間形成死鎖。
針對這種情況,我們可以使用 lua 指令碼來保持操作原子性,保證 SETNX
和 EXPIRE
兩個操作要麼都成功,要麼都不成功。
if (redis.call('setnx', KEYS[1], ARGV[1]) < 1)
then return 0;
end;
redis.call('expire', KEYS[1], tonumber(ARGV[2]));
return 1;
通過這樣的方法,我們初步解決了競爭鎖的原子性問題,雖然其他功能還未實現,但是應該不會造成死鎖 ???。
Redis 2.6.12 以上可靈活使用 SET 命令
if (set("item_1_lock", 1, "NX", "EX", 30)) {
try {
... 邏輯
} catch {
...
} finally {
del("item_1_lock");
}
}
改進後的方法不需要藉助 lua 指令碼就解決了 SETNX
和 EXPIRE
的原子性問題。現在我們再仔細琢磨琢磨,如果 A 拿到了鎖順利進入程式碼塊執行邏輯,但是由於各種原因導致超時自動釋放鎖。在這之後 B 成功拿到了鎖進入程式碼塊執行邏輯,但此時如果 A 執行邏輯完畢再來釋放鎖,就會把 B 剛獲得的鎖釋放了。就好比用自己家的鑰匙開了別家的門,這是不可接受的。
為了解決這個問題我們可以嘗試在 SET
的時候設定一個鎖標識,然後在 DEL
的時候驗證當前鎖是否為自己的鎖。
String value = UUID.randomUUID().toString().replaceAll("-", "");
if (set("item_1_lock", value, "NX", "EX", 30)) {
try {
... 邏輯
} catch {
...
} finally {
... lua 指令碼保證原子性
}
}
if (redis.call('get', KEYS[1]) == ARGV[1])
then return redis.call('del', KEYS[1])
else return 0
end
到這裡,我們終於解決了競爭鎖的原子性問題和誤刪鎖問題。但是鎖一般還需要支援可重入、迴圈等待和超時自動續約等功能點。下面我們學習使用一個非常好用的包來解決這些問題 ???。
入門 Redisson
Redission 的鎖,實現了可重入和超時自動續約功能,它都幫我們封裝好了,我們只要按照自己的需求呼叫它的 API 就可以輕鬆實現上面所提到的幾個功能點。詳細功能可以檢視 Redisson 文件
在專案中安裝 Redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.2</version>
</dependency>
implementation 'org.redisson:redisson:3.13.2'
用 Maven 或者 Gradle 構建,目前最新版本為 3.13.2
,也可以在這裡 Redisson 找到你需要的版本。
簡單嘗試
RedissonClient redissonClient = Redisson.create();
RLock lock = redissonClient.getLock("lock");
boolean res = lock.lock();
if (res) {
try {
... 邏輯
} finally {
lock.unlock();
}
}
Redisson 將底層邏輯全部做了一個封裝 ?,我們無需關心具體實現,幾行程式碼就能使用一把完美的鎖。下面我們簡單折騰折騰原始碼 ???。
加鎖
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
// 獲取當前執行緒 id
long threadId = Thread.currentThread().getId();
// 嘗試獲取鎖
Long ttl = tryAcquire(leaseTime, unit, threadId);
// 獲取成功直接返回
if (ttl == null) {
return;
}
// 獲取失敗,訂閱鎖對應的頻道
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
while (true) {
// 再次嘗試獲取鎖
ttl = tryAcquire(leaseTime, unit, threadId);
// 獲取成功直接返回
if (ttl == null) {
break;
}
// 等待 ttl 時間後繼續獲取
if (ttl >= 0) {
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
// 取消頻道訂閱
unsubscribe(future, threadId);
}
}
獲取鎖
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
// 如果設定了鎖過期時間,則按普通方式獲取鎖
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// 如果沒有設定鎖過期時間,則開啟自動續約功能,先設定 30 秒過期時間
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
// 有錯誤直接返回
if (e != null) {
return;
}
// 獲取鎖
if (ttlRemaining == null) {
// 開啟自動續約
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
/**
* 鎖不存在,使用 hincrby 建立新 hash 表以及給鎖計數自增 1,並設定過期時間
* 鎖存在並且屬於當前執行緒,給鎖計數自增 1,並設定過期時間
* 鎖存在但是不屬於當前執行緒,返回鎖過期時間
**/
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', 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.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
刪除鎖
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
// 解鎖邏輯
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.onComplete((opStatus, e) -> {
// 取消重新整理過期時間的定時任務
cancelExpirationRenewal(threadId);
if (e != null) {
result.tryFailure(e);
return;
}
// 解鎖執行緒和鎖不是同一個執行緒,拋錯
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;
}
result.trySuccess(null);
});
return result;
}
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
/**
* 判斷鎖是否屬於當前執行緒,不屬於直接返回
* 鎖計數減去 1,如果鎖計數還大於 0,則設定過期時間,否則釋放鎖併發布鎖釋放訊息
**/
"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.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
總結
使用 Redis 做分散式鎖來解決併發問題仍存在一些困難,也有很多需要注意的點,我們應該正確評估系統的體量,不能為了使用某項技術而用。要完全解決併發問題,仍需要在資料庫層面做功夫。???