亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Redis分布式鎖及安全問題解決

 更新時間:2024年03月25日 15:47:16   作者:林+夕=夢  
在分布式環(huán)境中,遇到搶購等訪問共享資源的場景時,需要我們有一種鎖機(jī)制去解決并發(fā)問題,本文主要介紹了Redis分布式鎖及安全問題解決,具有一定的參考價值,感興趣的可以了解一下

一、為什么需要分布式鎖

單機(jī)鎖: 多個線程同時改變一個變量時,需要對變量或者代碼塊做同步從而保證串行修改變量.

多機(jī)系統(tǒng): 存在多機(jī)器多請求同時對同一個共享資源進(jìn)行修改,如果不加以限制,將導(dǎo)致數(shù)據(jù)錯亂和數(shù)據(jù)不一致性. 比如: 庫存超賣、抽獎多發(fā)、券多發(fā)放、訂單重復(fù)提交...

二、常見的分布式鎖

實現(xiàn)方式

優(yōu)點

缺點

應(yīng)用場景

MySQL數(shù)據(jù)庫表

易于理解/易于實現(xiàn)

容易出現(xiàn)單點故障、死鎖性能低/可靠性低

適用于并發(fā)量低、

性能要求低的場景

Redis分布式鎖

性能高/易于實現(xiàn)可跨集群部署,無單點故障

鎖失效時間的控制不穩(wěn)定穩(wěn)定性低于

ZooKeeper

適用于高并發(fā)、高性能場景

ZooKeeper

分布式鎖

無單點故障/可靠性高不可重入/無死鎖問題

實現(xiàn)復(fù)雜性能低于緩存分布式鎖

適用于大部分分布式場景,

除對性能要求極高的場景

三、 用Redis實現(xiàn)一個分布式鎖

3.1 SETNX

SET lock 1 NX
    String buyTicket() {
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
        if (lock) {
            try {
                int stockNum = byTicketMapper.selectStockNum();
                if (stockNum > 0) {
                    //TODO  by ticket process....
                    byTicketMapper.reduceStock();
                    return "SUCCESS";
                }
                return "FAILED";
            }finally {
                redisTemplate.delete("lock");
            }
        }
        return "OOPS...PLEASE TRY LATTER";
    }

Java代碼很容易看出, 假如執(zhí)行了加鎖后程序出現(xiàn)宕機(jī), 執(zhí)行不到finally語句塊里的解鎖, 就出會有死鎖問題. 為了解決死鎖, 很容易就想到給鎖設(shè)置一個過期時間. 

3.2 設(shè)置鎖過期時間和唯一ID

設(shè)置key時同時設(shè)置過期時間:  

SET lock 1 NX EX 30

Java代碼: 

Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1", Duration.ofSeconds(10L));

但這會導(dǎo)致更嚴(yán)重的錯刪鎖問題, 比如某個線程1加鎖后, 執(zhí)行業(yè)務(wù)邏輯比較慢, 鎖過期自動釋放了, 此時線程2競爭加鎖成功, 而線程1執(zhí)行了刪除鎖, 以此類推, 相當(dāng)于鎖失效

改進(jìn): 設(shè)置線程UUID, 并且用lua腳本保證GET和DEL原子性操作, 防止刪錯key

String buyTicket() {
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, Duration.ofSeconds(10L));
        if (lock) {
            try {
                int stockNum = byTicketMapper.selectStockNum();
                if (stockNum > 0) {
                    //TODO by ticket process....
                    byTicketMapper.reduceStock();
                    return "SUCCESS";
                }
                return "FAILED";
            } finally {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                this.redisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList("lock"), uuid);
            }
        }
        return "OOPS...PLEASE TRY LATTER";
    }

看起來好像還不錯, 但是依然有過期時間無法完全匹配實際需求的問題:

太短 -> 鎖失效無法保證程序正確處理業(yè)務(wù)

太長 -> 異常流程過度占有鎖導(dǎo)致資源浪費

有更好的解決方案嗎? 比如開啟一個后臺線程, 定時檢查主線程是否持有鎖(即未完成操作資源), 給它自動延長鎖過期時間.  幸運的javaer 已經(jīng)Redisson庫封裝好了這些操作.

3.3  Redisson

看門狗機(jī)制: 加一個后臺線程定時檢查鎖,自動續(xù)過期時間

Java代碼

String buyTicket() {
        RLock lock = redissonClient.getLock("lock");
        try {
            if (lock.tryLock(30,TimeUnit.SECONDS)) {
                int stockNum = byTicketMapper.selectStockNum();
                if (stockNum > 0) {
                    //TODO  by ticket process....
                    byTicketMapper.reduceStock();
                    return "SUCCESS";
                }
                return "FAILED";
            }
        }catch (InterruptedException e){
            log.error("Try Lock Error:{}",e.getMessage());
        }finally {
            lock.unlock();
        }
        return "OOPS...PLEASE TRY LATTER";
    }

