App下載

Redis分布式鎖如何實現(xiàn) ?

唐僧洗頭愛飄柔 2023-12-02 15:35:37 瀏覽數(shù) (815)
反饋

在分布式系統(tǒng)中,保證數(shù)據(jù)一致性和并發(fā)控制是至關(guān)重要的挑戰(zhàn)之一。分布式鎖是一種常用的解決方案,而Redis作為一個快速、可靠的內(nèi)存數(shù)據(jù)庫,提供了實現(xiàn)分布式鎖的有效方法。本文將介紹Redis分布式鎖的實現(xiàn)原理和使用方法,以確保數(shù)據(jù)一致性并控制并發(fā)訪問,幫助讀者理解和應(yīng)用這一關(guān)鍵技術(shù)。

Redis分布式鎖主要依靠一個 SETNX 指令實現(xiàn)的 , 這條命令的含義就是“SET if Not Exists”,即不存在的時候才會設(shè)置值。只有在key不存在的情況下,將鍵key的值設(shè)置為value。如果key已經(jīng)存在,則SETNX命令不做任何操作。這個命令的返回值如下:

  • 命令在設(shè)置成功時返回1。
  • 命令在設(shè)置失敗時返回0。

假設(shè)此時有線程A和線程B同時訪問臨界區(qū)代碼,假設(shè)線程A首先執(zhí)行了SETNX命令,并返回結(jié)果1,繼續(xù)向下執(zhí)行。而此時線程B再次執(zhí)行SETNX命令時,返回的結(jié)果為0,則線程B不能繼續(xù)向下執(zhí)行。只有當(dāng)線程A執(zhí)行DELETE命令將設(shè)置的鎖狀態(tài)刪除時,線程B才會成功執(zhí)行SETNX命令設(shè)置加鎖狀態(tài)后繼續(xù)向下執(zhí)行 

Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe");

當(dāng)然我們在使用分布式鎖的時候也不能這么簡單, 會考慮到一些實際場景下的問題 , 例如 :

  • 死鎖問題:在使用分布式鎖的時候, 如果因為一些原因?qū)е孪到y(tǒng)宕機, 鎖資源沒有被釋放, 就會產(chǎn)生死鎖,解決的方案 : 上鎖的時候設(shè)置鎖的超時時間

Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe", 30, TimeUnit.SECONDS);

  • 鎖超時問題:如果業(yè)務(wù)執(zhí)行需要的時間, 超過的鎖的超時時間 , 這個時候業(yè)務(wù)還沒有執(zhí)行完成, 鎖就已經(jīng)自動被刪除了 ,其他請求就能獲取鎖, 操作這個資源 , 這個時候就會出現(xiàn)并發(fā)問題 , 解決的方案 :

            1.引入Redis的watch dog機制, 自動為鎖續(xù)期 
            2.開啟子線程 , 每隔20S運行一次, 重新設(shè)置鎖的超時時間

  • 歸一問題:如果一個線程獲取了分布式鎖, 但是這個線程業(yè)務(wù)沒有執(zhí)行完成之前 , 鎖被其他的線程刪掉了, 又會出現(xiàn)線程并發(fā)問題 , 這個時候就需要考慮歸一化問題,就是一個線程執(zhí)行了加鎖操作后,后續(xù)必須由這個線程執(zhí)行解鎖操作,加鎖和解鎖操作由同一個線程來完成。為了解決只有加鎖的線程才能進(jìn)行相應(yīng)的解鎖操作的問題,那么,我們就需要將加鎖和解鎖操作綁定到同一個線程中,可以使用ThreadLocal來解決這個問題 , 加鎖的時候生成唯一標(biāo)識保存到ThreadLocal , 并且設(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相同時,再執(zhí)行刪除鎖的操作
        if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
            stringRedisTemplate.delete(key);
        }
    }
}

  • 可重入問題:當(dāng)一個線程成功設(shè)置了鎖標(biāo)志位后,其他的線程再設(shè)置鎖標(biāo)志位時,就會返回失敗。還有一種場景就是在一個業(yè)務(wù)中, 有個操作都需要獲取到鎖, 這個時候第二個操作就無法獲取鎖了 , 操作會失敗

例如 : 下單業(yè)務(wù)中, 扣減商品庫存會給商品加鎖, 增加商品銷量也需要給商品加鎖 , 這個時候需要獲取二次鎖。第二次獲取商品鎖就會失敗 , 這就需要我們的分布式鎖能夠?qū)崿F(xiàn)可重入。實現(xiàn)可重入鎖最簡單的方式就是使用計數(shù)器 , 加鎖成功之后計數(shù)器+ 1 , 取消鎖之后計數(shù)器 -1 , 計數(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;
        }
        //加鎖成功后將計數(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相同時,再執(zhí)行刪除鎖的操作
        if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
            Integer count = threadLocalInteger.get();
            //計數(shù)器減為0時釋放鎖
            if(count == null || --count <= 0){
                stringRedisTemplate.delete(key);
            }
        }
    }
}

  • 阻塞與非阻塞問題:在使用分布式鎖的時候 , 如果當(dāng)前需要操作的資源已經(jīng)加了鎖, 這個時候會獲取鎖失敗, 直接向用戶返回失敗信息 , 用戶的體驗非常不好 , 所以我們在實現(xiàn)分布式鎖的時候, 我們可以將后續(xù)的請求進(jìn)行阻塞,直到當(dāng)前請求釋放鎖后,再喚醒阻塞的請求獲得分布式鎖來執(zhí)行方法。具體的實現(xiàn)就是參考自旋鎖的思想, 獲取鎖失敗自選獲取鎖, 直到成功為止 , 當(dāng)然為了防止多條線程自旋帶來的系統(tǒng)資料消耗, 可以設(shè)置一個自旋的超時時間 , 超過時間之后, 自動終止線程 , 返回失敗信息。

Snipaste_2023-11-30_16-47-08

總結(jié)

Redis分布式鎖是一種強大的工具,用于確保在分布式系統(tǒng)中數(shù)據(jù)的一致性和并發(fā)控制。通過Redis的SETNX命令和過期時間的設(shè)置,我們可以實現(xiàn)簡單而有效的分布式鎖機制。然而,在使用Redis分布式鎖時,我們需要注意原子性、異常處理和適當(dāng)?shù)呐渲玫确矫妫源_保鎖的可靠性和系統(tǒng)的穩(wěn)定性。在實際應(yīng)用中,根據(jù)業(yè)務(wù)需求選擇合適的過期時間和鎖的管理策略,并考慮使用更復(fù)雜的算法和工具來增強分布式鎖的功能。通過合理的設(shè)計和使用,Redis分布式鎖將成為分布式系統(tǒng)中實現(xiàn)數(shù)據(jù)一致性和并發(fā)控制的重要利器。

1698630578111788

如果你對編程知識和相關(guān)職業(yè)感興趣,歡迎訪問編程獅官網(wǎng)(http://www.o2fo.com/)。在編程獅,我們提供廣泛的技術(shù)教程、文章和資源,幫助你在技術(shù)領(lǐng)域不斷成長。無論你是剛剛起步還是已經(jīng)擁有多年經(jīng)驗,我們都有適合你的內(nèi)容,助你取得成功。


0 人點贊