輕松構(gòu)建微服務(wù)之分布式鎖

前言

在多線程情況下訪問資源,我們需要加鎖來保證業(yè)務(wù)的正常進行,JDK中提供了很多并發(fā)控制相關(guān)的工具包,來保證多線程下可以高效工作,同樣在分布式環(huán)境下,有些互斥操作我們可以借助分布式鎖來實現(xiàn)兩個操作不能同時運行,必須等到另外一個任務(wù)結(jié)束了把鎖釋放了才能獲取鎖然后執(zhí)行,因為跨JVM我們需要一個第三方系統(tǒng)來協(xié)助實現(xiàn)分布式鎖,一般我們可以用
數(shù)據(jù)庫,redis,zookeeper,etcd等來實現(xiàn).

要實現(xiàn)一把分布式鎖,我們需要先分析下這把鎖有哪些特性

  • 1.在分布式集群中,也就是不同的JVM中,相互有沖突的方法,可以是不同JVM相同實例內(nèi)的同一個方法,也可以是不同方法,也就是不同業(yè)務(wù)間的隔離和同一個業(yè)務(wù)操作不能并行運行,而分布式鎖需要保證這兩個方法在同一時間只能有一個運行.

  • 2.這把鎖最好是可重入的,因為不可重入的鎖很容易出現(xiàn)死鎖

  • 3.獲取鎖和釋放鎖的性能要很高

  • 4.支持獲取鎖的時候可以阻塞等待,以及等待時間

  • 5.獲取鎖后支持設(shè)置一個期限,超過這個期限可以自動釋放,防止程序沒有自己釋放的情況

  • 6.這是一把輕量鎖,對業(yè)務(wù)侵入小

  • 7.易用

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

由于數(shù)據(jù)庫的鎖無能是在性能高可用上都不及其他方式,這里我們簡單介紹下可能的方案

  • 1.獲取鎖的時候,往數(shù)據(jù)庫里插入一條記錄,可以根據(jù)方法名作唯一鍵約束,其他線程獲取鎖的時候無法插入所以會等待,釋放鎖的時候刪除,這種方式不支持可重入
  • 2.根據(jù)數(shù)據(jù)庫的排他鎖 for update實現(xiàn),當(dāng)commit的時候釋放,這種方式如果鎖不釋放就會一直占有一個connection,而且加鎖導(dǎo)致性能低
  • 3.將每一個鎖作為表里的一條記錄,這個記錄加一個狀態(tài),每次獲取鎖的時候都update status = 1 where status = -1,這種類似CAS的方式可以解決排他鎖性能低.但是mysql是一個單點,而且和業(yè)務(wù)系統(tǒng)關(guān)聯(lián),因為兩個業(yè)務(wù)方可能屬于不同系統(tǒng)不同數(shù)據(jù)庫,如果做到不和業(yè)務(wù)關(guān)聯(lián)還需要增加一次RPC請求,將鎖業(yè)務(wù)抽為一個單獨系統(tǒng),不夠輕量

redis的分布式鎖

SET resource_name my_random_value NX PX 30000
  • SET NX 只會在key不存在的時候給key賦值,當(dāng)多個進程同時爭搶鎖資源的時候,會下發(fā)多個SET NX只會有一個返回成功,并且SET NX對外是一個原子操作
  • PX 設(shè)置過期時間,代表這個key的存活時間,也就是獲取到的鎖只會占有這么長,超過這個時間將會自動釋放
  • my_random_value 一般是全局唯一值,這個隨機數(shù)一般可以用時間戳加隨機數(shù),這種方式在多機器實例上可能不唯一,如果需要保證絕對唯一可以采用UUID,但是性能會有影響,這個值的用途會在鎖釋放的時候用到

我們可以看看下面獲取分布式鎖的使用場景,假設(shè)我們釋放鎖,直接del這個key

if (!redisComponent.acquireLock(lockKey) {
    LOGGER.warn(">>分布式并發(fā)鎖獲取失敗");
    return ;
}

try {
      // do  business  ...
} catch (BusinessException e) {
      // exception handler  ...
} finally {
  redisComponent.releaseLock(lockKey);
}

  • 1.進程A獲取到鎖,超時時間為1分鐘
  • 2.1分鐘時間到,進程A還沒有處理完,鎖自動釋放了
  • 3.進程B獲取到鎖,開始進行業(yè)務(wù)處理
  • 4.進程A處理結(jié)束,釋放鎖,這個時候?qū)⑦M程B獲取到的鎖釋放了
  • 5.進程C獲取到鎖,開始業(yè)務(wù)處理,進程B還沒有處理結(jié)束,結(jié)果B和C開始并行處理,發(fā)生并發(fā)

為了解決以上問題,我們可以在釋放鎖的時候,判斷下鎖是否存在,這樣進程A在釋放鎖的時候就不會將進程B加的鎖釋放了,
或者通過以下方式,將過期時間做為value存儲在對應(yīng)的key中,釋放鎖的時候,判斷當(dāng)前時間是否小于過期時間,只有小于當(dāng)前時間才處理,我們也可以在進行del操作的時候判斷下對應(yīng)的value是否相等,這個時候就需要在del操作的時候傳人
my_random_value