對于單機(jī)版的redis至此已經(jīng)是很好的方案了, 然而現(xiàn)實中大多數(shù)使用的是集群redis... 

四、 主從同步對分布式鎖的影響

高并發(fā)場景主從切換鎖失效: 試想一下這樣的場景, 主節(jié)點加鎖成功, 沒有同步到從節(jié)點時主節(jié)點宕機(jī), 此時從節(jié)點選舉出新的主節(jié)點, 它就丟失了還沒同步的鎖, 此時其他客戶端向新的主節(jié)點請求加鎖會成功, 導(dǎo)致沖突.

4.1 Redlock

Redlock 的方案官網(wǎng)解釋: 既然主從架構(gòu)有問題, 那就部署多個主庫實例. 

Redlock整體流程:

  • 客戶端在多個 Redis 實例上申請加鎖
  • 必須保證大多數(shù)節(jié)點(超過半數(shù))加鎖成功
  • 大多數(shù)節(jié)點加鎖的總耗時,要小于鎖設(shè)置的過期時間
  • 釋放鎖,要向全部節(jié)點發(fā)起釋放鎖請求

疑問: 

1 ) 假如有3個客戶端競爭同一資源, 向5個Redis請求獲取鎖, 容易出現(xiàn)沒有獲勝者的情況.

-> redis官方:  多路復(fù)用 以及 沒有獲得過半數(shù)鎖的客戶端盡快釋放鎖

2) 某個主節(jié)點宕機(jī)時可能出現(xiàn)鎖安全性問題. 比如: 當(dāng)Redis持久化策略為AOF使用appendfsync=everysec即每秒fsync一次, 故障時會丟失1秒的數(shù)據(jù), 也就是丟鎖. 當(dāng)該節(jié)點恢復(fù)時, 其他客戶端來獲取鎖成功

-> redis官方:  在崩潰后使實例不可用, 至少比最大 TTL多一點, 保證崩潰時的鎖在所有節(jié)點都自動失效. [損失了可用性]

RedLock的爭論: 

針對RedLock的方案, 業(yè)界大佬Martin Kleppmann專門寫過一篇文章分析它的效率, 正確性和NPC問題 , redis的作者也一一反駁, 有興趣可以看文章末尾參考資料.  

NPC問題: 

Clock Drift時鐘漂移

-> redis作者: 與鎖的自動釋放時間相比,誤差幅度很小

Network Delay網(wǎng)絡(luò)延遲

Process Pause進(jìn)程暫停(GC)

-> redis作者: 第3步已經(jīng)考慮了以上問題, 當(dāng)出現(xiàn) 加鎖總耗時 > 鎖過期時間 就會認(rèn)為加鎖失敗, 而在步驟3之后出現(xiàn)GC或ND問題, 其他鎖服務(wù)比如zookeeper也這樣.

 通過以上爭論, 我們看到redlock確實存在一些缺點: 

1) 性能折損, 且無法做到100%安全的分布式鎖

2) 不能橫向擴(kuò)容: 如果要提升高可用, 只能增加更多單節(jié)點,  每個單節(jié)點不能再加從節(jié)點

4.2 Fencing Token

針對主從架構(gòu)下的分布式鎖, 前面提到的Martin Kleppmann, 在它的文章里提出了"fencing token"的解決方案: 

  • 客戶端在獲取鎖時,鎖服務(wù)可以提供一個「遞增」的 token

  • 客戶端拿著這個 token 去操作共享資源

  • 共享資源可以根據(jù) token 拒絕「后來者」的請求

這個方案要求共享資源具備"互斥"能力, 而且在分布式環(huán)境下做嚴(yán)格自增的token無疑也是個難題.

有沒有其他方案呢, 在找資料的過程中, 我發(fā)現(xiàn)Redisson較新的版本(我用的是3.25.0)提供了FencedLock.

4.3 FencedLock

它的底層獲取鎖的同時, 使用 incr 命令從redis獲取自增的token: 

但是在redis集群環(huán)境下, 這樣使用incr會有可靠性問題. 當(dāng)多個客戶端同時調(diào)用incr命令時,可能會出現(xiàn)并發(fā)沖突,導(dǎo)致數(shù)據(jù)不一致.

雖然redisson的官方文檔說RedLock已棄用,推薦使用Lock or FencedLock, 但如前述我覺得上述FencedLock會有可靠性問題. (如果大佬們有其他見解, 請賜教, 感激~)

4.4 兜底鎖

對安全性要求比較高的場景, 也許我們可以參考fencing token的思路在資源層再做一個兜底鎖, 比如MySQL:

在操作資源前先標(biāo)記token, 再(檢查+修改)共享資源

UPDATE
    table T
