分布式最佳實踐:分布式鎖

為什么

在傳統(tǒng)的單體服務中,我們經(jīng)常會遇到多線程對于單一資源的搶占導致的線程安全問題以及對數(shù)據(jù)庫數(shù)據(jù)操作的一致性問題,如果是在單體系統(tǒng)中,我們可以很方便的使用編程語言提供的鎖以及數(shù)據(jù)庫事務來解決這些問題。

一旦單體系統(tǒng)轉(zhuǎn)為分布式架構(gòu),那么本地事務和線程鎖就無法滿足跨進程的鎖效果;分布式鎖則是用于進程間同步訪問共享資源的一種方式,通過全局共享來實現(xiàn)全局鎖的效果,保證數(shù)據(jù)的一致性。

總的來說,在分布式系統(tǒng)中,當我們期望一個操作(一個請求、一個方法、一個數(shù)據(jù)庫操作...)在整個系統(tǒng)中同一時間只能有一個線程執(zhí)行,那我們就需要用到分布式鎖; 抽象來看就是兩個場景:

  • 單一資源的數(shù)據(jù)變更:比如對共享存儲數(shù)據(jù)(數(shù)據(jù)庫、緩存...)進行修改,多線程的互斥
  • access token:對于多個資源的原子性操作,期望整個業(yè)務邏輯就是單一線程執(zhí)行保持一致性,在入口處就鎖住

分布式鎖應該具備的特性:

  • 原子性:在分布式系統(tǒng)中,一個方法在同一時間只能被一個線程執(zhí)行
  • 阻塞性:在沒獲取到鎖時可以進行阻塞也可以返回失敗
  • 高可用:能夠正確的獲取鎖和釋放鎖,且具備鎖失效的能力
  • 高性能:獲取鎖與釋放鎖的性能保障
  • 可重入:能夠具備可重入特性

實現(xiàn)方式

基于數(shù)據(jù)庫實現(xiàn)

樂觀鎖實現(xiàn)

先去干,能不能干,能不能干成先不管,這就是樂觀心態(tài)。在開發(fā)過程中,樂觀鎖用的非常多,比如典型的 CAS ;在不加鎖的情況下保證數(shù)據(jù)的一致性。

使用方式也很簡單,只需要在表中添加一個版本號的字段,每次對數(shù)據(jù)進行修改的時候,通過版本來確定是否能夠更新 update xx set version = OLD_VERSION+1 where id = ID and version = OLD_VERSION , 如果更新不成功,客戶端可以選擇是否重試。當然,需要加上索引。