下面我們看下redis實現(xiàn)分布式鎖java代碼實現(xiàn),我們采用在del的時候判斷下當(dāng)前時間是否小于過期時間

 public boolean acquireLock(String lockKey, long expired) {

        ShardedJedis jedis = null;

        try {

            jedis = pool.getResource();
            String value = String.valueOf(System.currentTimeMillis() + expired + 1);
            int tryTimes = 0;

            while (tryTimes++ < 3) {

                /*
                 *  1. 嘗試鎖
                 *  setnx : set if not exist
                 */
                if (jedis.setnx(lockKey, value).equals(1L)) {
                    return true;
                }

                /*
                 * 2. 已經(jīng)被別的線程鎖住,判斷是否失效
                 */
                String oldValue = jedis.get(lockKey);
                if (StringUtils.isBlank(oldValue)) {
                    /*
                     * 2.1 value存的是超時時間努咐,如果為空有2種情況
                     *      1. 異常數(shù)據(jù)坤检,沒有value 或者 value為空字符
                     *      2. 鎖恰好被別的線程釋放了
                     * 此時需要嘗試重新嘗試空扎,為了避免出現(xiàn)情況1時導(dǎo)致死循環(huán)叫挟,只重試3次
                     */
                    continue;
                }

                Long oldValueL = Long.valueOf(oldValue);
                if (oldValueL < System.currentTimeMillis()) {
                    /*
                     * 已超時双炕,重新嘗試鎖
                     *
                     * Redis:getSet 操作步驟:
                     *      1.獲取 Key 對應(yīng)的 Value 作為返回值踱蠢,不存在時返回null
                     *      2.設(shè)置 Key 對應(yīng)的 Value 為傳入的值
                     * 這里如果返回的 getValue != oldValue 表示已經(jīng)被其它線程重新修改了
                     */
                    String getValue = jedis.getSet(lockKey, value);
                    return oldValue.equals(getValue);
                } else {
                    // 未超時袖肥,則直接返回失敗
                    return false;
                }
            }

            return false;

        } catch (Throwable e) {
            logger.error("acquireLock error", e);
            return false;

        } finally {
            returnResource(jedis);
        }
    }


    /**
     * 釋放鎖
     *
     * @param lockKey
     *            key
     */
    public void releaseLock(String lockKey) {
        ShardedJedis jedis = null;
        try {
            jedis = pool.getResource();
            long current = System.currentTimeMillis();
            // 避免刪除非自己獲取到的鎖
            String value = jedis.get(lockKey);
            if (StringUtils.isNotBlank(value) && current < Long.valueOf(value)) {
                jedis.del(lockKey);
            }
        } catch (Throwable e) {
            logger.error("releaseLock error", e);
        } finally {
            returnResource(jedis);
        }
    }

這種方式?jīng)]有用到剛剛說的my_random_value,我們看下如果我們按以下代碼獲取鎖會有什么問題

