1. 面試題:Redis如何實(shí)現(xiàn)分布式鎖!

        共 5493字,需瀏覽 11分鐘

         ·

        2021-05-19 09:03

        前言

        之前的個(gè)人網(wǎng)站已經(jīng)下線(xiàn)了,https://upheart.cn/,維護(hù)太花時(shí)間了,之后會(huì)把網(wǎng)站內(nèi)容全部搬到公眾號(hào)中,內(nèi)容也會(huì)比網(wǎng)站內(nèi)容更豐富,更全面,帶大家真正的吊打面試官!

        之后分享的面試系列文章,主要是對(duì)之前面經(jīng)的答案總結(jié):社招一年半面經(jīng)分享(含阿里美團(tuán)頭條京東滴滴)

        文章內(nèi)容會(huì)盡量少?gòu)U話(huà),多干貨!

        開(kāi)始吧!

        為什么需要分布式鎖

        為什么需要分布式鎖

        使用分布式鎖的目的,無(wú)外乎就是保證同一時(shí)間只有一個(gè)客戶(hù)端可以對(duì)共享資源進(jìn)行操作

        我們?cè)诜植际綉?yīng)用進(jìn)行邏輯處理時(shí)經(jīng)常會(huì)遇到并發(fā)問(wèn)題。

        比如一個(gè)操作要修改用戶(hù)的狀態(tài),修改狀態(tài)需要先讀出用戶(hù)的狀態(tài),在內(nèi)存里進(jìn)行修改,改完了再存回去。如果這樣的操作同時(shí)進(jìn)行了,就會(huì)出現(xiàn)并發(fā)問(wèn)題,因?yàn)樽x取和保存狀態(tài)這兩個(gè)操作不是原子的。

        這個(gè)時(shí)候就要使用到分布式鎖來(lái)限制程序的并發(fā)執(zhí)行。redis作為一個(gè)緩存中間件系統(tǒng),就能提供這種分布式鎖機(jī)制,

        其本質(zhì)就是在redis里面占一個(gè)坑,當(dāng)別的進(jìn)程也要來(lái)占坑時(shí),發(fā)現(xiàn)已經(jīng)被占領(lǐng)了,就只要等待稍后再?lài)L試

        一般來(lái)說(shuō),生產(chǎn)環(huán)境可用的分布式鎖需要滿(mǎn)足以下幾點(diǎn):

        • 互斥性,互斥是鎖的基本特征,同一時(shí)刻只能有一個(gè)線(xiàn)程持有鎖,執(zhí)行臨界操作;
        • 超時(shí)釋放,超時(shí)釋放是鎖的另一個(gè)必備特性,可以對(duì)比 MySQL InnoDB 引擎中的 innodb_lock_wait_timeout配置,通過(guò)超時(shí)釋放,防止不必要的線(xiàn)程等待和資源浪費(fèi);
        • 可重入性,在分布式環(huán)境下,同一個(gè)節(jié)點(diǎn)上的同一個(gè)線(xiàn)程如果獲取了鎖之后,再次請(qǐng)求還是可以成功;

        實(shí)現(xiàn)方式

        使用SETNX實(shí)現(xiàn)

        SETNX的使用方式為:SETNX key value,只在鍵key不存在的情況下,將鍵key的值設(shè)置為value,若鍵key存在,則SETNX不做任何動(dòng)作。

        boolean result = jedis.setnx("lock-key",true)== 1L;
        if  (result) {
            try {
                // do something
            } finally {
                jedis.del("lock-key");
            }
         }

        這種方案有一個(gè)致命問(wèn)題,就是某個(gè)線(xiàn)程在獲取鎖之后由于某些異常因素(比如宕機(jī))而不能正常的執(zhí)行解鎖操作,那么這個(gè)鎖就永遠(yuǎn)釋放不掉了。

        為此,我們可以為這個(gè)鎖加上一個(gè)超時(shí)時(shí)間

        執(zhí)行 SET key value EX seconds 的效果等同于執(zhí)行 SETEX key seconds value

        執(zhí)行 SET key value PX milliseconds 的效果等同于執(zhí)行 PSETEX key milliseconds value

        String result = jedis.set("lock-key",true5);
        if ("OK".equals(result)) {
            try {
                // do something
            } finally {
                jedis.del("lock-key");
            }
        }

        方案看上去很完美,但實(shí)際上還是會(huì)有問(wèn)題

        試想一下,某線(xiàn)程A獲取了鎖并且設(shè)置了過(guò)期時(shí)間為10s,然后在執(zhí)行業(yè)務(wù)邏輯的時(shí)候耗費(fèi)了15s,此時(shí)線(xiàn)程A獲取的鎖早已被Redis的過(guò)期機(jī)制自動(dòng)釋放了

        在線(xiàn)程A獲取鎖并經(jīng)過(guò)10s之后,改鎖可能已經(jīng)被其它線(xiàn)程獲取到了。當(dāng)線(xiàn)程A執(zhí)行完業(yè)務(wù)邏輯準(zhǔn)備解鎖(DEL key)的時(shí)候,有可能刪除掉的是其它線(xiàn)程已經(jīng)獲取到的鎖。

        所以最好的方式是在解鎖時(shí)判斷鎖是否是自己的,我們可以在設(shè)置key的時(shí)候?qū)alue設(shè)置為一個(gè)唯一值uniqueValue(可以是隨機(jī)值、UUID、或者機(jī)器號(hào)+線(xiàn)程號(hào)的組合、簽名等)。

        當(dāng)解鎖時(shí),也就是刪除key的時(shí)候先判斷一下key對(duì)應(yīng)的value是否等于先前設(shè)置的值,如果相等才能刪除key

        String velue= String.valueOf(System.currentTimeMillis())
        String result = jedis.set("lock-key",velue, 5);
        if ("OK".equals(result)) {
            try {
                // do something
            } finally {
               //非原子操作
               if(jedis.get("lock-key")==value){
                  jedis.del("lock-key");
                }    
            }
        }

        這里我們一眼就可以看出問(wèn)題來(lái):GETDEL是兩個(gè)分開(kāi)的操作,在GET執(zhí)行之后且在DEL執(zhí)行之前的間隙是可能會(huì)發(fā)生異常的。

        如果我們只要保證解鎖的代碼是原子性的就能解決問(wèn)題了

        這里我們引入了一種新的方式,就是Lua腳本,示例如下:

        if redis.call("get",KEYS[1]) == ARGV[1] then
            return redis.call("del",KEYS[1])
        else
            return 0
        end

        其中ARGV[1]表示設(shè)置key時(shí)指定的唯一值。

        由于Lua腳本的原子性,在Redis執(zhí)行該腳本的過(guò)程中,其他客戶(hù)端的命令都需要等待該Lua腳本執(zhí)行完才能執(zhí)行。

        確保過(guò)期時(shí)間大于業(yè)務(wù)執(zhí)行時(shí)間

        為了防止多個(gè)線(xiàn)程同時(shí)執(zhí)行業(yè)務(wù)代碼,需要確保過(guò)期時(shí)間大于業(yè)務(wù)執(zhí)行時(shí)間

        增加一個(gè)boolean類(lèi)型的屬性isOpenExpirationRenewal,用來(lái)標(biāo)識(shí)是否開(kāi)啟定時(shí)刷新過(guò)期時(shí)間

        在增加一個(gè)scheduleExpirationRenewal方法用于開(kāi)啟刷新過(guò)期時(shí)間的線(xiàn)程

        加鎖代碼在獲取鎖成功后將isOpenExpirationRenewal置為true,并且調(diào)用scheduleExpirationRenewal方法,開(kāi)啟刷新過(guò)期時(shí)間的線(xiàn)程

        解鎖代碼增加一行代碼,將isOpenExpirationRenewal屬性置為false,停止刷新過(guò)期時(shí)間的線(xiàn)程輪詢(xún)

        Redisson實(shí)現(xiàn)

        獲取鎖成功就會(huì)開(kāi)啟一個(gè)定時(shí)任務(wù),定時(shí)任務(wù)會(huì)定期檢查去續(xù)期

        該定時(shí)調(diào)度每次調(diào)用的時(shí)間差是internalLockLeaseTime / 3,也就10秒

        默認(rèn)情況下,加鎖的時(shí)間是30秒.如果加鎖的業(yè)務(wù)沒(méi)有執(zhí)行完,那么到 30-10 = 20秒的時(shí)候,就會(huì)進(jìn)行一次續(xù)期,把鎖重置成30秒

        RedLock

        在集群中,主節(jié)點(diǎn)掛掉時(shí),從節(jié)點(diǎn)會(huì)取而代之,客戶(hù)端上卻并沒(méi)有明顯感知。原先第一個(gè)客戶(hù)端在主節(jié)點(diǎn)中申請(qǐng)成功了一把鎖,但是這把鎖還沒(méi)有來(lái)得及同步到從節(jié)點(diǎn),主節(jié)點(diǎn)突然掛掉了。然后從節(jié)點(diǎn)變成了主節(jié)點(diǎn),這個(gè)新的節(jié)點(diǎn)內(nèi)部沒(méi)有這個(gè)鎖,所以當(dāng)另一個(gè)客戶(hù)端過(guò)來(lái)請(qǐng)求加鎖時(shí),立即就批準(zhǔn)了。這樣就會(huì)導(dǎo)致系統(tǒng)中同樣一把鎖被兩個(gè)客戶(hù)端同時(shí)持有,不安全性由此產(chǎn)生

        Redlock算法就是為了解決這個(gè)問(wèn)題

        使用 Redlock,需要提供多個(gè) Redis 實(shí)例,這些實(shí)例之前相互獨(dú)立沒(méi)有主從關(guān)系。同很多分布式算法一樣,redlock 也使用大多數(shù)機(jī)制

        加鎖時(shí),它會(huì)向過(guò)半節(jié)點(diǎn)發(fā)送 set指令,只要過(guò)半節(jié)點(diǎn) set 成功,那就認(rèn)為加鎖成功。釋放鎖時(shí),需要向所有節(jié)點(diǎn)發(fā)送 del 指令。不過(guò) Redlock 算法還需要考慮出錯(cuò)重試、時(shí)鐘漂移等很多細(xì)節(jié)問(wèn)題,同時(shí)因?yàn)?Redlock 需要向多個(gè)節(jié)點(diǎn)進(jìn)行讀寫(xiě),意味著相比單實(shí)例 Redis 性能會(huì)下降一些

        Redlock 算法是在單 Redis 節(jié)點(diǎn)基礎(chǔ)上引入的高可用模式,Redlock 基于 N 個(gè)完全獨(dú)立的 Redis 節(jié)點(diǎn),一般是大于 3 的奇數(shù)個(gè)(通常情況下 N 可以設(shè)置為 5),可以基本保證集群內(nèi)各個(gè)節(jié)點(diǎn)不會(huì)同時(shí)宕機(jī)。

        假設(shè)當(dāng)前集群有 5 個(gè)節(jié)點(diǎn),運(yùn)行 Redlock 算法的客戶(hù)端依次執(zhí)行下面各個(gè)步驟,來(lái)完成獲取鎖的操作

        • 客戶(hù)端記錄當(dāng)前系統(tǒng)時(shí)間,以毫秒為單位;
        • 依次嘗試從 5 個(gè) Redis 實(shí)例中,使用相同的 key 獲取鎖,當(dāng)向 Redis 請(qǐng)求獲取鎖時(shí),客戶(hù)端應(yīng)該設(shè)置一個(gè)網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間,超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間,避免因?yàn)榫W(wǎng)絡(luò)故障出現(xiàn)的問(wèn)題;
        • 客戶(hù)端使用當(dāng)前時(shí)間減去開(kāi)始獲取鎖時(shí)間就得到了獲取鎖使用的時(shí)間,當(dāng)且僅當(dāng)從半數(shù)以上的 Redis 節(jié)點(diǎn)獲取到鎖,并且當(dāng)使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功;
        • 如果獲取到了鎖,key 的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間,減少超時(shí)的幾率;
        • 如果獲取鎖失敗,客戶(hù)端應(yīng)該在所有的 Redis 實(shí)例上進(jìn)行解鎖,即使是上一步操作請(qǐng)求失敗的節(jié)點(diǎn),防止因?yàn)榉?wù)端響應(yīng)消息丟失,但是實(shí)際數(shù)據(jù)添加成功導(dǎo)致的不一致。

        也就是說(shuō),假設(shè)鎖30秒過(guò)期,三個(gè)節(jié)點(diǎn)加鎖花了31秒,自然是加鎖失敗了

        在 Redis 官方推薦的 Java 客戶(hù)端 Redisson 中,內(nèi)置了對(duì) RedLock 的實(shí)現(xiàn)

        https://redis.io/topics/distlock

        https://github.com/redisson/redisson/wiki

        RedLock問(wèn)題:

        RedLock 只是保證了鎖的高可用性,并沒(méi)有保證鎖的正確性

        RedLock 是一個(gè)嚴(yán)重依賴(lài)系統(tǒng)時(shí)鐘的分布式系統(tǒng)

        Martin 對(duì) RedLock 的批評(píng):

        • 對(duì)于提升效率的場(chǎng)景下,RedLock 太重。
        • 對(duì)于對(duì)正確性要求極高的場(chǎng)景下,RedLock 并不能保證正確性。

        最后

        覺(jué)得有收獲,希望幫忙點(diǎn)贊,轉(zhuǎn)發(fā)下哈,謝謝,謝謝

        微信搜索:月伴飛魚(yú),交個(gè)朋友

        公眾號(hào)后臺(tái)回復(fù)666,可以獲得免費(fèi)電子書(shū)籍

        這些線(xiàn)程安全的坑,你在工作中踩了么?


        關(guān)于內(nèi)存安全問(wèn)題,你應(yīng)該了解的幾點(diǎn)!


        慢查詢(xún)引發(fā)的車(chē)禍現(xiàn)場(chǎng),案例分析!

        瀏覽 38
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 翔田千里 無碼破解 | 成人免费性生活视频 | 深夜福利日韩 | 亚洲精品字幕 | 午夜天堂精品久久久久 |