分布式鎖(數(shù)據(jù)庫、Redis窝趣、ZK)拍了拍你

來自公眾號:非科班的科班
作者:黎杜

前言

標(biāo)題使用最近異撤枋睿火熱的微信拍一拍的方式命名,最近拍一拍的玩法被各位網(wǎng)友玩壞了高帖,出現(xiàn)了各種版本的拍一拍缰儿。

比如:下面的這個版本是不是似曾相識的感覺,曾幾何時你也曾有這種沖動的想法散址,但是奈于生活乖阵,你不得不把這股沖動埋在心底,畢竟沖動是魔鬼预麸。

image

還有比較重口味的瞪浸,有點(diǎn)哭笑不得,這網(wǎng)友的腦洞真大吏祸,要是能把這些心思放在學(xué)習(xí)和事業(yè)上对蒲,必是成大事之人,不得不佩服贡翘,假如你在吃飯蹈矮,千萬別打我。

image

不得不說拍一拍有點(diǎn)東西鸣驱,好了泛鸟,水話就說那么一兩句,在開始真正的分布式鎖講解之前踊东,先來個人的分析一下拍一拍的戰(zhàn)略動機(jī)北滥。

對于老板和一個公司來說,公司付出的每一個商品都是有商用價(jià)值的闸翅,老板不會把沒有商用的價(jià)值功能和產(chǎn)品創(chuàng)造出來再芋。

對于拍一拍這個功能,我想是一個引導(dǎo)性的戰(zhàn)略思維坚冀,對于這個拍一拍新功能济赎,很多網(wǎng)友都會躍躍欲試,不經(jīng)意間就會嘗試遗菠,雙擊別人的頭像進(jìn)行拍一拍联喘。

那么這個雙擊的動作可能將來微信服務(wù)于某項(xiàng)功能而做的準(zhǔn)備,待微信的用戶習(xí)慣了雙擊操作辙纬,微信對于后面的這類操作的功能的推廣會變得更加容易。

好了叭喜,不能再深究下去了贺拣,要是被小馬哥看到,估計(jì)小馬哥就要拍一拍我了,這個純屬個人觀點(diǎn)譬涡,不代表官方的觀點(diǎn)闪幽,下面開始我們的分布式鎖的講解。

分布式鎖簡介

分布式鎖的實(shí)現(xiàn)方式有以下三種方式:「數(shù)據(jù)庫分布式鎖涡匀、Redis實(shí)現(xiàn)分布式鎖盯腌、ZooKeeper實(shí)現(xiàn)分布式鎖」

為什么需要分布式鎖呢陨瘩?在很久以前腕够,用戶全體不大的時候,單體應(yīng)用就可以足夠滿足用戶的所有請求舌劳,當(dāng)用戶增加的時候帚湘,出現(xiàn)了一定的并發(fā)度,可以使用簡單的鎖機(jī)制來協(xié)調(diào)并發(fā)的共享資源的獲取甚淡。

但是大诸,隨著業(yè)務(wù)的增大,用戶數(shù)量的增加贯卦,為了滿足業(yè)務(wù)的高效性资柔,集群的出現(xiàn),簡單的鎖機(jī)制已經(jīng)不能夠滿足協(xié)調(diào)多個應(yīng)用之間的共享資源了撵割,于是就出現(xiàn)了分布式鎖讨越。

分布式鎖是協(xié)調(diào)集群中多應(yīng)用之間的共享資源的獲取的一種方式,可以說它是一種約束私植、規(guī)則艘蹋。

那么對于一個分布式系統(tǒng)中分布式鎖應(yīng)該滿足什么條件呢?也就是它應(yīng)該具備怎樣的約束外遇、規(guī)則注簿,下面是我總結(jié)的分布式鎖至少擁有的幾個規(guī)則。

1.「鎖的互斥性」:在分布式集群應(yīng)用中跳仿,共享資源的鎖在同一時間只能被一個對象獲取诡渴。2. 「可重入」:為了避免死鎖,這把鎖是可以重入的菲语,并且可以設(shè)置超時妄辩。3. 「高效的加鎖和解鎖」:能夠高效的加鎖和解鎖,獲取鎖和釋放鎖的性能也好山上。4. 「阻塞眼耀、公平」:可以根據(jù)業(yè)務(wù)的需要,考慮是使用阻塞佩憾、還是非阻塞哮伟,公平還是非公平的鎖干花。

