您好,登錄后才能下訂單哦!
分布式鎖是啥?
單機(jī)鎖的概念:我們正常跑的單機(jī)項(xiàng)目(也就是在tomcat下跑一個(gè)項(xiàng)目不配置集群)想要在高并發(fā)的時(shí)候加鎖很容易就可以搞定,java提供了很多的機(jī)制例如:synchronized、volatile、ReentrantLock等鎖的機(jī)制。
為啥需要分布式鎖:當(dāng)我們的項(xiàng)目比較龐大的時(shí)候,單機(jī)版的項(xiàng)目已經(jīng)不能滿足吞吐量的需求了,需要對(duì)項(xiàng)目做負(fù)載均衡,有可能還需要對(duì)項(xiàng)目進(jìn)行解耦拆分成不同的服務(wù),那么肯定是做成分布式的項(xiàng)目,分布式的項(xiàng)目因?yàn)槭遣煌某绦蚩刂?,所以使用java提供的鎖并不能完全保證并發(fā)需求,需要借助第三方的框架來(lái)實(shí)現(xiàn)對(duì)并發(fā)的阻塞控制,來(lái)滿足實(shí)際業(yè)務(wù)的需要。
一、使用分布式鎖要滿足的幾個(gè)條件:
1.系統(tǒng)是一個(gè)分布式系統(tǒng)(關(guān)鍵是分布式,單機(jī)的可以使用ReentrantLock或者synchronized代碼塊來(lái)實(shí)現(xiàn))
2.共享資源(各個(gè)系統(tǒng)訪問(wèn)同一個(gè)資源,資源的載體可能是傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)或者NoSQL)
3.同步訪問(wèn)(即有很多個(gè)進(jìn)程同事訪問(wèn)同一個(gè)共享資源。沒(méi)有同步訪問(wèn),誰(shuí)管你資源競(jìng)爭(zhēng)不競(jìng)爭(zhēng))
二、應(yīng)用的場(chǎng)景例子
管理后臺(tái)的部署架構(gòu)(多臺(tái)tomcat服務(wù)器+redis【多臺(tái)tomcat服務(wù)器訪問(wèn)一臺(tái)redis】+mysql【多臺(tái)tomcat服務(wù)器訪問(wèn)一臺(tái)服務(wù)器上的mysql】)就滿足使用分布式鎖的條件。多臺(tái)服務(wù)器要訪問(wèn)redis全局緩存的資源,如果不使用分布式鎖就會(huì)出現(xiàn)問(wèn)題。 看如下偽代碼:
long N=0L; //N從redis獲取值 if(N<5){ N++; //N寫(xiě)回redis }
上面的代碼主要實(shí)現(xiàn)的功能:
從redis獲取值N,對(duì)數(shù)值N進(jìn)行邊界檢查,自加1,然后N寫(xiě)回redis中。 這種應(yīng)用場(chǎng)景很常見(jiàn),像秒殺,全局遞增ID、IP訪問(wèn)限制等。以IP訪問(wèn)限制來(lái)說(shuō),惡意攻擊者可能發(fā)起無(wú)限次訪問(wèn),并發(fā)量比較大,分布式環(huán)境下對(duì)N的邊界檢查就不可靠,因?yàn)閺膔edis讀的N可能已經(jīng)是臟數(shù)據(jù)。傳統(tǒng)的加鎖的做法(如java的synchronized和Lock)也沒(méi)用,因?yàn)檫@是分布式環(huán)境,這個(gè)同步問(wèn)題的救火隊(duì)員也束手無(wú)策。在這危急存亡之秋,分布式鎖終于有用武之地了。
分布式鎖可以基于很多種方式實(shí)現(xiàn),比如zookeeper、redis...。不管哪種方式,他的基本原理是不變的:用一個(gè)狀態(tài)值表示鎖,對(duì)鎖的占用和釋放通過(guò)狀態(tài)值來(lái)標(biāo)識(shí)。
這里主要講如何用redis實(shí)現(xiàn)分布式鎖。
三、使用redis的setNX命令實(shí)現(xiàn)分布式鎖
1、實(shí)現(xiàn)的原理
Redis為單進(jìn)程單線程模式,采用隊(duì)列模式將并發(fā)訪問(wèn)變成串行訪問(wèn),且多客戶端對(duì)Redis的連接并不存在競(jìng)爭(zhēng)關(guān)系。redis的SETNX命令可以方便的實(shí)現(xiàn)分布式鎖。
2、基本命令解析
1)setNX(SET if Not eXists)
語(yǔ)法:
SETNX key value
將 key 的值設(shè)為 value ,當(dāng)且僅當(dāng) key 不存在。
若給定的 key 已經(jīng)存在,則 SETNX 不做任何動(dòng)作。
SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡(jiǎn)寫(xiě)
返回值:
設(shè)置成功,返回 1 。
設(shè)置失敗,返回 0 。
例子:
redis> EXISTS job # job 不存在 (integer) 0 redis> SETNX job "programmer" # job 設(shè)置成功 (integer) 1 redis> SETNX job "code-farmer" # 嘗試覆蓋 job ,失敗 (integer) 0 redis> GET job # 沒(méi)有被覆蓋 "programmer"
所以我們使用執(zhí)行下面的命令
SETNX lock.foo <current Unix time + lock timeout + 1>
如返回1,則該客戶端獲得鎖,把lock.foo的鍵值設(shè)置為時(shí)間值表示該鍵已被鎖定,該客戶端最后可以通過(guò)DEL lock.foo來(lái)釋放該鎖。
如返回0,表明該鎖已被其他客戶端取得,這時(shí)我們可以先返回或進(jìn)行重試等對(duì)方完成或等待鎖超時(shí)。
2)getSET
語(yǔ)法:
GETSET key value
將給定 key 的值設(shè)為 value ,并返回 key 的舊值(old value)。
當(dāng) key 存在但不是字符串類型時(shí),返回一個(gè)錯(cuò)誤。
返回值:
返回給定 key 的舊值。
當(dāng) key 沒(méi)有舊值時(shí),也即是, key 不存在時(shí),返回 nil 。
3)get
語(yǔ)法:
GET key
返回值:
當(dāng) key 不存在時(shí),返回 nil ,否則,返回 key 的值。
如果 key 不是字符串類型,那么返回一個(gè)錯(cuò)誤
四、解決死鎖
上面的鎖定邏輯有一個(gè)問(wèn)題:如果一個(gè)持有鎖的客戶端失敗或崩潰了不能釋放鎖,該怎么解決?
我們可以通過(guò)鎖的鍵對(duì)應(yīng)的時(shí)間戳來(lái)判斷這種情況是否發(fā)生了,如果當(dāng)前的時(shí)間已經(jīng)大于lock.foo的值,說(shuō)明該鎖已失效,可以被重新使用。
發(fā)生這種情況時(shí),可不能簡(jiǎn)單的通過(guò)DEL來(lái)刪除鎖,然后再SETNX一次(講道理,刪除鎖的操作應(yīng)該是鎖擁有這執(zhí)行的,這里只需要等它超時(shí)即可),當(dāng)多個(gè)客戶端檢測(cè)到鎖超時(shí)后都會(huì)嘗試去釋放它,這里就可能出現(xiàn)一個(gè)競(jìng)態(tài)條件,讓我們模擬一下這個(gè)場(chǎng)景:
C0操作超時(shí)了,但它還持有著鎖,C1和C2讀取lock.foo檢查時(shí)間戳,先后發(fā)現(xiàn)超時(shí)了。
C1 發(fā)送DEL lock.foo
C1 發(fā)送SETNX lock.foo 并且成功了。
C2 發(fā)送DEL lock.foo
C2 發(fā)送SETNX lock.foo 并且成功了。
這樣一來(lái),C1,C2都拿到了鎖!問(wèn)題大了!
幸好這種問(wèn)題是可以避免的,讓我們來(lái)看看C3這個(gè)客戶端是怎樣做的:
C3發(fā)送SETNX lock.foo 想要獲得鎖,由于C0還持有鎖,所以Redis返回給C3一個(gè)0
C3發(fā)送GET lock.foo 以檢查鎖是否超時(shí)了,如果沒(méi)超時(shí),則等待或重試。
反之,如果已超時(shí),C3通過(guò)下面的操作來(lái)嘗試獲得鎖:
GETSET lock.foo <current Unix time + lock timeout + 1>
通過(guò)GETSET,C3拿到的時(shí)間戳如果仍然是超時(shí)的,那就說(shuō)明,C3如愿以償拿到鎖了。
如果在C3之前,有個(gè)叫C4的客戶端比C3快一步執(zhí)行了上面的操作,那么C3拿到的時(shí)間戳是個(gè)未超時(shí)的值,這時(shí),C3沒(méi)有如期獲得鎖,需要再次等待或重試。留意一下,盡管C3沒(méi)拿到鎖,但它改寫(xiě)了C4設(shè)置的鎖的超時(shí)值,不過(guò)這一點(diǎn)非常微小的誤差帶來(lái)的影響可以忽略不計(jì)。
注意:為了讓分布式鎖的算法更穩(wěn)鍵些,持有鎖的客戶端在解鎖之前應(yīng)該再檢查一次自己的鎖是否已經(jīng)超時(shí),再去做DEL操作,因?yàn)榭赡芸蛻舳艘驗(yàn)槟硞€(gè)耗時(shí)的操作而掛起,操作完的時(shí)候鎖因?yàn)槌瑫r(shí)已經(jīng)被別人獲得,這時(shí)就不必解鎖了。
五、代碼實(shí)現(xiàn)
expireMsecs 鎖持有超時(shí),防止線程在入鎖以后,無(wú)限的執(zhí)行下去,讓鎖無(wú)法釋放
timeoutMsecs 鎖等待超時(shí),防止線程饑餓,永遠(yuǎn)沒(méi)有入鎖執(zhí)行代碼的機(jī)會(huì)
注意:項(xiàng)目里面需要先搭建好redis的相關(guān)配置
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * Redis distributed lock implementation. * * @author zhengcanrui */ public class RedisLock { private static Logger logger = LoggerFactory.getLogger(RedisLock.class); private RedisTemplate redisTemplate; private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100; /** * Lock key path. */ private String lockKey; /** * 鎖超時(shí)時(shí)間,防止線程在入鎖以后,無(wú)限的執(zhí)行等待 */ private int expireMsecs = 60 * 1000; /** * 鎖等待時(shí)間,防止線程饑餓 */ private int timeoutMsecs = 10 * 1000; private volatile boolean locked = false; /** * Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs. * * @param lockKey lock key (ex. account:1, ...) */ public RedisLock(RedisTemplate redisTemplate, String lockKey) { this.redisTemplate = redisTemplate; this.lockKey = lockKey + "_lock"; } /** * Detailed constructor with default lock expiration of 60000 msecs. * */ public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs) { this(redisTemplate, lockKey); this.timeoutMsecs = timeoutMsecs; } /** * Detailed constructor. * */ public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) { this(redisTemplate, lockKey, timeoutMsecs); this.expireMsecs = expireMsecs; } /** * @return lock key */ public String getLockKey() { return lockKey; } private String get(final String key) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); byte[] data = connection.get(serializer.serialize(key)); connection.close(); if (data == null) { return null; } return serializer.deserialize(data); } }); } catch (Exception e) { logger.error("get redis error, key : {}", key); } return obj != null ? obj.toString() : null; } private boolean setNX(final String key, final String value) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value)); connection.close(); return success; } }); } catch (Exception e) { logger.error("setNX redis error, key : {}", key); } return obj != null ? (Boolean) obj : false; } private String getSet(final String key, final String value) { Object obj = null; try { obj = redisTemplate.execute(new RedisCallback<Object>() { @Override public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisSerializer serializer = new StringRedisSerializer(); byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value)); connection.close(); return serializer.deserialize(ret); } }); } catch (Exception e) { logger.error("setNX redis error, key : {}", key); } return obj != null ? (String) obj : null; } /** * 獲得 lock. * 實(shí)現(xiàn)思路: 主要是使用了redis 的setnx命令,緩存了鎖. * reids緩存的key是鎖的key,所有的共享, value是鎖的到期時(shí)間(注意:這里把過(guò)期時(shí)間放在value了,沒(méi)有時(shí)間上設(shè)置其超時(shí)時(shí)間) * 執(zhí)行過(guò)程: * 1.通過(guò)setnx嘗試設(shè)置某個(gè)key的值,成功(當(dāng)前沒(méi)有這個(gè)鎖)則返回,成功獲得鎖 * 2.鎖已經(jīng)存在則獲取鎖的到期時(shí)間,和當(dāng)前時(shí)間比較,超時(shí)的話,則設(shè)置新的值 * * @return true if lock is acquired, false acquire timeouted * @throws InterruptedException in case of thread interruption */ public synchronized boolean lock() throws InterruptedException { int timeout = timeoutMsecs; while (timeout >= 0) { long expires = System.currentTimeMillis() + expireMsecs + 1; String expiresStr = String.valueOf(expires); //鎖到期時(shí)間 if (this.setNX(lockKey, expiresStr)) { // lock acquired locked = true; return true; } String currentValueStr = this.get(lockKey); //redis里的時(shí)間 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { //判斷是否為空,不為空的情況下,如果被其他線程設(shè)置了值,則第二個(gè)條件判斷是過(guò)不去的 // lock is expired String oldValueStr = this.getSet(lockKey, expiresStr); //獲取上一個(gè)鎖到期時(shí)間,并設(shè)置現(xiàn)在的鎖到期時(shí)間, //只有一個(gè)線程才能獲取上一個(gè)線上的設(shè)置時(shí)間,因?yàn)閖edis.getSet是同步的 if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { //防止誤刪(覆蓋,因?yàn)閗ey是相同的)了他人的鎖——這里達(dá)不到效果,這里值會(huì)被覆蓋,但是因?yàn)槭裁聪嗖盍撕苌俚臅r(shí)間,所以可以接受 //[分布式的情況下]:如過(guò)這個(gè)時(shí)候,多個(gè)線程恰好都到了這里,但是只有一個(gè)線程的設(shè)置值和當(dāng)前值相同,他才有權(quán)利獲取鎖 // lock acquired locked = true; return true; } } timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS; /* 延遲100 毫秒, 這里使用隨機(jī)時(shí)間可能會(huì)好一點(diǎn),可以防止饑餓進(jìn)程的出現(xiàn),即,當(dāng)同時(shí)到達(dá)多個(gè)進(jìn)程, 只會(huì)有一個(gè)進(jìn)程獲得鎖,其他的都用同樣的頻率進(jìn)行嘗試,后面有來(lái)了一些進(jìn)行,也以同樣的頻率申請(qǐng)鎖,這將可能導(dǎo)致前面來(lái)的鎖得不到滿足. 使用隨機(jī)的等待時(shí)間可以一定程度上保證公平性 */ Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS); } return false; } /** * Acqurired lock release. */ public synchronized void unlock() { if (locked) { redisTemplate.delete(lockKey); locked = false; } } }
調(diào)用:
RedisLock lock = new RedisLock(redisTemplate, key, 10000, 20000); try { if(lock.lock()) { //需要加鎖的代碼 } } } catch (InterruptedException e) { e.printStackTrace(); }finally { //為了讓分布式鎖的算法更穩(wěn)鍵些,持有鎖的客戶端在解鎖之前應(yīng)該再檢查一次自己的鎖是否已經(jīng)超時(shí),再去做DEL操作,因?yàn)榭赡芸蛻舳艘驗(yàn)槟硞€(gè)耗時(shí)的操作而掛起, //操作完的時(shí)候鎖因?yàn)槌瑫r(shí)已經(jīng)被別人獲得,這時(shí)就不必解鎖了。 ————這里沒(méi)有做 lock.unlock(); }
六、一些問(wèn)題
1、為什么不直接使用expire設(shè)置超時(shí)時(shí)間,而將時(shí)間的毫秒數(shù)其作為value放在redis中?
如下面的方式,把超時(shí)的交給redis處理:
lock(key, expireSec){ isSuccess = setnx key if (isSuccess) expire key expireSec }
這種方式貌似沒(méi)什么問(wèn)題,但是假如在setnx后,redis崩潰了,expire就沒(méi)有執(zhí)行,結(jié)果就是死鎖了。鎖永遠(yuǎn)不會(huì)超時(shí)。
2、為什么前面的鎖已經(jīng)超時(shí)了,還要用getSet去設(shè)置新的時(shí)間戳的時(shí)間獲取舊的值,然后和外面的判斷超時(shí)時(shí)間的時(shí)間戳比較呢?
因?yàn)槭欠植际降沫h(huán)境下,可以在前一個(gè)鎖失效的時(shí)候,有兩個(gè)進(jìn)程進(jìn)入到鎖超時(shí)的判斷。如:
C0超時(shí)了,還持有鎖,C1/C2同時(shí)請(qǐng)求進(jìn)入了方法里面
C1/C2獲取到了C0的超時(shí)時(shí)間
C1使用getSet方法
C2也執(zhí)行了getSet方法
假如我們不加 oldValueStr.equals(currentValueStr) 的判斷,將會(huì)C1/C2都將獲得鎖,加了之后,能保證C1和C2只能一個(gè)能獲得鎖,一個(gè)只能繼續(xù)等待。
注意:這里可能導(dǎo)致超時(shí)時(shí)間不是其原本的超時(shí)時(shí)間,C1的超時(shí)時(shí)間可能被C2覆蓋了,但是他們相差的毫秒及其小,這里忽略了。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。