<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 分布式鎖

          共 12812字,需瀏覽 26分鐘

           ·

          2021-04-01 11:48

          什么是分布式鎖

          說到 Redis,我們第一想到的功能就是可以緩存數(shù)據(jù),除此之外,Redis 因?yàn)閱芜M(jìn)程、性能高的特點(diǎn),它還經(jīng)常被用于做分布式鎖。

          鎖我們都知道,在程序中的作用就是同步工具,保證共享資源在同一時(shí)刻只能被一個(gè)線程訪問,Java 中的鎖我們都很熟悉了,像synchronizedLock都是我們經(jīng)常使用的,但是 Java 的鎖只能保證單機(jī)的時(shí)候有效,分布式集群環(huán)境就無能為力了,這個(gè)時(shí)候我們就需要用到分布式鎖。

          分布式鎖,顧名思義,就是分布式項(xiàng)目開發(fā)中用到的鎖,可以用來控制分布式系統(tǒng)之間同步訪問共享資源,一般來說,分布式鎖需要滿足的特性有這么幾點(diǎn):

          1. 互斥性 :在任何時(shí)刻,對(duì)于同一條數(shù)據(jù),只有一臺(tái)應(yīng)用可以獲取到分布式鎖;
          2. 高可用性 :在分布式場(chǎng)景下,一小部分服務(wù)器宕機(jī)不影響正常使用,這種情況就需要將提供分布式鎖的服務(wù)以集群的方式部署;
          3. 防止鎖超時(shí) :如果客戶端沒有主動(dòng)釋放鎖,服務(wù)器會(huì)在一段時(shí)間之后自動(dòng)釋放鎖,防止客戶端宕機(jī)或者網(wǎng)絡(luò)不可達(dá)時(shí)產(chǎn)生死鎖;
          4. 獨(dú)占性 :加鎖解鎖必須由同一臺(tái)服務(wù)器進(jìn)行,也就是鎖的持有者才可以釋放鎖,不能出現(xiàn)你加的鎖,別人給你解鎖了;

          業(yè)界里可以實(shí)現(xiàn)分布式鎖效果的工具很多,但操作無非這么幾個(gè):加鎖、解鎖、防止鎖超時(shí)。

          既然本文說的是 Redis 分布式鎖,那我們理所當(dāng)然就以 Redis 的知識(shí)點(diǎn)來延伸。

          實(shí)現(xiàn)鎖的命令

          先介紹下 Redis 的幾個(gè)命令,

          1、SETNX,用法是SETNX key value

          SETNX是『 SET if Not eXists』(如果不存在,則 SET)的簡(jiǎn)寫,設(shè)置成功就返回 1,否則返回 0。

          可以看出,當(dāng)把keylock的值設(shè)置為"Java"后,再設(shè)置成別的值就會(huì)失敗,看上去很簡(jiǎn)單,也好像獨(dú)占了鎖,但有個(gè)致命的問題,就是key沒有過期時(shí)間,這樣一來,除非手動(dòng)刪除 key 或者獲取鎖后設(shè)置過期時(shí)間,不然其他線程永遠(yuǎn)拿不到鎖。

          既然這樣,我們給 key 加個(gè)過期時(shí)間總可以吧,直接讓線程獲取鎖的時(shí)候執(zhí)行兩步操作:

          SETNX Key 1
          EXPIRE Key Seconds

          這個(gè)方案也有問題,因?yàn)楂@取鎖和設(shè)置過期時(shí)間分成兩步了,不是原子性操作,有可能獲取鎖成功但設(shè)置時(shí)間失敗,那樣不就白干了嗎。

          不過也不用急,這種事情 Redis 官方早為我們考慮到了,所以就引出了下面這個(gè)命令

          2、SETEX,用法SETEX key seconds value

          將值 value 關(guān)聯(lián)到 key ,并將 key 的生存時(shí)間設(shè)為 seconds (以秒為單位)。如果 key 已經(jīng)存在,SETEX 命令將覆寫舊值。

          這個(gè)命令類似于以下兩個(gè)命令:

          SET key value
          EXPIRE key seconds  # 設(shè)置生存時(shí)間

          這兩步動(dòng)作是原子性的,會(huì)在同一時(shí)間完成。

          3、PSETEX ,用法PSETEX key milliseconds value

          這個(gè)命令和SETEX命令相似,但它以毫秒為單位設(shè)置  key  的生存時(shí)間,而不是像SETEX命令那樣,以秒為單位。

          不過,從 Redis 2.6.12 版本開始,SET命令可以通過參數(shù)來實(shí)現(xiàn)和SETNX、SETEXPSETEX   三個(gè)命令的效果。

          就比如這條命令

          SET key value NX EX seconds

          加上 NX、EX 參數(shù)后,效果就相當(dāng)于 SETEX,這也是 Redis 獲取鎖寫法里面最常見的。

          怎么釋放鎖

          釋放鎖的命令就簡(jiǎn)單了,直接刪除 key 就行,但我們前面說了,因?yàn)榉植际芥i必須由鎖的持有者自己釋放,所以我們必須先確保當(dāng)前釋放鎖的線程是持有者,沒問題了再刪除,這樣一來,就變成兩個(gè)步驟了,似乎又違背了原子性了,怎么辦呢?

          不慌,我們可以用 lua 腳本把兩步操作做拼裝,就好像這樣:

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

          KEYS[1]是當(dāng)前 key 的名稱,ARGV[1]可以是當(dāng)前線程的 ID(或者其他不固定的值,能識(shí)別所屬線程即可),這樣就可以防止持有過期鎖的線程,或者其他線程誤刪現(xiàn)有鎖的情況出現(xiàn)。

          代碼實(shí)現(xiàn)

          知道了原理后,我們就可以手寫代碼來實(shí)現(xiàn) Redis 分布式鎖的功能了,因?yàn)楸疚牡哪康闹饕菫榱酥v解原理,不是為了教大家怎么寫分布式鎖,所以我就用偽代碼實(shí)現(xiàn)了。

          首先是 Redis 鎖的工具類,包含了加鎖和解鎖的基礎(chǔ)方法:

          public class RedisLockUtil {

              private String LOCK_KEY = "redis_lock";

              // key的持有時(shí)間,5ms
              private long EXPIRE_TIME = 5;

              // 等待超時(shí)時(shí)間,1s
              private long TIME_OUT = 1000;

              // Redis命令參數(shù),相當(dāng)于nx和px的命令合集
              private SetParams params = SetParams.setParams().nx().px(EXPIRE_TIME);

              // Redis連接池,連的是本地的Redis客戶端
              JedisPool jedisPool = new JedisPool("127.0.0.1"6379);

              /**
               * 加鎖
               *
               * @param id
               *            線程的id,或者其他可識(shí)別當(dāng)前線程且不重復(fù)的字段
               * @return
               */

              public boolean lock(String id) {
                  Long start = System.currentTimeMillis();
                  Jedis jedis = jedisPool.getResource();
                  try {
                      for (;;) {
                          // SET命令返回OK ,則證明獲取鎖成功
                          String lock = jedis.set(LOCK_KEY, id, params);
                          if ("OK".equals(lock)) {
                              return true;
                          }
                          // 否則循環(huán)等待,在TIME_OUT時(shí)間內(nèi)仍未獲取到鎖,則獲取失敗
                          long l = System.currentTimeMillis() - start;
                          if (l >= TIME_OUT) {
                              return false;
                          }
                          try {
                              // 休眠一會(huì),不然反復(fù)執(zhí)行循環(huán)會(huì)一直失敗
                              Thread.sleep(100);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  } finally {
                      jedis.close();
                  }
              }

              /**
               * 解鎖
               *
               * @param id
               *            線程的id,或者其他可識(shí)別當(dāng)前線程且不重復(fù)的字段
               * @return
               */

              public boolean unlock(String id) {
                  Jedis jedis = jedisPool.getResource();
                  // 刪除key的lua腳本
                  String script = "if Redis.call('get',KEYS[1]) == ARGV[1] then" + "   return Redis.call('del',KEYS[1]) " + "else"
                      + "   return 0 " + "end";
                  try {
                      String result =
                          jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();
                      return "1".equals(result);
                  } finally {
                      jedis.close();
                  }
              }
          }

          具體的代碼作用注釋已經(jīng)寫得很清楚了,然后我們就可以寫一個(gè) demo 類來測(cè)試一下效果:

          public class RedisLockTest {
              private static RedisLockUtil demo = new RedisLockUtil();
              private static Integer NUM = 101;

              public static void main(String[] args) {
                  for (int i = 0; i < 100; i++) {
                      new Thread(() -> {
                          String id = Thread.currentThread().getId() + "";
                          boolean isLock = demo.lock(id);
                          try {
                           // 拿到鎖的話,就對(duì)共享參數(shù)減一
                              if (isLock) {
                                  NUM--;
                                  System.out.println(NUM);
                              }
                          } finally {
                           // 釋放鎖一定要注意放在finally
                              demo.unlock(id);
                          }
                      }).start();
                  }
              }
          }

          我們創(chuàng)建 100 個(gè)線程來模擬并發(fā)的情況,執(zhí)行后的結(jié)果是這樣的:

          可以看出,鎖的效果達(dá)到了,線程安全是可以保證的。

          當(dāng)然,上面的代碼只是簡(jiǎn)單的實(shí)現(xiàn)了效果,功能肯定是不完整的,一個(gè)健全的分布式鎖要考慮的方面還有很多,實(shí)際設(shè)計(jì)起來不是那么容易的。

          我們的目的只是為了學(xué)習(xí)和了解原理,手寫一個(gè)工業(yè)級(jí)的分布式鎖工具不現(xiàn)實(shí),也沒必要,類似的開源工具一大堆(Redisson),原理都差不多,而且早已經(jīng)過業(yè)界同行的檢驗(yàn),直接拿來用就行。

          雖然功能是實(shí)現(xiàn)了,但其實(shí)從設(shè)計(jì)上來說,這樣的分布式鎖存在著很大的缺陷,這也是本篇文章想重點(diǎn)探討的內(nèi)容,那到底存在哪些缺陷呢?

          分布式鎖的缺陷

          客戶端長(zhǎng)時(shí)間阻塞導(dǎo)致鎖失效問題

          客戶端 1 得到了鎖,因?yàn)榫W(wǎng)絡(luò)問題或者 GC 等原因?qū)е麻L(zhǎng)時(shí)間阻塞,然后業(yè)務(wù)程序還沒執(zhí)行完鎖就過期了,這時(shí)候客戶端 2 也能正常拿到鎖,可能會(huì)導(dǎo)致線程安全的問題。

          那么該如何防止這樣的異常呢?我們先不說解決方案,介紹完其他的缺陷后再來討論。

          Redis 服務(wù)器時(shí)鐘漂移問題

          如果 Redis 服務(wù)器的機(jī)器時(shí)鐘發(fā)生了向前跳躍,就會(huì)導(dǎo)致這個(gè) key 過早超時(shí)失效,比如說客戶端 1 拿到鎖后,key 的過期時(shí)間是 12:02 分,但 Redis 服務(wù)器本身的時(shí)鐘比客戶端快了 2 分鐘,導(dǎo)致 key 在 12:00 的時(shí)候就失效了,這時(shí)候,如果客戶端 1 還沒有釋放鎖的話,就可能導(dǎo)致多個(gè)客戶端同時(shí)持有同一把鎖的問題。

          單點(diǎn)實(shí)例安全問題

          如果 Redis 是單 master 模式的,當(dāng)這臺(tái)機(jī)宕機(jī)的時(shí)候,那么所有的客戶端都獲取不到鎖了,為了提高可用性,可能就會(huì)給這個(gè) master 加一個(gè) slave,但是因?yàn)?Redis 的主從同步是異步進(jìn)行的,可能會(huì)出現(xiàn)客戶端 1 設(shè)置完鎖后,master 掛掉,slave 提升為 master,因?yàn)楫惒綇?fù)制的特性,客戶端 1 設(shè)置的鎖丟失了,這時(shí)候客戶端 2 設(shè)置鎖也能夠成功,導(dǎo)致客戶端 1 和客戶端 2 同時(shí)擁有鎖。

          為了解決 Redis 單點(diǎn)問題,Redis 的作者提出了RedLock算法。

          RedLock 算法

          該算法的實(shí)現(xiàn)前提在于 Redis 必須是多節(jié)點(diǎn)部署的,可以有效防止單點(diǎn)故障,具體的實(shí)現(xiàn)思路是這樣的:

          1. 獲取當(dāng)前時(shí)間戳(ms);
          2. 先設(shè)定 key 的有效時(shí)長(zhǎng)(TTL),超出這個(gè)時(shí)間就會(huì)自動(dòng)釋放,然后 client(客戶端)嘗試使用相同的 key 和 value 對(duì)所有 Redis 實(shí)例進(jìn)行設(shè)置,每次鏈接 Redis 實(shí)例時(shí)設(shè)置一個(gè)比 TTL 短很多的超時(shí)時(shí)間,這是為了不要過長(zhǎng)時(shí)間等待已經(jīng)關(guān)閉的 Redis 服務(wù)。并且試著獲取下一個(gè) Redis 實(shí)例。比如:TTL(也就是過期時(shí)間)為 5s,那獲取鎖的超時(shí)時(shí)間就可以設(shè)置成 50ms,所以如果 50ms 內(nèi)無法獲取鎖,就放棄獲取這個(gè)鎖,從而嘗試獲取下個(gè)鎖;
          3. client 通過獲取所有能獲取的鎖后的時(shí)間減去第一步的時(shí)間,還有 Redis 服務(wù)器的時(shí)鐘漂移誤差,然后這個(gè)時(shí)間差要小于 TTL 時(shí)間并且成功設(shè)置鎖的實(shí)例數(shù)>= N/2 + 1(N 為 Redis 實(shí)例的數(shù)量),那么加鎖成功。比如 TTL 是 5s,連接 Redis 獲取所有鎖用了 2s,然后再減去時(shí)鐘漂移(假設(shè)誤差是 1s 左右),那么鎖的真正有效時(shí)長(zhǎng)就只有 2s 了;
          4. 如果客戶端由于某些原因獲取鎖失敗,便會(huì)開始解鎖所有 Redis 實(shí)例。

          根據(jù)這樣的算法,我們假設(shè)有 5 個(gè) Redis 實(shí)例的話,那么 client 只要獲取其中 3 臺(tái)以上的鎖就算是成功了,用流程圖演示大概就像這樣:

          好了,算法也介紹完了,從設(shè)計(jì)上看,毫無疑問,RedLock 算法的思想主要是為了有效防止 Redis 單點(diǎn)故障的問題,而且在設(shè)計(jì) TTL 的時(shí)候也考慮到了服務(wù)器時(shí)鐘漂移的誤差,讓分布式鎖的安全性提高了不少。

          但事實(shí)真的是這樣嗎?反正我個(gè)人的話感覺效果一般般,

          首先第一點(diǎn),我們可以看到,在 RedLock 算法中,鎖的有效時(shí)間會(huì)減去連接 Redis 實(shí)例的時(shí)長(zhǎng),如果這個(gè)過程因?yàn)榫W(wǎng)絡(luò)問題導(dǎo)致耗時(shí)太長(zhǎng)的話,那么最終留給鎖的有效時(shí)長(zhǎng)就會(huì)大大減少,客戶端訪問共享資源的時(shí)間很短,很可能程序處理的過程中鎖就到期了。而且,鎖的有效時(shí)間還需要減去服務(wù)器的時(shí)鐘漂移,但是應(yīng)該減多少合適呢,要是這個(gè)值設(shè)置不好,很容易出現(xiàn)問題。

          然后第二點(diǎn),這樣的算法雖然考慮到用多節(jié)點(diǎn)來防止 Redis 單點(diǎn)故障的問題,但但如果有節(jié)點(diǎn)發(fā)生崩潰重啟的話,還是有可能出現(xiàn)多個(gè)客戶端同時(shí)獲取鎖的情況。

          假設(shè)一共有 5 個(gè) Redis 節(jié)點(diǎn):A、B、C、D、E,客戶端 1 和 2 分別加鎖

          1. 客戶端 1 成功鎖住了 A,B,C,獲取鎖成功(但 D 和 E 沒有鎖住)。
          2. 節(jié)點(diǎn) C 的 master 掛了,然后鎖還沒同步到 slave,slave 升級(jí)為 master 后丟失了客戶端 1 加的鎖。
          3. 客戶端 2 這個(gè)時(shí)候獲取鎖,鎖住了 C,D,E,獲取鎖成功。

          這樣,客戶端 1 和客戶端 2 就同時(shí)拿到了鎖,程序安全的隱患依然存在。除此之外,如果這些節(jié)點(diǎn)里面某個(gè)節(jié)點(diǎn)發(fā)生了時(shí)間漂移的話,也有可能導(dǎo)致鎖的安全問題。

          所以說,雖然通過多實(shí)例的部署提高了可用性和可靠性,但 RedLock 并沒有完全解決 Redis 單點(diǎn)故障存在的隱患,也沒有解決時(shí)鐘漂移、客戶端長(zhǎng)時(shí)間阻塞而導(dǎo)致的鎖超時(shí)失效問題。

          從這一點(diǎn)上看,RedLock 算法也并沒有保證鎖的安全性。

          結(jié)論

          有人可能要進(jìn)一步問了,那該怎么做才能保證鎖的絕對(duì)安全呢?

          對(duì)此我只能說,魚和熊掌不可兼得,我們之所以用 Redis 作為分布式鎖的工具,很大程度上是因?yàn)?Redis 本身效率高且單進(jìn)程的特點(diǎn),即使在高并發(fā)的情況下也能很好的保證性能,但很多時(shí)候,性能和安全不能完全兼顧,如果你一定要保證鎖的安全性的話,可以用其他的中間件如 db、zookeeper 來做控制,這些工具能很好的保證鎖的安全,但性能方面只能說是差強(qiáng)人意,否則大家早就用上了。

          一般來說,用 Redis 控制共享資源并且還要求數(shù)據(jù)安全要求較高的話,最終的保底方案是對(duì)業(yè)務(wù)數(shù)據(jù)做冪等控制,這樣一來,即使出現(xiàn)多個(gè)客戶端獲得鎖的情況也不會(huì)影響數(shù)據(jù)的一致性。當(dāng)然,也不是所有的場(chǎng)景都適合這么做,具體怎么取舍就需要各位看官自己處理啦,畢竟,沒有完美的技術(shù),只有適合的才是最好的。

          推薦?? :1049天,100K!簡(jiǎn)單復(fù)盤!

          推薦?? :匯報(bào)一下2020的工作

          推薦?? :Github掘金計(jì)劃:Github上的一些優(yōu)質(zhì)項(xiàng)目搜羅

          我是 Guide哥,擁抱開源,喜歡烹飪。Github 接近 10w 點(diǎn)贊的開源項(xiàng)目 JavaGuide 的作者。未來幾年,希望持續(xù)完善 JavaGuide,爭(zhēng)取能夠幫助更多學(xué)習(xí) Java 的小伙伴!共勉!凎!
          原創(chuàng)不易,歡迎點(diǎn)贊分享。咱們下期再會(huì)!
          瀏覽 46
          點(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>
                  伊人激情综合 | 狠狠狠狠撸,天天日 | 91小电影| 五月丁香综合久久 | 天天干干天天 |