一個分布式鎖能夠具備上面的幾種條件,應(yīng)該來說是比較好的分布式鎖了楞黄,但是現(xiàn)實(shí)中沒有十全十美的鎖池凄,對于不同的分布式鎖,沒有最好鬼廓,只能說那種場景更加適合肿仑。

下面我們詳細(xì)的聊一聊上面說的三種分布式鎖的實(shí)現(xiàn)原理,先來看看數(shù)據(jù)庫的分布式鎖碎税。

數(shù)據(jù)庫分布式鎖

在數(shù)據(jù)庫的分布式鎖的實(shí)現(xiàn)中尤慰,分為「悲觀鎖和樂觀鎖」「悲觀鎖的實(shí)現(xiàn)依賴于數(shù)據(jù)庫自身的鎖機(jī)制實(shí)現(xiàn)」蚣录。

若是要測試數(shù)據(jù)庫的悲觀的分布式鎖割择,可以執(zhí)行下面的sql:select … where … for update (排他鎖),注意:where 后面的查詢條件要走索引萎河,若是沒有走索引荔泳,會使用全表掃描,鎖全表虐杯。

當(dāng)一個數(shù)據(jù)庫表被加上了排它鎖玛歌,其它的客戶端是不能夠再對加鎖的數(shù)據(jù)行加任何的鎖,只能等待當(dāng)前持有鎖的釋放鎖擎椰。

全表掃描對于測試就沒有太大意義了支子,where后面的條件是否走索引,要注意自己的索引的使用方式是否正確达舒,并且還取決于「mysql優(yōu)化器」值朋。

排它鎖是基于InnoDB存儲引擎的,在執(zhí)行操作的時候巩搏,在sql中加入for update昨登,可以給數(shù)據(jù)行加上排它鎖。

在代碼的代碼的層面上使用connection.commit();贯底,便可以釋放鎖丰辣,但是數(shù)據(jù)庫復(fù)雜的加鎖和解鎖、事務(wù)等一系列消耗性能的操作禽捆,終歸是無法抗高并發(fā)笙什。

數(shù)據(jù)庫樂觀鎖的方式實(shí)現(xiàn)分布式鎖是基于「版本號控制」的方式實(shí)現(xiàn),類似于「CAS的思想」胚想,它認(rèn)為操作的過程并不會存在并發(fā)的情況琐凭,只有在update version的時候才會去比較。

樂觀鎖的方式并沒有鎖的等待浊服,不會因?yàn)樗却馁Y源淘正,下面來測試一下樂觀鎖的方式實(shí)現(xiàn)的分布式鎖摆马。

樂觀鎖的方式實(shí)現(xiàn)分布式鎖要基于數(shù)據(jù)庫表的方式進(jìn)行實(shí)現(xiàn)臼闻,我們認(rèn)為在數(shù)據(jù)庫表中成功存儲該某方法的線程獲取到該方法的鎖鸿吆,才能操作該方法。

首先要創(chuàng)建一個表用于儲存各個線程操作方法的對應(yīng)該關(guān)系表LOCK

