1、單機鎖
考慮在并發(fā)場景并且存在競態(tài)的狀況下,我們就要實現(xiàn)同步機制了,最簡單的同步機制就是加鎖。
加鎖可以幫我們鎖住資源,如內(nèi)存中的變量,或者鎖住臨界區(qū)(線程中的一段代碼),使得同一個時刻只有一個線程能訪問某一個區(qū)域。
如果是單實例(單進程部署),那么單機鎖就可以滿足我們的要求了,如synchronized,ReentrantLock。
因為在一個進程中的不同線程可以共享這個鎖。
2、分布式鎖
但是如果場景來到了分布式系統(tǒng)呢?
分布式系統(tǒng)部署在不同的機器上,或者只是簡單的多進程部署。這樣各個進程之間無法共享同一個鎖。
這時候我們要加分布式鎖。
分布式鎖大概就是這么一個東西:通過共享的存儲緩存一個狀態(tài)值,用狀態(tài)值的變化標識鎖的占用和釋放。
可以通過mysql,redis,zk等實現(xiàn)分布式鎖,這里我們實現(xiàn)一個redis的。如果你用java其實使用zk會很簡單。
3、為什么redis能用來實現(xiàn)分布式鎖?
1)Redis是單進程單線程模式
redis實現(xiàn)為單進程單線程模式,這樣多個客戶端并不存在競態(tài)關(guān)系。
2)原子性原語
redis提供了可以實現(xiàn)原子操作的原語如setnx、getset等。
setnx
1)SETNX key value
將 key 的值設(shè)為 value ,當且僅當 key 不存在。
若給定的 key 已經(jīng)存在,則 SETNX 不做任何動作。
SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。
可用版本:
>= 1.0.0
時間復(fù)雜度:
O(1)
返回值:
設(shè)置成功,返回 1 。
設(shè)置失敗,返回 0 。
復(fù)制
getset
GETSET key value
將給定 key 的值設(shè)為 value ,并返回 key 的舊值(old value)。
當 key 存在但不是字符串類型時,返回一個錯誤。
可用版本:
>= 1.0.0
時間復(fù)雜度:
O(1)
返回值:
返回給定 key 的舊值。
當 key 沒有舊值時,也即是, key 不存在時,返回 nil 。
復(fù)制
4、實現(xiàn)
package com.xiaoju.dqa.fusor.utils;
import com.xiaoju.dqa.fusor.client.RedisClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DistributeLockUtil {
// 鎖超時時間, 防止死鎖
private static final long LOCK_TIMEOUT = 60;
@Autowired
private RedisClient redisClient;
private boolean locked = false;
public boolean lock(String key) {
String expireTime = String.valueOf(System.currentTimeMillis() + LOCK_TIMEOUT * 1000);
/*
* setnx 返回1
* 說明: 1)key不存在, 2)成功寫入鎖, 并更新鎖的生存時間
* 也就是get鎖
* */
if (redisClient.setnx(key, expireTime) == 1) {
locked = true;
return true;
}
/*
* 沒有g(shù)et鎖, 下面進入判斷鎖超時邏輯
* */
String currentExpireTime = redisClient.get(key);
/*
* 鎖生存時間已經(jīng)過了, 說明鎖已經(jīng)超時
* */
if (Long.parseLong(currentExpireTime) < System.currentTimeMillis()) {
String oldValueStr = redisClient.getSet(key, expireTime);
/*
* 判斷鎖生存時間和你改的寫那個時間是否相等
* 相當于你競爭了一個更新鎖
* */
if (oldValueStr.equals(currentExpireTime)) {
locked = true;
return true;
}
}
return false;
}
public void release(String key) {
if (locked) {
redisClient.del(key);
locked = false;
}
}
}
復(fù)制
5、死鎖
為了解決死鎖,這里設(shè)置了鎖的超時時間。
private static final long LOCK_TIMEOUT = 60;
復(fù)制
并通過setnx時更新鎖生存時間來維護鎖超時的判定。
String expireTime = String.valueOf(System.currentTimeMillis() + LOCK_TIMEOUT * 1000);
...
if (redisClient.setnx(key, expireTime) == 1) {
...
}
...
String oldValueStr = redisClient.getSet(key, expireTime);
...
復(fù)制
為什么要使用這種方式,而不是expire呢?
因為setnx和expire不能作為一個原子性的操作存在,設(shè)想如果setnx之后,在執(zhí)行expire之前出現(xiàn)了異常,那么鎖將沒有超時時間。也就是死鎖。
6、解決鎖超時引入的競態(tài)
設(shè)想三個客戶端,C0,C1,C2
如果C0持有鎖并且崩潰,鎖沒有釋放。
C1和C2同時發(fā)現(xiàn)了鎖超時。
然后都通過getset去拿到了舊值,在對比了舊值和之前值之后,如果相等,那么說明“我”成功修改了舊值,那么我就拿到了鎖。
7、 時鐘同步
我們看到foo.lock的value值為時間戳,所以要在多客戶端情況下,保證鎖有效,一定要同步各服務(wù)器的時間,如果各服務(wù)器間,時間有差異。時間不一致的客戶端,在判斷鎖超時,就會出現(xiàn)偏差,從而產(chǎn)生競爭條件。 鎖的超時與否,嚴格依賴時間戳,時間戳本身也是有精度限制,假如我們的時間精度為秒,從加鎖到執(zhí)行操作再到解鎖,一般操作肯定都能在一秒內(nèi)完成。這樣的話,我們上面的CASE,就很容易出現(xiàn)。所以,最好把時間精度提升到毫秒級。這樣的話,可以保證毫秒級別的鎖是安全的。
8、一些處理不了的情況
設(shè)想三個客戶端,C0,C1,C2
如果C0持有鎖很長,鎖已經(jīng)超時。這時候有C1,C2判斷鎖超時了,然后通過超時競爭,C1拿到了鎖。
這時C0醒了過來,刪除了C1的鎖。
這時,C1認為自己獨占了鎖,其他的進程也進入了競爭鎖的情況
對于這種情況,這里是沒有提供解決辦法的。文章來源:http://www.zghlxwxcb.cn/news/detail-518333.html
思路是:你降級你的鎖,比如給你的鎖加上uuid,對不同的業(yè)務(wù)或者不同的session加上對應(yīng)粒度的鎖。文章來源地址http://www.zghlxwxcb.cn/news/detail-518333.html
到了這里,關(guān)于分布式鎖的實現(xiàn)(redis)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!