<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>

          三種分布式鎖詳解!

          共 8000字,需瀏覽 16分鐘

           ·

          2022-02-27 00:22

          相關(guān)閱讀:2T架構(gòu)師學習資料干貨分享

          作者:zhong0316
          來源:jianshu.com/p/a64df8dcfade

          Java中的鎖主要包括synchronized鎖和JUC包中的鎖,這些鎖都是針對單個JVM實例上的鎖,對于分布式環(huán)境如果我們需要加鎖就顯得無能為力。在單個JVM實例上,鎖的競爭者通常是一些不同的線程,而在分布式環(huán)境中,鎖的競爭者通常是一些不同的線程或者進程。如何實現(xiàn)在分布式環(huán)境中對一個對象進行加鎖呢?答案就是分布式鎖。

          分布式鎖實現(xiàn)方案

          目前分布式鎖的實現(xiàn)方案主要包括三種:

          1. 基于數(shù)據(jù)庫(唯一索引)

          2. 基于緩存(Redis,memcached,tair)

          3. 基于Zookeeper

          基于數(shù)據(jù)庫實現(xiàn)分布式鎖主要是利用數(shù)據(jù)庫的唯一索引來實現(xiàn),唯一索引天然具有排他性,這剛好符合我們對鎖的要求:同一時刻只能允許一個競爭者獲取鎖。加鎖時我們在數(shù)據(jù)庫中插入一條鎖記錄,利用業(yè)務(wù)id進行防重。當?shù)谝粋€競爭者加鎖成功后,第二個競爭者再來加鎖就會拋出唯一索引沖突,如果拋出這個異常,我們就判定當前競爭者加鎖失敗。防重業(yè)務(wù)id需要我們自己來定義,例如我們的鎖對象是一個方法,則我們的業(yè)務(wù)防重id就是這個方法的名字,如果鎖定的對象是一個類,則業(yè)務(wù)防重id就是這個類名。

          基于緩存實現(xiàn)分布式鎖:理論上來說使用緩存來實現(xiàn)分布式鎖的效率最高,加鎖速度最快,因為Redis幾乎都是純內(nèi)存操作,而基于數(shù)據(jù)庫的方案和基于Zookeeper的方案都會涉及到磁盤文件IO,效率相對低下。一般使用Redis來實現(xiàn)分布式鎖都是利用Redis的SETNX key value這個命令,只有當key不存在時才會執(zhí)行成功,如果key已經(jīng)存在則命令執(zhí)行失敗。

          基于Zookeeper:Zookeeper一般用作配置中心,其實現(xiàn)分布式鎖的原理和Redis類似,我們在Zookeeper中創(chuàng)建瞬時節(jié)點,利用節(jié)點不能重復(fù)創(chuàng)建的特性來保證排他性。

          在實現(xiàn)分布式鎖的時候我們需要考慮一些問題,例如:分布式鎖是否可重入,分布式鎖的釋放時機,分布式鎖服務(wù)端是否有單點問題等。

          基于數(shù)據(jù)庫實現(xiàn)分布式鎖

          上面已經(jīng)分析了基于數(shù)據(jù)庫實現(xiàn)分布式鎖的基本原理:通過唯一索引保持排他性,加鎖時插入一條記錄,解鎖是刪除這條記錄。下面我們就簡要實現(xiàn)一下基于數(shù)據(jù)庫的分布式鎖。

          表設(shè)計

          CREATE TABLE `distributed_lock` (
            `id` bigint(20NOT NULL AUTO_INCREMENT,
            `unique_mutex` varchar(255NOT NULL COMMENT '業(yè)務(wù)防重id',
            `holder_id` varchar(255NOT NULL COMMENT '鎖持有者id',
            `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
            PRIMARY KEY (`id`),
            UNIQUE KEY `mutex_index` (`unique_mutex`)
          ENGINE=InnoDB DEFAULT CHARSET=utf8;

          id字段是數(shù)據(jù)庫的自增id,unique_mutex字段就是我們的防重id,也就是加鎖的對象,此對象唯一。在這張表上我們加了一個唯一索引,保證unique_mutex唯一性。holder_id代表競爭到鎖的持有者id。

          加鎖

          insert into distributed_lock(unique_mutex, holder_id) values ('unique_mutex''holder_id');

          如果當前sql執(zhí)行成功代表加鎖成功,如果拋出唯一索引異常(DuplicatedKeyException)則代表加鎖失敗,當前鎖已經(jīng)被其他競爭者獲取。

          解鎖

          delete from methodLock where unique_mutex='unique_mutex' and holder_id='holder_id';

          解鎖很簡單,直接刪除此條記錄即可。

          分析

          是否可重入:就以上的方案來說,我們實現(xiàn)的分布式鎖是不可重入的,即是是同一個競爭者,在獲取鎖后未釋放鎖之前再來加鎖,一樣會加鎖失敗,因此是不可重入的。解決不可重入問題也很簡單:加鎖時判斷記錄中是否存在unique_mutex的記錄,如果存在且holder_id和當前競爭者id相同,則加鎖成功。這樣就可以解決不可重入問題。

          鎖釋放時機:設(shè)想如果一個競爭者獲取鎖時候,進程掛了,此時distributed_lock表中的這條記錄就會一直存在,其他競爭者無法加鎖。為了解決這個問題,每次加鎖之前我們先判斷已經(jīng)存在的記錄的創(chuàng)建時間和當前系統(tǒng)時間之間的差是否已經(jīng)超過超時時間,如果已經(jīng)超過則先刪除這條記錄,再插入新的記錄。另外在解鎖時,必須是鎖的持有者來解鎖,其他競爭者無法解鎖。這點可以通過holder_id字段來判定。

          數(shù)據(jù)庫單點問題:單個數(shù)據(jù)庫容易產(chǎn)生單點問題:如果數(shù)據(jù)庫掛了,我們的鎖服務(wù)就掛了。對于這個問題,可以考慮實現(xiàn)數(shù)據(jù)庫的高可用方案,例如MySQL的MHA高可用解決方案。

          基于緩存實現(xiàn)分布式鎖,以Redis為例

          使用Jedis來和Redis通信。

          加鎖

          public class RedisTool {

              private static final String LOCK_SUCCESS = "OK";
              private static final String SET_IF_NOT_EXIST = "NX";
              private static final String SET_WITH_EXPIRE_TIME = "PX";

              /**
               * 加鎖
               * @param jedis Redis客戶端
               * @param lockKey 鎖的key
               * @param requestId 競爭者id
               * @param expireTime 鎖超時時間,超時之后鎖自動釋放
               * @return 
               */

              public static boolean getDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
                  String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
                  return "OK".equals(result);
              }

          }

          可以看到,我們加鎖就一行代碼:
          jedis.set(String key, String value, String nxxx, String expx, int time);
          這個set()方法一共五個形參:
          第一個為key,我們使用key來當鎖,因為key是唯一的。
          第二個為value,這里寫的是鎖競爭者的id,在解鎖時,我們需要判斷當前解鎖的競爭者id是否為鎖持有者。
          第三個為nxxx,這個參數(shù)我們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經(jīng)存在,則不做任何操作。
          第四個為expx,這個參數(shù)我們傳的是PX,意思是我們要給這個key加一個過期時間的設(shè)置,具體時間由第五個參數(shù)決定;
          第五個參數(shù)為time,與第四個參數(shù)相呼應(yīng),代表key的過期時間。
          總的來說,執(zhí)行上面的set()方法就只會導致兩種結(jié)果:1.當前沒有鎖(key不存在),那么久進行加鎖操作,并對鎖設(shè)置一個有效期,同時value表示加鎖的客戶端。2.已經(jīng)有鎖存在,不做任何操作。
          上述解鎖請求中,SET_IF_NOT_EXIST(不存在則執(zhí)行)保證了加鎖請求的排他性,緩存超時機制保證了即使一個競爭者加鎖之后掛了,也不會產(chǎn)生死鎖問題:超時之后其他競爭者依然可以獲取鎖。通過設(shè)置value為競爭者的id,保證了只有鎖的持有者才能來解鎖,否則任何競爭者都能解鎖,那豈不是亂套了。

          解鎖

          public class RedisTool {

              private static final Long RELEASE_SUCCESS = 1L;

              /**
               * 釋放分布式鎖
               * @param jedis Redis客戶端
               * @param lockKey 鎖
               * @param requestId 鎖持有者id
               * @return 是否釋放成功
               */

              public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
                  String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                  Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
                  return RELEASE_SUCCESS.equals(result);
              }
          }

          解鎖的步驟:

          1. 判斷當前解鎖的競爭者id是否為鎖的持有者,如果不是直接返回失敗,如果是則進入第2步。另外,搜索公眾號Linux就該這樣學后臺回復(fù)“Linux”,獲取一份驚喜禮包。

          2. 刪除key,如果刪除成功,返回解鎖成功,否則解鎖失敗。

          注意到這里解鎖其實是分為2個步驟,涉及到解鎖操作的一個原子性操作問題。這也是為什么我們解鎖的時候用Lua腳本來實現(xiàn),因為Lua腳本可以保證操作的原子性。那么這里為什么需要保證這兩個步驟的操作是原子操作呢?
          設(shè)想:假設(shè)當前鎖的持有者是競爭者1,競爭者1來解鎖,成功執(zhí)行第1步,判斷自己就是鎖持有者,這是還未執(zhí)行第2步。這是鎖過期了,然后競爭者2對這個key進行了加鎖。加鎖完成后,競爭者1又來執(zhí)行第2步,此時錯誤產(chǎn)生了:競爭者1解鎖了不屬于自己持有的鎖??赡軙腥藛枮槭裁锤偁幷?執(zhí)行完第1步之后突然停止了呢?這個問題其實很好回答,例如競爭者1所在的JVM發(fā)生了GC停頓,導致競爭者1的線程停頓。這樣的情況發(fā)生的概率很低,但是請記住即使只有萬分之一的概率,在線上環(huán)境中完全可能發(fā)生。因此必須保證這兩個步驟的操作是原子操作。

          分析

          是否可重入:以上實現(xiàn)的鎖是不可重入的,如果需要實現(xiàn)可重入,在SET_IF_NOT_EXIST之后,再判斷key對應(yīng)的value是否為當前競爭者id,如果是返回加鎖成功,否則失敗。

          鎖釋放時機:加鎖時我們設(shè)置了key的超時,當超時后,如果還未解鎖,則自動刪除key達到解鎖的目的。如果一個競爭者獲取鎖之后掛了,我們的鎖服務(wù)最多也就在超時時間的這段時間之內(nèi)不可用。

          Redis單點問題:如果需要保證鎖服務(wù)的高可用,可以對Redis做高可用方案:Redis集群+主從切換。目前都有比較成熟的解決方案。

          基于Zookeeper實現(xiàn)分布式鎖

          加鎖和解鎖流程

          利用Zookeeper創(chuàng)建臨時有序節(jié)點來實現(xiàn)分布式鎖:

          1. 當一個客戶端來請求時,在鎖的空間下面創(chuàng)建一個臨時有序節(jié)點。

          2. 如果當前節(jié)點的序列是這個空間下面最小的,則代表加鎖成功,否則加鎖失敗,加鎖失敗后設(shè)置Watcher,等待前面節(jié)點的通知。

          3. 當前節(jié)點監(jiān)聽其前面一個節(jié)點,如果前面一個節(jié)點刪除了就通知當前節(jié)點。

          4. 當解鎖時當前節(jié)點通知其后繼節(jié)點,并刪除當前節(jié)點。

          其基本思想類似于AQS中的等待隊列,將請求排隊處理。其流程圖如下:

          分析

          解決不可重入:客戶端加鎖時將主機和線程信息寫入鎖中,下一次再來加鎖時直接和序列最小的節(jié)點對比,如果相同,則加鎖成功,鎖重入。

          鎖釋放時機:由于我們創(chuàng)建的節(jié)點是順序臨時節(jié)點,當客戶端獲取鎖成功之后突然session會話斷開,ZK會自動刪除這個臨時節(jié)點。

          單點問題:ZK是集群部署的,主要一半以上的機器存活,就可以保證服務(wù)可用性。

          利用curator實現(xiàn)

          Zookeeper第三方客戶端curator中已經(jīng)實現(xiàn)了基于Zookeeper的分布式鎖。利用curator加鎖和解鎖的代碼如下:

          // 加鎖,支持超時,可重入
          public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
              try {
                  return interProcessMutex.acquire(timeout, unit);
              } catch (Exception e) {
                  e.printStackTrace();
              }
              return true;
          }
          // 解鎖
          public boolean unlock() {
              try {
                  interProcessMutex.release();
              } catch (Throwable e) {
                  log.error(e.getMessage(), e);
              } finally {
                  executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
              }
              return true;
          }

          三種方案比較


          -End-

          1、985副教授工資曝光

          2、心態(tài)崩了!稅前2萬4,到手1萬4,年終獎扣稅方式1月1日起施行~

          3、雷軍做程序員時寫的博客,很強大!

          4、人臉識別的時候,一定要穿上衣服啊!

          5、清華大學:2021 元宇宙研究報告!

          6、績效被打3.25B,員工將支付寶告上了法院,判了

          瀏覽 41
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  上床视频网站在线 | 国产视频播放 | 亚洲欧美不卡高清在线 | 在线观看成人毛片 | 老女人性爱视频 |