CREATE TABLE `LOCK` ( 
`ID` int PRIMARY KEY NOT NULL AUTO_INCREMENT,  
`METHODNAME` varchar(64) NOT NULL DEFAULT '',
`DESCRIPTION` varchar(1024) NOT NULL DEFAULT '',  
`TIME` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  
UNIQUE KEY `UNIQUEMETHODNAME` (`METHODNAME`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

該表是存儲某個方法的是否已經(jīng)被鎖定的信息述呐,若是被鎖定則無法獲取到該方法的鎖惩淳,這里注意的是使用UNIQUE KEY唯一約束,表示該方法布恩那個夠被第二個線程同時持有乓搬。

當(dāng)你要獲取鎖的時候思犁,通過執(zhí)行下面的sql來嘗試獲取鎖:insert into LOCK(METHODNAME,DESCRIPTION) values (‘getLock’,‘獲取鎖’) ;來獲取鎖。

這條sql執(zhí)行的結(jié)果有兩種成功和失敗进肯,成功說明該方法還沒有被某個線程所持有激蹲,失敗則表明數(shù)據(jù)庫中已經(jīng)存在該條數(shù)據(jù),該方法的鎖已經(jīng)被某個線程所持有江掩。

當(dāng)你需要釋放鎖的時候学辱,可以通過執(zhí)行這條sql:delete from LOCK where METHODNAME='getLock';來釋放鎖。

樂觀鎖實(shí)現(xiàn)方式還是存在很多問題的环形,一個是「并發(fā)性能問題」策泣,再者「不可重入」以及「沒有自動失效的功能」「非公平鎖」抬吟,只要當(dāng)前的庫表中已經(jīng)存在該信息,執(zhí)行插入就會失敗火本。

其實(shí)危队,對于上面的問題基于數(shù)據(jù)庫也可以解決盅弛,比如:不可重復(fù),你可以「增加字段保存當(dāng)前線程的信息以及可重復(fù)的次數(shù)」,只要是再判斷是當(dāng)前線程,可重復(fù)的次數(shù)就會+1,每次執(zhí)行釋放鎖就會-1,直到為0。

「沒有失效的功能,可以增加一個字段存儲最后的失效時間」,根據(jù)這個字段判斷當(dāng)前時間是否大于存儲的失效時間蔬浙,若是大于則表明垢夹,該方法的索索已經(jīng)可以被釋放。

「非公平鎖可以增加一個中間表的形式血公,作為一個排隊(duì)隊(duì)列」命辖,競爭的線程都會按照時間存儲于這個中間表,當(dāng)要某個線程嘗試獲取某個方法的鎖的時候分蓖,檢查中間表中是否已經(jīng)存在等待的隊(duì)列尔艇。

每次都只要獲取中間表中最小的時間的鎖,也實(shí)現(xiàn)公平的排隊(duì)等候的效果咆疗,所有的問題總是有解決的思路漓帚。

上面就是兩種基于數(shù)據(jù)庫實(shí)現(xiàn)分布式鎖的方式,但是午磁,數(shù)據(jù)庫實(shí)現(xiàn)分布式鎖的方式只作為學(xué)習(xí)的例子尝抖,實(shí)際中不會使用它作為實(shí)現(xiàn)分布式鎖毡们,重要的是學(xué)習(xí)解決問題的思路和思想。

Redis實(shí)現(xiàn)的分布式鎖

之前講了一篇Redis事務(wù)的文章昧辽,很多讀者Redis事務(wù)有啥用衙熔,主要是因?yàn)镽edis的事務(wù)并沒有Mysql的事務(wù)那么強(qiáng)大,所以一般的公司一般確實(shí)是用不到搅荞。

這里就來說一說Redis事務(wù)的一個實(shí)際用途红氯,它可以用來實(shí)現(xiàn)一個簡單的秒殺系統(tǒng)的庫存扣減,下面我們就來進(jìn)行代碼的實(shí)現(xiàn)咕痛。

(1)首先使用線程池初始化5000個客戶端痢甘。

public static void intitClients() {
 ExecutorService threadPool= Executors.newCachedThreadPool();
 for (int i = 0; i < 5000; i++) {
  threadPool.execute(new Client(i));
 }
 threadPool.shutdown();

 while(true){ 
         if(threadPool.isTerminated()){  
             break;  
         }  
     }  
}

(2)接著初始化商品的庫存數(shù)為1000。

public static void initPrductNum() {
  Jedis jedis = RedisUtil.getInstance().getJedis();
  jedisUtils.set("produce", "1000");// 初始化商品庫存數(shù)
  RedisUtil.returnResource(jedis);// 返還數(shù)據(jù)庫連接
 }
}

(3)最后是庫存扣減的每條線程的處理邏輯茉贡。

/**
 * 顧客線程
 * 
 *
 */
class client implements Runnable {
 Jedis jedis = null;
 String key = "produce"; // 商品數(shù)量的主鍵
 String name;

 public ClientThread(int num) {
  name= "編號=" + num;
 }