可見這種方式的優(yōu)勢其實很明顯,不加鎖,使用簡單。但也有一些局限性

  • 只能支持單數(shù)據(jù)更新的一致性(對于數(shù)據(jù)的插入可以通過唯一索引來解決
  • 由于是樂觀鎖(先干,在檢查),也就意味著可能活干完咯,發(fā)現(xiàn)更新不了,浪費了計算資源
  • 無法支持 access token

悲觀鎖實現(xiàn)

先自我審查自己能不能干,能不能干成,如果答案是no,那么就等著(阻塞)或先溜(返回),這就是悲觀心態(tài)。悲觀鎖在 access token 模式更加適用。

使用方式同樣很好理解(這只是基于數(shù)據(jù)庫的悲觀鎖的一種實現(xiàn)方式)

  1. 有一張 資源鎖 表,表中包含 字段,并需要加上唯一索引
  2. 當有線程想要獲取某個鎖時,只需要在 資源鎖 表中插入一條數(shù)據(jù)
  3. 如果插入成功,表示獲取鎖成功,插入失敗則表示鎖已經(jīng)被占用
  4. 業(yè)務執(zhí)行完釋放鎖,刪除對應的鎖記錄即可
// 1. 創(chuàng)建資源鎖表
CREATE TABLE `resource_lock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `lock_name` varchar(64) NOT NULL COMMENT '鎖名',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

//2. 獲取鎖(插入成本表示獲取到鎖
INSERT INTO resource_lock (lock_name) VALUES ('lockName');

//3. 釋放鎖
delete from resource_lock where lock_name ='lockName';

乍一看好像很簡單,如果程序一直保證正確執(zhí)行,這種方式好像也行,但沒如果... 對于一個分布式系統(tǒng),服務宕機是會出現(xiàn)的,所以還需要考慮一些新的可能發(fā)生的問題

  • 沒有失效機制:持有鎖的線程所在的服務宕機了,還沒來的及釋放鎖怎么辦? 可以通過在表中新增過期時間,寫一個定時任務定期刪除過期鎖
  • 不可重入:需要在表中新增線程信息,重入的時候先查詢是否存在鎖
  • 不支持鎖阻塞:需要編寫相應的邏輯
  • 基于數(shù)據(jù)庫實現(xiàn),那么數(shù)據(jù)庫的可用性就需要得到保證,而且在并發(fā)大的時候,對于數(shù)據(jù)庫的性能的影響問題

這么一分析...為了確保悲觀鎖的功能完整性,實現(xiàn)也會越來越復雜...以至于既然要用存儲去實現(xiàn),為撒不直接用緩存,性能至少有保障。

基于Redis :AP架構(gòu)

既然想到存儲用緩存來做,那必然想到的第一個就是 Redis 了,Redis 也很給力,可以很好的支撐分布式鎖的能力,提供了比較好用的命令

  • setnx: 當且僅當 key 不存在,將 key 的值設(shè)為 value ,并返回1;若給定的 key 已經(jīng)存在,則 SETNX 不做任何動作,并返回0。
  • expire: 為key設(shè)置一個超時時間,單位為second,超過這個時間鎖會自動釋放,避免死鎖。

大致的流程如下圖


  1. client A 和 client B 同時執(zhí)行 setnx("lock",UUID) 嘗試獲取到鎖,Redis 的實現(xiàn)保證了只會有一個 client 成功,假如 client A 運氣好成功了
  2. client A 緊接著馬上設(shè)置一個過期時間expire("lock",10)
  3. client A 繼續(xù)執(zhí)行業(yè)務邏輯
  4. 執(zhí)行完業(yè)務邏輯后釋放鎖

如果程序能夠正常走,好像也沒什么問題...但我們知道分布式架構(gòu)中,網(wǎng)絡(luò)是不可靠的,如果在設(shè)置過期時間前 client B 掛掉咯,那就 GG 了,因為沒有設(shè)置過期時間,那就成死鎖了.. 就像下面這樣


所以我們需要保證 setnxexpire 的原子性。在 Redis 2.6.12 之后增強了 setnx 命令,可以同時設(shè)置過期時間,從而保證原子性。

解決了死鎖問題,再來看看過期時間的問題,我們?nèi)绾闻袛辔覀儜撛O(shè)置多長時間的過期時間?

  • 設(shè)置短了,業(yè)務邏輯可能還沒執(zhí)行完,鎖被釋放了,被其它線程獲取執(zhí)行
  • 設(shè)置長了,需要業(yè)務邏輯處理完了自己釋放鎖(同樣會存在線程掛掉的情況)

其實我們想要到達一種效果,如果能夠自動續(xù)期,鎖快要過期了,但是業(yè)務操作還沒有處理完,就自動對鎖進行續(xù)期。Java 中的 Redisson 客戶端就通過 watch dog 機制(守護線程)來支持這個功能。

通過 Redisson 客戶端獲取鎖時會創(chuàng)建一個守護線程,通過守護線程來定期 check 過期時間,如果業(yè)務邏輯還在運行,那么就會續(xù)時。如果程序宕機,那么守護線程也會一起掛掉,redis 中的鎖也將不會再次續(xù)時,最后過期。從而自動實現(xiàn)續(xù)期且不會出現(xiàn)死鎖的問題。

簡單回顧一下,我們解決了

  • 獲取鎖和設(shè)置過期時間的原子性問題
  • 過期時間自動續(xù)時的問題

在單機模式下看起來已經(jīng)沒什么問題了。而在生產(chǎn)環(huán)境下一般都會是集群模式,比如哨兵模式。得益于 Redis 的 AP 架構(gòu),選擇了可用性,使得其性能非常好,但也正是因為AP架構(gòu),可能會導致數(shù)據(jù)丟失的情況。


  1. client A 獲取鎖成功
  2. master 節(jié)點在同步鎖信息到 slave 節(jié)點時,master 宕機,信息沒有向 slave 節(jié)點同步成功
  3. slave 節(jié)點通過選舉成為 master 節(jié)點
  4. client B 再次獲取相同的鎖,發(fā)現(xiàn) slave 節(jié)點上并沒有其它線程占用,所以也獲取到了鎖
  5. client A 和 client B 獲取到了相同的鎖

當然,這個是非常極端的情況下會出現(xiàn)的問題;雖然 Redis 之父 Antirez 提出來了分布式鎖的一種 「健壯」 的實現(xiàn)算法 RedLock,但依舊還是會有新的問題,比如節(jié)點奔潰重啟、時鐘跳躍...

總的來看,基于 Redis 實現(xiàn)分布式鎖是很常用的,性能也比較高,滿足絕大部分業(yè)務場景,如果我們能夠接受非常極端情況下帶來的鎖丟失問題,Redis 分布式鎖是個很好的選擇。

基于Zookeeper :CP 架構(gòu)

Zookeeper 是一種提供「分布式服務協(xié)調(diào)」的中心化服務,是以 Paxos 算法為基礎(chǔ)實現(xiàn)的。Zookeeper 采用的是 CP 架構(gòu),選擇了強一致性,這也就意味著不會像 Redis 那樣出現(xiàn)數(shù)據(jù)丟失的情況(主從切換時),但為了實現(xiàn)強一致性,那么性能肯定是要比 Redis 差一些。

使用 Zookeeper 來實現(xiàn)分布式鎖是比較簡單的

  1. client 會在 Zookeeper 中創(chuàng)建一個臨時節(jié)點,比如`/zk/lock
  2. 如果獲取成功,那么 client 會創(chuàng)建一個 session 保持和 Zookeeper 臨時節(jié)點的關(guān)聯(lián)
  3. client 處理業(yè)務邏輯
  4. client 處理完業(yè)務邏輯后刪除 臨時節(jié)點,關(guān)閉 session

如果 client 宕機,那么 session 就會結(jié)束,臨時節(jié)點也會自動刪除,其它 client 就可以創(chuàng)建 lock 節(jié)點。

session 的維護是依賴于 client 的定時心跳來維護的,也就是說,如果 client 沒有及時的給 Zookeeper 發(fā)送心跳檢查,那么 Zookeeper 就會認為這個 session 已經(jīng)過期了,就會刪除調(diào)臨時節(jié)點。比如出現(xiàn)長時間的 GC 或者長時間的網(wǎng)絡(luò)延遲,都可能會導致臨時節(jié)點被刪除的可能。

對于 Zookeeper 來說,實現(xiàn)分布式鎖從使用者角度來看比較簡單,不需要考慮太多的東西,比如過期時間的設(shè)置。但維護成本會比較高,性能相對 Redis 也會差一些,以及可能會出現(xiàn)長時間失聯(lián)導致的節(jié)點數(shù)據(jù)丟失的問題。

總結(jié)

  1. 優(yōu)先使用基于數(shù)據(jù)庫的樂觀鎖
  2. 如果期望更高的性能且能夠接受極少數(shù)情況的鎖丟失,那么優(yōu)先選擇 Redis
  3. 如果期望盡可能的避免鎖丟失,優(yōu)先選擇 Zookeeper,且考慮 GC 時間和 心跳檢查的設(shè)置
  4. 在分布式系統(tǒng)中極端情況下,分布式鎖都不太可靠,所以需要我們在業(yè)務層面的入口也相應的隔離,在真的發(fā)生了鎖丟失導致的數(shù)據(jù)不一致的情況做對應的補償
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

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