在分布式系統(tǒng)中,保證數(shù)據(jù)一致性和并發(fā)控制是至關(guān)重要的挑戰(zhàn)之一。分布式鎖是一種常用的解決方案,而Redis作為一個(gè)快速、可靠的內(nèi)存數(shù)據(jù)庫,提供了實(shí)現(xiàn)分布式鎖的有效方法。本文將介紹Redis分布式鎖的實(shí)現(xiàn)原理和使用方法,以確保數(shù)據(jù)一致性并控制并發(fā)訪問,幫助讀者理解和應(yīng)用這一關(guān)鍵技術(shù)。
Redis分布式鎖主要依靠一個(gè) SETNX 指令實(shí)現(xiàn)的 , 這條命令的含義就是“SET if Not Exists”,即不存在的時(shí)候才會設(shè)置值。只有在key不存在的情況下,將鍵key的值設(shè)置為value。如果key已經(jīng)存在,則SETNX命令不做任何操作。這個(gè)命令的返回值如下:
- 命令在設(shè)置成功時(shí)返回1。
- 命令在設(shè)置失敗時(shí)返回0。
假設(shè)此時(shí)有線程A和線程B同時(shí)訪問臨界區(qū)代碼,假設(shè)線程A首先執(zhí)行了SETNX命令,并返回結(jié)果1,繼續(xù)向下執(zhí)行。而此時(shí)線程B再次執(zhí)行SETNX命令時(shí),返回的結(jié)果為0,則線程B不能繼續(xù)向下執(zhí)行。只有當(dāng)線程A執(zhí)行DELETE命令將設(shè)置的鎖狀態(tài)刪除時(shí),線程B才會成功執(zhí)行SETNX命令設(shè)置加鎖狀態(tài)后繼續(xù)向下執(zhí)行
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");
當(dāng)然我們在使用分布式鎖的時(shí)候也不能這么簡單, 會考慮到一些實(shí)際場景下的問題 , 例如 :
- 死鎖問題:在使用分布式鎖的時(shí)候, 如果因?yàn)橐恍┰驅(qū)е孪到y(tǒng)宕機(jī), 鎖資源沒有被釋放, 就會產(chǎn)生死鎖,解決的方案 : 上鎖的時(shí)候設(shè)置鎖的超時(shí)時(shí)間
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe", 30, TimeUnit.SECONDS);
- 鎖超時(shí)問題:如果業(yè)務(wù)執(zhí)行需要的時(shí)間, 超過的鎖的超時(shí)時(shí)間 , 這個(gè)時(shí)候業(yè)務(wù)還沒有執(zhí)行完成, 鎖就已經(jīng)自動(dòng)被刪除了 ,其他請求就能獲取鎖, 操作這個(gè)資源 , 這個(gè)時(shí)候就會出現(xiàn)并發(fā)問題 , 解決的方案 :
1.引入Redis的watch dog機(jī)制, 自動(dòng)為鎖續(xù)期
2.開啟子線程 , 每隔20S運(yùn)行一次, 重新設(shè)置鎖的超時(shí)時(shí)間
- 歸一問題:如果一個(gè)線程獲取了分布式鎖, 但是這個(gè)線程業(yè)務(wù)沒有執(zhí)行完成之前 , 鎖被其他的線程刪掉了, 又會出現(xiàn)線程并發(fā)問題 , 這個(gè)時(shí)候就需要考慮歸一化問題,就是一個(gè)線程執(zhí)行了加鎖操作后,后續(xù)必須由這個(gè)線程執(zhí)行解鎖操作,加鎖和解鎖操作由同一個(gè)線程來完成。為了解決只有加鎖的線程才能進(jìn)行相應(yīng)的解鎖操作的問題,那么,我們就需要將加鎖和解鎖操作綁定到同一個(gè)線程中,可以使用ThreadLocal來解決這個(gè)問題 , 加鎖的時(shí)候生成唯一標(biāo)識保存到ThreadLocal , 并且設(shè)置到鎖的值中 , 釋放鎖的時(shí)候, 判斷線程中的唯一標(biāo)識和鎖的唯一標(biāo)識是否相同, 只有相同才會釋放。
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid,timeout, unit);
}
@Override
public void releaseLock(String key){
//當(dāng)前線程中綁定的uuid與Redis中的uuid相同時(shí),再執(zhí)行刪除鎖的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
stringRedisTemplate.delete(key);
}
}
}
- 可重入問題:當(dāng)一個(gè)線程成功設(shè)置了鎖標(biāo)志位后,其他的線程再設(shè)置鎖標(biāo)志位時(shí),就會返回失敗。還有一種場景就是在一個(gè)業(yè)務(wù)中, 有個(gè)操作都需要獲取到鎖, 這個(gè)時(shí)候第二個(gè)操作就無法獲取鎖了 , 操作會失敗
例如 : 下單業(yè)務(wù)中, 扣減商品庫存會給商品加鎖, 增加商品銷量也需要給商品加鎖 , 這個(gè)時(shí)候需要獲取二次鎖。第二次獲取商品鎖就會失敗 , 這就需要我們的分布式鎖能夠?qū)崿F(xiàn)可重入。實(shí)現(xiàn)可重入鎖最簡單的方式就是使用計(jì)數(shù)器 , 加鎖成功之后計(jì)數(shù)器+ 1 , 取消鎖之后計(jì)數(shù)器
-1 , 計(jì)數(shù)器減為0 , 真正從Redis刪除鎖。
public class RedisLockImpl implements RedisLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();
@Override
public boolean tryLock(String key, long timeout, TimeUnit unit){
Boolean isLocked = false;
if(threadLocal.get() == null){
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key,uuid, timeout, unit);
}else{
isLocked = true;
}
//加鎖成功后將計(jì)數(shù)器加1
if(isLocked){
Integer count = threadLocalInteger.get() == null ? 0 :threadLocalInteger.get();
threadLocalInteger.set(count++);
}
return isLocked;
}
@Override
public void releaseLock(String key){
//當(dāng)前線程中綁定的uuid與Redis中的uuid相同時(shí),再執(zhí)行刪除鎖的操作
if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
Integer count = threadLocalInteger.get();
//計(jì)數(shù)器減為0時(shí)釋放鎖
if(count == null || --count <= 0){
stringRedisTemplate.delete(key);
}
}
}
}
- 阻塞與非阻塞問題:在使用分布式鎖的時(shí)候 , 如果當(dāng)前需要操作的資源已經(jīng)加了鎖, 這個(gè)時(shí)候會獲取鎖失敗, 直接向用戶返回失敗信息 , 用戶的體驗(yàn)非常不好 , 所以我們在實(shí)現(xiàn)分布式鎖的時(shí)候, 我們可以將后續(xù)的請求進(jìn)行阻塞,直到當(dāng)前請求釋放鎖后,再喚醒阻塞的請求獲得分布式鎖來執(zhí)行方法。具體的實(shí)現(xiàn)就是參考自旋鎖的思想, 獲取鎖失敗自選獲取鎖, 直到成功為止 , 當(dāng)然為了防止多條線程自旋帶來的系統(tǒng)資料消耗, 可以設(shè)置一個(gè)自旋的超時(shí)時(shí)間 , 超過時(shí)間之后, 自動(dòng)終止線程 , 返回失敗信息。
總結(jié)
Redis分布式鎖是一種強(qiáng)大的工具,用于確保在分布式系統(tǒng)中數(shù)據(jù)的一致性和并發(fā)控制。通過Redis的SETNX命令和過期時(shí)間的設(shè)置,我們可以實(shí)現(xiàn)簡單而有效的分布式鎖機(jī)制。然而,在使用Redis分布式鎖時(shí),我們需要注意原子性、異常處理和適當(dāng)?shù)呐渲玫确矫?,以確保鎖的可靠性和系統(tǒng)的穩(wěn)定性。在實(shí)際應(yīng)用中,根據(jù)業(yè)務(wù)需求選擇合適的過期時(shí)間和鎖的管理策略,并考慮使用更復(fù)雜的算法和工具來增強(qiáng)分布式鎖的功能。通過合理的設(shè)計(jì)和使用,Redis分布式鎖將成為分布式系統(tǒng)中實(shí)現(xiàn)數(shù)據(jù)一致性和并發(fā)控制的重要利器。
如果你對編程知識和相關(guān)職業(yè)感興趣,歡迎訪問編程獅官網(wǎng)(http://o2fo.com/)。在編程獅,我們提供廣泛的技術(shù)教程、文章和資源,幫助你在技術(shù)領(lǐng)域不斷成長。無論你是剛剛起步還是已經(jīng)擁有多年經(jīng)驗(yàn),我們都有適合你的內(nèi)容,助你取得成功。