 public void run() {

  while (true) {
   jedis = RedisUtil.getInstance().getJedis();
   try {
    jedis.watch(key);
    int num= Integer.parseInt(jedis.get(key));// 當(dāng)前商品個數(shù)
    if (num> 0) {
     Transaction ts= jedis.multi(); // 開始事務(wù)
     ts.set(key, String.valueOf(num - 1)); // 庫存扣減
     List<Object> result = ts.exec(); // 執(zhí)行事務(wù)
     if (result == null || result.isEmpty()) {
      System.out.println("抱歉塞栅,您搶購失敗,請?jiān)俅沃卦?);
     } else {
      System.out.println("恭喜您腔丧,搶購成功");
      break;
     }
    } else {
     System.out.println("抱歉放椰,商品已經(jīng)賣完");
     break;
    }
   } catch (Exception e) {
    e.printStackTrace();
   } finally {
    jedis.unwatch(); // 解除被監(jiān)視的key
    RedisUtil.returnResource(jedis);
   }
  }
 }
}

在代碼的實(shí)現(xiàn)中有一個重要的點(diǎn)就是「商品的數(shù)據(jù)量被watch了」,當(dāng)前的客戶端只要發(fā)現(xiàn)數(shù)量被改變就會搶購失敗愉粤,然后不斷的自旋進(jìn)行搶購砾医。

這個是基于Redis事務(wù)實(shí)現(xiàn)的簡單的秒殺系統(tǒng),Redis事務(wù)中的watch命令有點(diǎn)類似樂觀鎖的機(jī)制衣厘,只要發(fā)現(xiàn)商品數(shù)量被修改如蚜,就執(zhí)行失敗。

Redis實(shí)現(xiàn)分布式鎖的第二種方式头滔,可以使用setnx怖亭、getset、expire坤检、del這四個命令來實(shí)現(xiàn)兴猩。

