這幾種常見的“分布式鎖”寫法弊予,搞懂再也不怕面試官祥楣,安排

什么是分布式鎖?

今天跟大家聊一聊分布式鎖汉柒。

首先說下什么是分布式鎖误褪,當我們在進行下訂單減庫存,搶票碾褂,選課兽间,搶紅包這些業(yè)務場景時,如果在此處沒有鎖的控制斋扰,會導致很嚴重的問題渡八。

學過多線程的小伙們知道啃洋,為了防止多個線程同時執(zhí)行同一段代碼,我們可以用 synchronized 關鍵字或 JUC 里面的 ReentrantLock 類來控制屎鳍,但是目前幾乎任何一個系統(tǒng)都是部署多臺機器的宏娄,單機部署的應用很少,synchronized 和 ReentrantLock 發(fā)揮不出任何作用逮壁,此時就需要一把全局的鎖孵坚,來代替 JAVA 中的 synchronized 和 ReentrantLock。

分布式鎖的實現方式流行的主要有三種窥淆,分別是基于緩存 Redis 的實現方式卖宠,基于 zk 臨時順序節(jié)點的實現以及基于數據庫行鎖的實現。我們先來說下用 Jedis 中的 setnx 命令來構建這把鎖忧饭。

Jedis寫法

使用 Redis 做分布式鎖的思路是扛伍,在 redis 中設置一個值表示加了鎖,然后釋放鎖的時候就把這個 key 刪除词裤。思路是很簡單刺洒,但是在使用過程中要避免一些坑,我們先看下加鎖的代碼:

    /**
     * 嘗試獲取分布式鎖
     *
     * @param jedis      Redis客戶端
     * @param lockKey    鎖
     * @param requestId  請求標識
     * @param expireTime 超期時間
     * @return 是否獲取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        // set支持多個參數 NX(not exist) XX(exist) EX(seconds) PX(million seconds)
        String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

這段代碼很簡單吼砂,主要說下這里用的命令是 SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]逆航,而沒有使用 SETNX+EXPIRE 的命令,原因是 SETNX+EXPIRE 是兩條命令無法保證原子性渔肩,而 SET 是原子操作因俐。

那這里為什么要設置超時時間呢?

原因是當一個客戶端獲得了鎖在執(zhí)行任務的過程中掛掉了周偎,來不及顯式地釋放鎖抹剩,這塊資源將會永遠被鎖住,這將會導致死鎖栏饮,所以必須設置一個超時時間吧兔。

釋放鎖的代碼如下:

    /**
     * 釋放分布式鎖
     *
     * @param jedis     Redis客戶端
     * @param lockKey   鎖
     * @param requestId 請求標識,當前工作線程線程的名稱
     * @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));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

這里也有兩點注意的地方,第一是解鈴還須系鈴人袍嬉,怎么理解呢境蔼,就是 A 加的鎖 B 不能去 del 掉吧,不然豈不是全亂套了伺通,誰加的鎖就誰去解箍土,我們一般把 value 設為當前線程的 Id,Thread.currentThread().getId()罐监,然后在刪的時候判斷下是不是當前線程吴藻。

第二點是驗證和釋放鎖是兩個獨立操作,不是原子性弓柱,這個怎么解決呢沟堡?使用 Lua 腳本侧但,即 if redis.call('get', KEYS[1]) == ARGV[1] then returnredis.call('del', KEYS[1]) else return 0 end,它能給我們保證原子性航罗。

Redisson寫法

Redisson 是 Java 的 Redis 客戶端之一禀横,提供了一些 API 方便操作 Redis。但是 Redisson 這個客戶端可有點厲害粥血,我們先打開官網看下https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

file

這個目錄里面有很多的功能柏锄,Redisson 跟 Jedis 定位不同,它不是一個單純的 Redis 客戶端复亏,而是基于 Redis 實現的分布式的服務趾娃,我們可以看到還有 JUC 包下面的類名,Redisson 幫我們搞了分布式的版本缔御,比如 AtomicLong抬闷,直接用 RedissonAtomicLong 就行了。

鎖只是它的冰山一角耕突,并且它對主從饶氏,哨兵,集群等模式都支持有勾,當然了,單節(jié)點模式肯定是支持的古程。

在 Redisson 里面提供了更加簡單的分布式鎖的實現蔼卡,我們來看下它的用法,相當的簡單挣磨,兩行代碼搞定雇逞,比 Jedis 要簡單的多,而且在 Jedis 里需要考慮的問題茁裙,它都已經幫我們封裝好了塘砸。

file

我們來看下,這里獲取鎖有很多種的方式晤锥,有公平鎖有讀寫鎖掉蔬,我們使用的是 redissonClient.getLock, 這是一個可重入鎖矾瘾。

file

現在我把程序啟動一下

file

打開 Redis Desktop Manager 工具女轿,看下到底它存的是什么。原來在加鎖的時候壕翩,寫入了一個 HASH 類型的值蛉迹,key 是鎖名稱 jackxu,field 是線程的名稱放妈,而 value 是 1(即表示鎖的重入次數)北救。

file

小伙伴可能覺得我在一派胡言荐操,沒關系,我們點進去看下它的源碼是具體實現的珍策。

file

點進 tryLock() 方法的 tryAcquire() 方法托启,再到->tryAcquireAsync() 再到->tryLockInnerAsync(),終于見到廬山真面目了膛壹,原來它最終也是通過 Lua 腳本來實現的驾中。

file

現在我把這段Lua腳本拉出來分析一下,很簡單模聋。

// KEYS[1] 鎖名稱 updateAccount
// ARGV[1] key 過期時間 10000ms
// ARGV[2] 線程名稱
// 鎖名稱不存在
if (redis.call('exists', KEYS[1]) == 0) then
// 創(chuàng)建一個 hash肩民,key=鎖名稱,field=線程名链方,value=1
redis.call('hset', KEYS[1], ARGV[2], 1);
// 設置 hash 的過期時間
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 鎖名稱存在持痰,判斷是否當前線程持有的鎖
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
// 如果是,value+1祟蚀,代表重入次數+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
// 重新獲得鎖工窍,需要重新設置 Key 的過期時間
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 鎖存在,但是不是當前線程持有前酿,返回過期時間(毫秒)
return redis.call('pttl', KEYS[1]);

unlock() 中的 unlockInnerAsync() 釋放鎖患雏,同樣也是通過 Lua 腳本實現。

// KEYS[1] 鎖的名稱 updateAccount
// KEYS[2] 頻道名稱 redisson_lock__channel:{updateAccount}
// ARGV[1] 釋放鎖的消息 0
// ARGV[2] 鎖釋放時間 10000
// ARGV[3] 線程名稱
// 鎖不存在(過期或者已經釋放了)
if (redis.call('exists', KEYS[1]) == 0) then
// 發(fā)布鎖已經釋放的消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
// 鎖存在罢维,但是不是當前線程加的鎖
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;

// 鎖存在淹仑,是當前線程加的鎖
// 重入次數-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
// -1 后大于 0,說明這個線程持有這把鎖還有其他的任務需要執(zhí)行
if (counter > 0) then
// 重新設置鎖的過期時間
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
// -1 之后等于 0肺孵,現在可以刪除鎖了
redis.call('del', KEYS[1]);
// 刪除之后發(fā)布釋放鎖的消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;

// 其他情況返回 nil
return nil;

看完它的使用后匀借,我們發(fā)現真的使用起來像 JDK 中的 ReentrantLock 一樣絲滑。

RedLock

RedLock 的中文是直譯過來的平窘,就叫紅鎖吓肋。紅鎖并非是一個工具,而是 Redis 官方提出的一種分布式鎖的算法瑰艘。

我們知道如果采用單機部署模式是鬼,會存在單點問題,只要 redis 故障了磅叛,加鎖就不行了屑咳。如果采用 master-slave 模式,加鎖的時候只對一個節(jié)點加鎖弊琴,即便通過 sentinel 做了高可用兆龙,但是如果 master 節(jié)點故障了,發(fā)生主從切換,此時就會有可能出現鎖丟失的問題紫皇。

基于以上的考慮慰安,其實 redis 的作者 Antirez 也考慮到這個問題,他提出了一個 RedLock 的算法聪铺。

我在這里畫了一個圖化焕,圖中這五個實例都是獨自部署的,沒有主從關系铃剔,它們就是5個 master 節(jié)點撒桨。

file

通過以下步驟獲取一把鎖:

  • 獲取當前時間戳,單位是毫秒
  • 輪流嘗試在每個 master 節(jié)點上創(chuàng)建鎖键兜,過期時間設置較短凤类,一般就幾十毫秒
  • 嘗試在大多數節(jié)點上建立一個鎖,比如5個節(jié)點就要求是3個節(jié)點(n / 2 +1)
  • 客戶端計算建立好鎖的時間普气,如果建立鎖的時間小于超時時間谜疤,就算建立成功了
  • 要是鎖建立失敗了,那么就依次刪除這個鎖
  • 只要別人建立了一把分布式鎖现诀,你就得不斷輪詢去嘗試獲取鎖

但是這樣的這種算法還是頗具爭議的夷磕,可能還會存在不少的問題,無法保證加鎖的過程一定正確仔沿。Martin Kleppmann 針對這個算法提出了質疑坐桩,接著 antirez 又回復了 Martin Kleppmann 的質疑。一個是很有資歷的分布式架構師封锉,一個是 Redis 之父撕攒,這個就是著名的關于紅鎖的神仙打架事件。

最后 Redisson 官網上也給出了如何使用紅鎖 redlock烘浦,幾行代碼搞定,依然很絲滑萍鲸,感興趣的小伙伴可以看下闷叉。

file

Zookeeper寫法

在介紹 zookeeper 實現分布式鎖的機制之前,先粗略介紹一下 zk 是什么東西:
zk 是一種提供配置管理脊阴、分布式協同以及命名的中心化服務握侧。

它的模型是這樣的:包含一系列的節(jié)點,叫做znode嘿期,就好像文件系統(tǒng)一樣每個 znode 表示一個目錄品擎,然后 znode 有一些特性,我們可以把它們分為四類:

  • 持久化節(jié)點(zk斷開節(jié)點還在)
  • 持久化順序節(jié)點(如果是第一個創(chuàng)建的子節(jié)點备徐,那么生成的子節(jié)點為/lock/node-0000000000萄传,下一個節(jié)點則為/lock/node-0000000001,依次類推)
  • 臨時節(jié)點(客戶端斷開后節(jié)點就刪除了)
  • 臨時順序節(jié)點

zookeeper分布式鎖恰恰應用了臨時順序節(jié)點蜜猾,下面我們就用圖解的方式來看下是怎么實現的秀菱。

1振诬、獲取鎖

首先,在 Zookeeper 當中創(chuàng)建一個持久節(jié)點 ParentLock衍菱。當第一個客戶端想要獲得鎖時赶么,需要在ParentLock這個節(jié)點下面創(chuàng)建一個臨時順序節(jié)點 Lock1。

file

之后脊串,Client1 查找 ParentLock 下面所有的臨時順序節(jié)點并排序辫呻,判斷自己所創(chuàng)建的節(jié)點 Lock1 是不是順序最靠前的一個。如果是第一個節(jié)點琼锋,則成功獲得鎖放闺。

file

這時候,如果再有一個客戶端 Client2 前來獲取鎖斩例,則在 ParentLock 下再創(chuàng)建一個臨時順序節(jié)點Lock2雄人。

file

Client2 查找 ParentLock 下面所有的臨時順序節(jié)點并排序,判斷自己所創(chuàng)建的節(jié)點 Lock2 是不是順序最靠前的一個念赶,結果發(fā)現節(jié)點 Lock2 并不是最小的础钠。

于是,Client2 向排序僅比它靠前的節(jié)點 Lock1 注冊 Watcher叉谜,用于監(jiān)聽 Lock1 節(jié)點是否存在旗吁。這意味著 Client2 搶鎖失敗,進入了等待狀態(tài)停局。

file

這時候很钓,如果又有一個客戶端 Client3 前來獲取鎖,則在ParentLock下載再創(chuàng)建一個臨時順序節(jié)點Lock3董栽。

file

Client3 查找 ParentLock 下面所有的臨時順序節(jié)點并排序码倦,判斷自己所創(chuàng)建的節(jié)點 Lock3 是不是順序最靠前的一個,結果同樣發(fā)現節(jié)點 Lock3 并不是最小的锭碳。

于是袁稽,Client3 向排序僅比它靠前的節(jié)點 Lock2 注冊 Watcher,用于監(jiān)聽 Lock2 節(jié)點是否存在擒抛。這意味著 Client3 同樣搶鎖失敗推汽,進入了等待狀態(tài)。

file

這樣一來歧沪,Client1 得到了鎖歹撒,Client2 監(jiān)聽了 Lock1,Client3 監(jiān)聽了 Lock2诊胞。這恰恰形成了一個等待隊列暖夭,很像是 Java 當中 ReentrantLock 所依賴的 AQS(AbstractQueuedSynchronizer)。

2、釋放鎖

釋放鎖分為兩種情況:

1)任務完成鳞尔,客戶端顯示釋放

當任務完成時嬉橙,Client1 會顯示調用刪除節(jié)點 Lock1 的指令。

file

2)任務執(zhí)行過程中寥假,客戶端崩潰

獲得鎖的 Client1 在任務執(zhí)行過程中市框,如果 Duang 的一聲崩潰,則會斷開與 Zookeeper 服務端的鏈接糕韧。根據臨時節(jié)點的特性枫振,相關聯的節(jié)點 Lock1 會隨之自動刪除。

file

由于 Client2 一直監(jiān)聽著 Lock1 的存在狀態(tài)萤彩,當 Lock1 節(jié)點被刪除粪滤,Client2 會立刻收到通知。這時候 Client2 會再次查詢 ParentLock 下面的所有節(jié)點雀扶,確認自己創(chuàng)建的節(jié)點 Lock2 是不是目前最小的節(jié)點杖小。如果是最小,則 Client2 順理成章獲得了鎖愚墓。

file

同理予权,如果 Client2 也因為任務完成或者節(jié)點崩潰而刪除了節(jié)點 Lock2,那么 Client3 就會接到通知浪册。

最終扫腺,Client3 成功得到了鎖。

file

3村象、Curator

在 Apache 的開源框架 Apache Curator 中笆环,包含了對 Zookeeper 分布式鎖的實現。

https://github.com/apache/curator/

它的使用方式也很簡單厚者,如下所示:

file

我們看了下依然絲滑躁劣,源碼我就不分析了。

總結

zookeeper 天生設計定位就是分布式協調库菲,強一致性习绢,鎖很健壯。如果獲取不到鎖蝙昙,只需要添加一個監(jiān)聽器就可以了,不用一直輪詢梧却,性能消耗較小奇颠。缺點: 在高請求高并發(fā)下,系統(tǒng)瘋狂的加鎖釋放鎖放航,最后 zk 承受不住這么大的壓力可能會存在宕機的風險烈拒。

在這里簡單的提一下,zk 鎖性能比 redis 低的原因:

zk 中的角色分為 leader,flower荆几,每次寫請求只能請求 leader吓妆,leader 會把寫請求廣播到所有 flower,如果 flower 都成功才會提交給 leader吨铸,其實這里相當于一個 2PC 的過程行拢。在加鎖的時候是一個寫請求,當寫請求很多時诞吱,zk 會有很大的壓力舟奠,最后導致服務器響應很慢。

redis 鎖實現簡單房维,理解邏輯簡單沼瘫,性能好,可以支撐高并發(fā)的獲取咙俩、釋放鎖操作耿戚。

缺點: Redis 容易單點故障,集群部署阿趁,并不是強一致性的膜蛔,鎖的不夠健壯; key 的過期時間設置多少不明確歌焦,只能根據實際情況調整飞几;需要自己不斷去嘗試獲取鎖,比較消耗性能独撇。

最后不管 redis 還是 zookeeper屑墨,它們都應滿足分布式鎖的特性:

  • 具備可重入特性(已經獲得鎖的線程在執(zhí)行的過程中不需要再次獲得鎖)
  • 異常或者超時自動刪除纷铣,避免死鎖
  • 互斥(在分布式環(huán)境下同一時刻只能被單個線程獲嚷咽贰)
  • 分布式環(huán)境下高性能、高可用搜立、容錯機制

作者:jack_xu

鏈接:https://juejin.im/post/5ee227b46fb9a047a86237cf

文源網絡以躯,僅供學習之用,如有侵權請聯系刪除啄踊。

我將面試題和答案都整理成了PDF文檔忧设,還有一套學習資料,涵蓋Java虛擬機颠通、spring框架址晕、Java線程、數據結構顿锰、設計模式等等谨垃,但不僅限于此启搂。

關注公眾號【java圈子】獲取資料,還有優(yōu)質文章每日送達刘陶。

file
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末胳赌,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子匙隔,更是在濱河造成了極大的恐慌疑苫,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件牡直,死亡現場離奇詭異缀匕,居然都是意外死亡,警方通過查閱死者的電腦和手機碰逸,發(fā)現死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門乡小,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人饵史,你說我怎么就攤上這事满钟。” “怎么了胳喷?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵湃番,是天一觀的道長。 經常有香客問我吭露,道長吠撮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任讲竿,我火速辦了婚禮泥兰,結果婚禮上,老公的妹妹穿的比我還像新娘题禀。我一直安慰自己鞋诗,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布迈嘹。 她就那樣靜靜地躺著削彬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪秀仲。 梳的紋絲不亂的頭發(fā)上融痛,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音神僵,去河邊找鬼雁刷。 笑死,一個胖子當著我的面吹牛挑豌,可吹牛的內容都是我干的安券。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼氓英,長吁一口氣:“原來是場噩夢啊……” “哼侯勉!你這毒婦竟也來了?” 一聲冷哼從身側響起铝阐,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤址貌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后徘键,有當地人在樹林里發(fā)現了一具尸體练对,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年吹害,在試婚紗的時候發(fā)現自己被綠了螟凭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡它呀,死狀恐怖螺男,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情纵穿,我是刑警寧澤下隧,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站谓媒,受9級特大地震影響淆院,放射性物質發(fā)生泄漏。R本人自食惡果不足惜句惯,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一土辩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宗弯,春花似錦脯燃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至邓厕,卻和暖如春逝嚎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背详恼。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工补君, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昧互。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓挽铁,卻偏偏與公主長得像伟桅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子叽掘,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344