<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 5493字,需瀏覽 11分鐘

           ·

          2021-05-19 09:03

          前言

          之前的個(gè)人網(wǎng)站已經(jīng)下線了,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話,多干貨!

          開始吧!

          為什么需要分布式鎖

          為什么需要分布式鎖

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

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

          比如一個(gè)操作要修改用戶的狀態(tài),修改狀態(tài)需要先讀出用戶的狀態(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)說(shuō),生產(chǎn)環(huán)境可用的分布式鎖需要滿足以下幾點(diǎn):

          • 互斥性,互斥是鎖的基本特征,同一時(shí)刻只能有一個(gè)線程持有鎖,執(zhí)行臨界操作;
          • 超時(shí)釋放,超時(shí)釋放是鎖的另一個(gè)必備特性,可以對(duì)比 MySQL InnoDB 引擎中的 innodb_lock_wait_timeout配置,通過(guò)超時(shí)釋放,防止不必要的線程等待和資源浪費(fèi);
          • 可重入性,在分布式環(huán)境下,同一個(gè)節(jié)點(diǎn)上的同一個(gè)線程如果獲取了鎖之后,再次請(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è)線程在獲取鎖之后由于某些異常因素(比如宕機(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)題

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

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

          所以最好的方式是在解鎖時(shí)判斷鎖是否是自己的,我們可以在設(shè)置key的時(shí)候?qū)alue設(shè)置為一個(gè)唯一值uniqueValue(可以是隨機(jī)值、UUID、或者機(jī)器號(hào)+線程號(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è)分開的操作,在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ò)程中,其他客戶端的命令都需要等待該Lua腳本執(zhí)行完才能執(zhí)行。

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

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

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

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

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

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

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

          獲取鎖成功就會(huì)開啟一個(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ì)取而代之,客戶端上卻并沒(méi)有明顯感知。原先第一個(gè)客戶端在主節(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è)客戶端過(guò)來(lái)請(qǐng)求加鎖時(shí),立即就批準(zhǔn)了。這樣就會(huì)導(dǎo)致系統(tǒng)中同樣一把鎖被兩個(gè)客戶端同時(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)行讀寫,意味著相比單實(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 算法的客戶端依次執(zhí)行下面各個(gè)步驟,來(lái)完成獲取鎖的操作

          • 客戶端記錄當(dāng)前系統(tǒng)時(shí)間,以毫秒為單位;
          • 依次嘗試從 5 個(gè) Redis 實(shí)例中,使用相同的 key 獲取鎖,當(dāng)向 Redis 請(qǐng)求獲取鎖時(shí),客戶端應(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)題;
          • 客戶端使用當(dāng)前時(shí)間減去開始獲取鎖時(shí)間就得到了獲取鎖使用的時(shí)間,當(dāng)且僅當(dāng)從半數(shù)以上的 Redis 節(jié)點(diǎn)獲取到鎖,并且當(dāng)使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功;
          • 如果獲取到了鎖,key 的真正有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間,減少超時(shí)的幾率;
          • 如果獲取鎖失敗,客戶端應(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 客戶端 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)重依賴系統(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ā)下哈,謝謝,謝謝

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

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

          這些線程安全的坑,你在工作中踩了么?


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


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

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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日女人精品视品在线观看 | аⅴ资源新版在线天堂 | 亚洲婷婷五月综合 | 欧美精品在线播放 | 丁香五月天在线婷婷 |