  1. setnx:命令表示如果key不存在,就會執(zhí)行set命令早歇,若是key已經(jīng)存在倾芝,不會執(zhí)行任何操作。
  2. getset:將key設(shè)置為給定的value值箭跳,并返回原來的舊value值晨另,若是key不存在就會返回返回nil 。
  3. expire:設(shè)置key生存時間谱姓,當(dāng)當(dāng)前時間超出了給定的時間借尿,就會自動刪除key。
  4. del:刪除key,它可以刪除多個key路翻,語法如下:DEL key [key …]狈癞,若是key不存在直接忽略。

下面通過一個代碼案例是實(shí)現(xiàn)以下這個命令的操作方式:

public void redis(Produce produce) {
        long timeout= 10000L; // 超時時間
        Long result= RedisUtil.setnx(produce.getId(), String.valueOf(System.currentTimeMillis() + timeout));
        if (result!= null && result.intValue() == 1) { // 返回1表示成功獲取到鎖
         RedisUtil.expire(produce.getId(), 10);//有效期為5秒茂契,防止死鎖
         //執(zhí)行業(yè)務(wù)操作
         ......
         //執(zhí)行完業(yè)務(wù)后蝶桶,釋放鎖
         RedisUtil.del(produce.getId());
        } else {
           System.println.out("沒有獲取到鎖")
        }
    }

在線程A通過setnx方法嘗試去獲取到produce對象的鎖,若是獲取成功就會返回1掉冶,獲取不成功真竖,說明當(dāng)前對象的鎖已經(jīng)被其它線程鎖持有。

獲取鎖成功后并設(shè)置key的生存時間厌小,能夠有效的防止出現(xiàn)死鎖恢共,最后就是通過del來實(shí)現(xiàn)刪除key,這樣其它的線程就也可以獲取到這個對象的鎖召锈。

執(zhí)行的邏輯很簡單旁振,但是簡單的同時也會出現(xiàn)問題,比如你在執(zhí)行完setnx成功后設(shè)置生存時間不生效涨岁,此時服務(wù)器宕機(jī),那么key就會一直存在Redis中吉嚣。

當(dāng)然解決的辦法梢薪,你可以在服務(wù)器destroy函數(shù)里面再次執(zhí)行:

RedisUtil.del(produce.getId());

或者通過「定時任務(wù)檢查是否有設(shè)置生存時間」,沒有的話都會統(tǒng)一進(jìn)行設(shè)置生存時間尝哆。

還有比較好的解決方案就是秉撇,在上面的執(zhí)行邏輯里面,若是沒有獲取到鎖再次進(jìn)行key的生存時間:

public void redis(Produce produce) {
        long timeout= 10000L; // 超時時間
        Long result= RedisUtil.setnx(produce.getId(), String.valueOf(System.currentTimeMillis() + timeout));
        if (result!= null && result.intValue() == 1) { // 返回1表示成功獲取到鎖
         RedisUtil.expire(produce.getId(), 10);//有效期為10秒秋泄,防止死鎖
         //執(zhí)行業(yè)務(wù)操作
         ......
         //執(zhí)行完業(yè)務(wù)后琐馆,釋放鎖
         RedisUtil.del(produce.getId());
        } else {
            String value= RedisUtil.get(produce.getId());
            // 存在該key,并且已經(jīng)超時
            if (value!= null && System.currentTimeMillis() > Long.parseLong(value)) {
                String result = RedisUtil.getSet(produce.getId(), String.valueOf(System.currentTimeMillis() + timeout)); 
                if (result == null || (result != null && StringUtils.equals(value, result))) {
                     RedisUtil.expire(produce.getId(), 10);//有效期為10秒恒序,防止死鎖
           //執(zhí)行業(yè)務(wù)操作
           ......
           //執(zhí)行完業(yè)務(wù)后瘦麸,釋放鎖
           RedisUtil.del(produce.getId());
                } else {
                    System.println("沒有獲取到鎖")
                }
            } else {
                System.println("沒有獲取到鎖")
            }
        }
    }

這里對上面的代碼進(jìn)行了改進(jìn),在獲取setnx失敗的時候歧胁,再次重新判斷該key的鎖時間是否失效或者不存在滋饲,并重新設(shè)置生存的時間,避免出現(xiàn)死鎖的情況喊巍。

第三種Redis實(shí)現(xiàn)分布式鎖屠缭,可以使用Redisson來實(shí)現(xiàn),它的實(shí)現(xiàn)簡單崭参,已經(jīng)幫我們封裝好了呵曹,屏蔽了底層復(fù)雜的實(shí)現(xiàn)邏輯。

先來一個Redisson的原理圖,后面會對這個原理圖進(jìn)行詳細(xì)的介紹:

image

我們在實(shí)際的項(xiàng)目中要使用它奄喂,只需要引入它的依賴铐殃,然后執(zhí)行下面的代碼:

RLock lock = redisson.getLock("lockName");
lock.locl();
lock.unlock();

并且它還支持「Redis單實(shí)例、Redis哨兵砍聊、redis cluster背稼、redis master-slave」等各種部署架構(gòu),都給你完美的實(shí)現(xiàn)玻蝌,不用自己再次擰螺絲蟹肘。

但是,crud的同時還是要學(xué)習(xí)一下它的底層的實(shí)現(xiàn)原理俯树,下面我們來了解下一下帘腹,對于一個分布式的鎖的框架主要的學(xué)習(xí)分為下面的5個點(diǎn):

  1. 加鎖機(jī)制
  2. 解鎖機(jī)制
  3. 生存時間延長機(jī)制
  4. 可重入加鎖機(jī)制
  5. 鎖釋放機(jī)制

只要掌握一個框架的這五個大點(diǎn),基本這個框架的核心思想就已經(jīng)掌握了许饿,若是要你去實(shí)現(xiàn)一個鎖機(jī)制框架阳欲,就會有大體的一個思路。

Redisson中的加鎖機(jī)制是通過lua腳本進(jìn)行實(shí)現(xiàn)陋率,Redisson首先會通過「hash算法」球化,選擇redis cluster集群中的一個節(jié)點(diǎn),接著會把一個lua腳本發(fā)送到Redis中瓦糟。

它底層實(shí)現(xiàn)的lua腳本如下:

returncommandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
 "if (redis.call('exists', KEYS[1]) == 0) then " +
       "redis.call('hset', KEYS[1], ARGV[2], 1); " +
       "redis.call('pexpire', KEYS[1], ARGV[1]); " +
       "return nil; " +
   "end; " +
   "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
       "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
       "redis.call('pexpire', KEYS[1], ARGV[1]); " +
       "return nil; " +
   "end; " +
   "return redis.call('pttl', KEYS[1]);",
     Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

「redis.call()的第一個參數(shù)表示要執(zhí)行的命令筒愚,KEYS[1]表示要加鎖的key值,ARGV[1]表示key的生存時間菩浙,默認(rèn)時30秒巢掺,ARGV[2]表示加鎖的客戶端的ID【Ⅱ撸」

比如第一行中redis.call('exists', KEYS[1]) == 0) 表示執(zhí)行exists命令判斷Redis中是否含有KEYS[1]陆淀,這個還是比較好理解的。