if (!redisComponent.acquireLock(lockKey) {
    LOGGER.warn(">>分布式并發(fā)鎖獲取失敗");
    return ;
}

try {
boolean locked = redisComponent.acquireLock(lockKey);
if(locked)
      // do  business  ...
} catch (BusinessException e) {
      // exception handler  ...
} finally {
  redisComponent.releaseLock(lockKey);
}

同樣這種方式當(dāng)進程A沒有獲取到鎖,之后進程B獲取到鎖,進程A會釋放進程B的鎖,這個時候我們可以借助my_random_value來實現(xiàn)

    /**
     * 釋放鎖
     *
     * @param lockKey ,value
     */
    public void releaseLock(String lockKey, long oldvalue) {
        ShardedJedis jedis = null;
        try {
            jedis = pool.getResource();
            String value = jedis.get(lockKey);
            if (StringUtils.isNotBlank(value) && oldvalue == Long.valueOf(value)) {
                jedis.del(lockKey);
            }
        } catch (Throwable e) {
            logger.error("releaseLock error", e);
        } finally {
            returnResource(jedis);
        }
    }

這種方式需要保存之前獲取鎖時候的value值,并在釋放鎖的帶上value值,不過這種實現(xiàn)方式,value的值為過期時間也不唯一

由于我們用了redis得超時機制來釋放鎖,那么當(dāng)進程在鎖租約到期后還沒有執(zhí)行結(jié)束,那么其他進程獲取到鎖后則會產(chǎn)生并發(fā)寫的情況,這種如果業(yè)務(wù)上需要精確控制,只能用樂觀鎖來控制了,每次寫入數(shù)據(jù)都帶一個鎖的版本,如果下次獲取鎖的時候版本加1,這樣上面那種情況,鎖到期釋放了新的進程獲取到鎖后會使用新的版本號,之前的進程鎖已經(jīng)釋放了如果繼續(xù)使用該鎖則會發(fā)現(xiàn)版本已經(jīng)不對了

zookeeper實現(xiàn)分布式鎖

可以借助zookeeper的順序節(jié)點,在一個父節(jié)點下,所有需要爭搶鎖的資源都去這個目錄下創(chuàng)建一個順序節(jié)點,然后判斷這個臨時順序節(jié)點是否是兄弟節(jié)點中順序最小的,如果是最小的則獲取到鎖,如果不是則監(jiān)聽這個順序最小的節(jié)點的刪除事件,然后在繼續(xù)根據(jù)這個流程獲取最小節(jié)點

 public void lock() {
        try {

            // 創(chuàng)建臨時子節(jié)點
            String myNode = zk.create(root + "/" + lockName , data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            System.out.println(j.join(Thread.currentThread().getName() + myNode, "created"));

            // 取出所有子節(jié)點
            List<String> subNodes = zk.getChildren(root, false);
            TreeSet<String> sortedNodes = new TreeSet<>();
            for(String node :subNodes) {
                sortedNodes.add(root +"/" +node);
            }

            String smallNode = sortedNodes.first();
            String preNode = sortedNodes.lower(myNode);

            if (myNode.equals( smallNode)) {
                // 如果是最小的節(jié)點,則表示取得鎖
                System.out.println(j.join(Thread.currentThread().getName(), myNode, "get lock"));
                this.nodeId.set(myNode);
                return;
            }

            CountDownLatch latch = new CountDownLatch(1);
            Stat stat = zk.exists(preNode, new LockWatcher(latch));// 同時注冊監(jiān)聽咪辱。
            // 判斷比自己小一個數(shù)的節(jié)點是否存在,如果不存在則無需等待鎖,同時注冊監(jiān)聽
            if (stat != null) {
                System.out.println(j.join(Thread.currentThread().getName(), myNode,
                        " waiting for " + root + "/" + preNode + " released lock"));

                latch.await();// 等待,這里應(yīng)該一直等待其他線程釋放鎖
                nodeId.set(myNode);
                latch = null;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    public void unlock() {
        try {
            System.out.println(j.join(Thread.currentThread().getName(), nodeId.get(), "unlock "));
            if (null != nodeId) {
                zk.delete(nodeId.get(), -1);
            }
            nodeId.remove();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

當(dāng)然如果我們開發(fā)環(huán)境使用的是etcs也可以用etcd來實現(xiàn)分布式鎖,原理和zookeeper類似

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末椎组,一起剝皮案震驚了整個濱河市油狂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌寸癌,老刑警劉巖专筷,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蒸苇,居然都是意外死亡磷蛹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門溪烤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來味咳,“玉大人,你說我怎么就攤上這事檬嘀〔凼唬” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵鸳兽,是天一觀的道長掂铐。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么堡纬? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任聂受,我火速辦了婚禮,結(jié)果婚禮上烤镐,老公的妹妹穿的比我還像新娘蛋济。我一直安慰自己,他們只是感情好炮叶,可當(dāng)我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布碗旅。 她就那樣靜靜地躺著,像睡著了一般镜悉。 火紅的嫁衣襯著肌膚如雪祟辟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天侣肄,我揣著相機與錄音旧困,去河邊找鬼。 笑死稼锅,一個胖子當(dāng)著我的面吹牛吼具,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播矩距,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拗盒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了锥债?” 一聲冷哼從身側(cè)響起陡蝇,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哮肚,沒想到半個月后登夫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡允趟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年悼嫉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拼窥。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡戏蔑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鲁纠,到底是詐尸還是另有隱情总棵,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布改含,位于F島的核電站情龄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜骤视,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一鞍爱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧专酗,春花似錦睹逃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至佑笋,卻和暖如春翼闹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蒋纬。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工猎荠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蜀备。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓关摇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親琼掠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 什么是鎖停撞?在單進程的系統(tǒng)中瓷蛙,當(dāng)存在多個線程可以同時改變某個變量(可變共享變量)時,就需要對變量或代碼塊做同步戈毒,使其...
    康康不遛貓閱讀 1,035評論 0 5
  • 最近碰到幾個業(yè)務(wù)場景艰猬,會遇到并發(fā)的問題。在單實例情況下埋市,我們會通過java.util.concurrent包...
    菜鳥小玄閱讀 2,255評論 0 5
  • 最近看了極客時間左耳聽風(fēng)的專欄冠桃,對于分布式系統(tǒng)的設(shè)計有了更深的認(rèn)識,準(zhǔn)備結(jié)合陳皓的總結(jié)加上自己看過的資料對于分布式...
    仰泳的雙魚閱讀 3,681評論 0 23
  • ZooKeeper是一個分布式的道宅,開放源碼的分布式應(yīng)用程序協(xié)調(diào)服務(wù)食听,是Google的Chubby一個開源的實現(xiàn),是...
    Java架構(gòu)007閱讀 2,308評論 0 4
  • 最近發(fā)生的一件事讓我倍受觸動污茵,使我第一次聯(lián)想到自己老去的那天樱报。 現(xiàn)在的我們年輕,所以意氣風(fēng)發(fā)泞当,瀟灑自在迹蛤,但我們生而...
    檬樂閱讀 730評論 5 12