SET
    val = $new_val
WHERE
    id = $id AND current_token = $token_value;

兩種思路結(jié)合我們就擁有了一個更安全可靠的分布式鎖體系: 

  • redis分布式鎖: 作用于上層, 完成了大多數(shù)"互斥", 把大部分請求擋在上層, 減輕了操作資源層的壓力.
  • MySQL兜底鎖: 通過版本號或者插入鎖的方式實現(xiàn)"互斥", 避免極端情況下的并發(fā)沖突, 由于上層已經(jīng)擋住了大部分請求, MySQL鎖也能很好的避開它本身的缺點.

五、總結(jié)

1) 沒有一把完美的分布式鎖, 在設(shè)計分布式鎖的時候, 需要多角度考慮它是否滿足了以下特性:

  • 獨占排他互斥
  • 防死鎖
  • 保證原子性
  • 正確性
  • 可重入
  • 容錯分布式  

2) 如果是要求數(shù)據(jù)絕對正確的業(yè)務(wù), 資源層要做好兜底。 

到此這篇關(guān)于Redis分布式鎖及安全問題解決的文章就介紹到這了,更多相關(guān)Redis分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • redis保存AtomicInteger對象踩坑及解決

    redis保存AtomicInteger對象踩坑及解決

    這篇文章主要介紹了redis保存AtomicInteger對象踩坑及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-11-11
  • 利用redisson快速實現(xiàn)自定義限流注解(接口防刷)

    利用redisson快速實現(xiàn)自定義限流注解(接口防刷)

    利用redis的有序集合即Sorted?Set數(shù)據(jù)結(jié)構(gòu),構(gòu)造一個令牌桶來實施限流,而redisson已經(jīng)幫我們封裝成了RRateLimiter,通過redisson,即可快速實現(xiàn)我們的目標(biāo),這篇文章主要介紹了利用redisson快速實現(xiàn)自定義限流注解,需要的朋友可以參考下
    2024-07-07
  • Redis 命令的詳解及簡單實例

    Redis 命令的詳解及簡單實例

    這篇文章主要介紹了Redis 命令的詳解及簡單實例的相關(guān)資料,這里提供基礎(chǔ)語法及使用實例,需要的朋友可以參考下
    2017-08-08
  • redis如何查看鎖是否存在

    redis如何查看鎖是否存在

    文章介紹了兩種方法來檢查Redis鎖的狀態(tài):使用GET命令查看鎖的值和使用EXISTS命令檢查鎖的存在性,這兩種方法都是通過連接到Redis服務(wù)器并執(zhí)行相應(yīng)的命令來實現(xiàn)的,GET命令用于獲取指定鍵的值,而EXISTS命令用于檢查指定鍵是否存在
    2025-01-01
  • Redis消息隊列的三種實現(xiàn)方式

    Redis消息隊列的三種實現(xiàn)方式

    本文主要介紹了Redis消息隊列的三種實現(xiàn)方式,主要包括List實現(xiàn)消息隊列,PubSub消息隊列,Stream消息隊列,具有一定的參考價值,感興趣的可以了解一下
    2023-12-12
  • Redis 大key的幾種刪除方式

    Redis 大key的幾種刪除方式

    大key刪除直接調(diào)用 del 命令刪除key,容易造成請求被阻塞,本文主要介紹了Redis 大key的幾種刪除方式,具有一定的參考價值,感興趣的可以了解一下
    2025-03-03
  • Redis不是一直號稱單線程效率也很高嗎,為什么又采用多線程了?

    Redis不是一直號稱單線程效率也很高嗎,為什么又采用多線程了?

    這篇文章主要介紹了Redis不是一直號稱單線程效率也很高嗎,為什么又采用多線程了的相關(guān)資料,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-03-03
  • redis由于目標(biāo)計算機(jī)積極拒絕,無法連接的解決

    redis由于目標(biāo)計算機(jī)積極拒絕,無法連接的解決

    這篇文章主要介紹了redis由于目標(biāo)計算機(jī)積極拒絕,無法連接的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • 基于Redis有序集合實現(xiàn)滑動窗口限流的步驟

    基于Redis有序集合實現(xiàn)滑動窗口限流的步驟

    滑動窗口算法是一種基于時間窗口的限流算法,通過動態(tài)地滑動窗口,可以動態(tài)調(diào)整限流的速率,Redis有序集合可以用來實現(xiàn)滑動窗口限流,本文介紹基于Redis有序集合實現(xiàn)滑動窗口限流,感興趣的朋友一起看看吧
    2024-12-12
  • 基于Redis實現(xiàn)抽獎功能及問題小結(jié)

    基于Redis實現(xiàn)抽獎功能及問題小結(jié)

    這篇文章主要介紹了基于Redis實現(xiàn)抽獎功能,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-08-08

最新評論