lua腳本中封裝了要執(zhí)行的業(yè)務(wù)邏輯代碼先嬉,它能夠保證執(zhí)行業(yè)務(wù)代碼的原子性轧苫,它通過hset lockName命令完成加鎖。

若是第一個客戶端已經(jīng)通過hset命令成功加鎖坝初,當(dāng)?shù)诙€客戶端繼續(xù)執(zhí)行l(wèi)ua腳本時浸剩,會發(fā)現(xiàn)鎖已經(jīng)被占用,就會通過pttl myLock返回第一個客戶端的持鎖生存時間鳄袍。

若是還有生存時間绢要,表示第一個客戶端會繼續(xù)持有鎖,那么第二個客戶端就會不停的自旋嘗試去獲取鎖拗小。

假如第一個客戶端持有鎖的時間快到期了重罪,想繼續(xù)持有鎖,可以給它啟動一個watch dog看門狗,他是一個后臺線程會每隔10秒檢查一次剿配,可以不斷的延長持有鎖的時間搅幅。

Redisson中可重入鎖的實(shí)現(xiàn)是通過incrby lockName來實(shí)現(xiàn),「重入一個計(jì)數(shù)就會+1呼胚,釋放一次鎖計(jì)數(shù)就會-1」茄唐。

最后,使用完鎖后執(zhí)行del lockName就可以直接「釋放鎖」蝇更,這樣其它的客戶端就可以爭搶到該鎖了沪编。

這就是分布式鎖的開源Redisson框架底層鎖機(jī)制的實(shí)現(xiàn)原理,我們可以在生產(chǎn)中實(shí)現(xiàn)該框架實(shí)現(xiàn)分布式鎖的高效使用年扩。

下面通過一個多窗口搶票的例子代碼來實(shí)現(xiàn):

public class SellTicket implements Runnable {
    private int ticketNum = 1000;
    RLock lock = getLock();
    // 獲取鎖 
    private RLock getLock() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        Redisson redisson = (Redisson) Redisson.create(config);
        RLock lock = redisson.getLock("keyName");
        return lock;
    }

    @Override
    public void run() {
        while (ticketNum>0) {
            // 獲取鎖,并設(shè)置超時時間
            lock.lock(1, TimeUnit.MINUTES);
            try {
                if (ticketNum> 0) {
                    System.out.println(Thread.currentThread().getName() + "出售第 " + ticketNum-- + " 張票");
                }
            } finally {
                lock.unlock(); // 釋放鎖
            }
        }
    }
}

測試的代碼如下:

public class Test {
    public static void main(String[] args) {
        SellTicket sellTick= new SellTicket();
        // 開啟5五條線程蚁廓,模擬5個窗口
        for (int i=1; i<=5; i++) {
            new Thread(sellTick, "窗口" + i).start();
        }
    }
}

是不是感覺很簡單,因?yàn)槎嗑€程競爭共享資源的復(fù)雜的過程它在底層都幫你實(shí)現(xiàn)了厨幻,屏蔽了這些復(fù)雜的過程相嵌,而你也就成為了優(yōu)秀的API調(diào)用者。

上面就是Redis三種方式實(shí)現(xiàn)分布式鎖的方式况脆,基于Redis的實(shí)現(xiàn)方式基本都會選擇Redisson的方式進(jìn)行實(shí)現(xiàn)饭宾,因?yàn)楹唵蚊睿挥米约簲Q螺絲格了,開箱即用捏雌。

ZK實(shí)現(xiàn)的分布式鎖

ZK實(shí)現(xiàn)的分布式鎖的原理是基于一個「臨時順序節(jié)點(diǎn)」實(shí)現(xiàn)的,開始的時候笆搓,首先會在ZK中創(chuàng)建一個ParentLock持久化節(jié)點(diǎn)。

image

當(dāng)有client1請求鎖的時候纬傲,满败,就會在ParentLock下創(chuàng)建一個臨時順序節(jié)點(diǎn),如下圖所示:

image

