構建微服務之分布式鎖

測試題

  • 為什么要用分布式鎖?分布式鎖的特點有哪些?
  • 用數據庫怎么實現分布式鎖?對于mysql的innodb能實現哪些鎖?
  • 用redis怎么實現分布式鎖?如何避免死鎖?如何解決a進程還沒執行完,鎖提前釋放,b進程就開始執行?a進程會釋放掉b進程的鎖,怎么解決?
  • 怎么用zookeeper實現分布式鎖?
  • redisson特點是什么?redisson的加鎖機制,watchdog自動延遲機制,可重入加載機制是什么?它獲取鎖的流程,加鎖的流程,釋放鎖的流程分別是什么?
  • redis的做分布式鎖的缺點?

前言

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

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

1.在分布式集群中,也就是不同的JVM中,相互有沖突的方法,可以是不同JVM相同實例內的同一個方法,也可以是不同方法,也就是不同業務間的隔離和同一個業務操作不能并行運行,而分布式鎖需要保證這兩個方法在同一時間只能有一個運行.
2.這把鎖最好是可重入的,因為不可重入的鎖很容易出現死鎖
3.獲取鎖和釋放鎖的性能要很高
4.支持獲取鎖的時候可以阻塞等待,以及等待時間
5.獲取鎖后支持設置一個期限,超過這個期限可以自動釋放,防止程序沒有自己釋放的情況
6.這是一把輕量鎖,對業務侵入小
7.易用

數據庫實現分布式鎖

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

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

redis的分布式鎖

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

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

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

1.進程A獲取到鎖,超時時間為1分鐘
2.1分鐘時間到,進程A還沒有處理完,鎖自動釋放了
3.進程B獲取到鎖,開始進行業務處理
4.進程A處理結束,釋放鎖,這個時候將進程B獲取到的鎖釋放了
5.進程C獲取到鎖,開始業務處理,進程B還沒有處理結束,結果B和C開始并行處理,發生并發

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

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

 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. 已經被別的線程鎖住,判斷是否失效
                 */
                String oldValue = jedis.get(lockKey);
                if (StringUtils.isBlank(oldValue)) {
                    /*
                     * 2.1 value存的是超時時間,如果為空有2種情況
                     *      1. 異常數據,沒有value 或者 value為空字符
                     *      2. 鎖恰好被別的線程釋放了
                     * 此時需要嘗試重新嘗試,為了避免出現情況1時導致死循環,只重試3次
                     */
                    continue;
                }

                Long oldValueL = Long.valueOf(oldValue);
                if (oldValueL < System.currentTimeMillis()) {
                    /*
                     * 已超時,重新嘗試鎖
                     *
                     * Redis:getSet 操作步驟:
                     *      1.獲取 Key 對應的 Value 作為返回值,不存在時返回null
                     *      2.設置 Key 對應的 Value 為傳入的值
                     * 這里如果返回的 getValue != oldValue 表示已經被其它線程重新修改了
                     */
                    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);
        }
    }

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

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

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

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

    /**
     * 釋放鎖
     *
     * @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值,不過這種實現方式,value的值為過期時間也不唯一

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

zookeeper實現分布式鎖

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

 public void lock() {
        try {

            // 創建臨時子節點
            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"));

            // 取出所有子節點
            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)) {
                // 如果是最小的節點,則表示取得鎖
                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));// 同時注冊監聽。
            // 判斷比自己小一個數的節點是否存在,如果不存在則無需等待鎖,同時注冊監聽
            if (stat != null) {
                System.out.println(j.join(Thread.currentThread().getName(), myNode,
                        " waiting for " + root + "/" + preNode + " released lock"));

                latch.await();// 等待,這里應該一直等待其他線程釋放鎖
                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();
        }
    }

當然如果我們開發環境使用的是etcs也可以用etcd來實現分布式鎖,原理和zookeeper類似

相關鏈接

我猜你還沒明白如何利用好Redis、Redisson使用實現分布式鎖?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容