并且叹括,該節(jié)點(diǎn)是有序的算墨,在ZK的內(nèi)部會自動維護(hù)一個節(jié)點(diǎn)的序號,比如:第一個進(jìn)來的創(chuàng)建的臨時順序節(jié)點(diǎn)叫做xxx-000001汁雷,那么第二個就叫做xxx-000002净嘀,這里的序號是一次遞增的。

當(dāng)client1創(chuàng)建完臨時順序節(jié)點(diǎn)后侠讯,就會檢查ParentLock下面的所有的子節(jié)點(diǎn)挖藏,會判斷自己前面是否還有節(jié)點(diǎn),此時明顯是沒有的厢漩,所以獲取鎖成功膜眠。

image

當(dāng)?shù)诙€客戶端client2進(jìn)來獲取鎖的時候,也會執(zhí)行相同的邏輯,會先在創(chuàng)建一個臨時的順序節(jié)點(diǎn)宵膨,并且序號是排在第一個節(jié)點(diǎn)的后面:

image

并且第二部也會判斷ParnetLock下面的所有的子節(jié)點(diǎn)架谎,看自己是否是第一個,明顯不是辟躏,此時就會加鎖失敗谷扣。

那么此時client2會創(chuàng)建一個對client1的lock1的監(jiān)聽(Watcher),用于監(jiān)聽lock1是否存在捎琐,同時client2會進(jìn)入等待狀態(tài):

image

當(dāng)client1執(zhí)行完自己的業(yè)務(wù)邏輯之后会涎,就會刪除鎖,刪除鎖很簡單野哭,就是把這個lock1給刪除掉:

image

此時就會通知client2:監(jiān)聽的lock1已經(jīng)被刪除在塔,鎖被釋放,此時client2創(chuàng)建的lock2也就變成了第一個節(jié)點(diǎn)拨黔,嘗試獲取所得時候就會獲取鎖成功蛔溃。

image

這就是ZK分布式鎖的底層實(shí)現(xiàn)原理,內(nèi)容還是挺多的篱蝇,畢竟分布式鎖要求有一定并發(fā)度才會用到贺待,對于一般的用戶群體不大的根本就不會涉及到,所以第一次接觸的肯定也是需要時間吸收的零截。

總結(jié)

三種方案的比較麸塞,從不同的角度看這三種實(shí)現(xiàn)方式,比較的結(jié)果也不一樣:

  1. 性能:緩存 > Zookeeper >= 數(shù)據(jù)庫涧衙。
  2. 可靠性:Zookeeper > 緩存 > 數(shù)據(jù)庫
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哪工,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子弧哎,更是在濱河造成了極大的恐慌雁比,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件撤嫩,死亡現(xiàn)場離奇詭異偎捎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)序攘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門茴她,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人程奠,你說我怎么就攤上這事丈牢。” “怎么了梦染?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵赡麦,是天一觀的道長朴皆。 經(jīng)常有香客問我,道長泛粹,這世上最難降的妖魔是什么遂铡? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮晶姊,結(jié)果婚禮上扒接,老公的妹妹穿的比我還像新娘。我一直安慰自己们衙,他們只是感情好钾怔,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蒙挑,像睡著了一般宗侦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上忆蚀,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天矾利,我揣著相機(jī)與錄音,去河邊找鬼馋袜。 笑死男旗,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的欣鳖。 我是一名探鬼主播察皇,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼泽台!你這毒婦竟也來了什荣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤怀酷,失蹤者是張志新(化名)和其女友劉穎溃睹,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胰坟,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年泞辐,在試婚紗的時候發(fā)現(xiàn)自己被綠了笔横。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡咐吼,死狀恐怖吹缔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锯茄,我是刑警寧澤厢塘,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布茶没,位于F島的核電站,受9級特大地震影響晚碾,放射性物質(zhì)發(fā)生泄漏抓半。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一格嘁、第九天 我趴在偏房一處隱蔽的房頂上張望笛求。 院中可真熱鬧,春花似錦糕簿、人聲如沸探入。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蜂嗽。三九已至,卻和暖如春殃恒,著一層夾襖步出監(jiān)牢的瞬間植旧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工芋类, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留隆嗅,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓侯繁,卻偏偏與公主長得像胖喳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子贮竟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355