01 背景
在單機系統中,多個線程同時訪問某個共享資源時,能夠採用線程間加鎖保證數據一致性。可是,在分佈式系統中,程序運行在多臺機器上,各個節點之間無從知曉共享資源的鎖定狀態,即這種共享資源已經不是線程級別的,而是進程之間的。node
此時,就須要引入分佈式鎖,以實現多個客戶端互斥訪問共享資源。redis
須要加鎖的場景須要知足如下條件:算法
一、共享資源;數據庫
二、共享資源互斥;緩存
三、多任務環境。網絡
分佈式鎖的思路是:在系統中提供一個全局惟一的針對共享資源獲取鎖的組件,系統中須要訪問共享資源時,都向該組件申請鎖,待使用完畢後,釋放鎖。併發
02 特色
分佈式鎖通常要有如下特色:分佈式
排他性:任意時刻,只能有一個客戶端能獲取到鎖。高併發
容錯性:分佈式鎖服務通常要知足AP(便可用性Availability、分區容錯性Partition tolerance,這與分佈式事務強調一致性有區別),也就是說,只要分佈式鎖服務集羣節點大部分存活,客戶端就能夠進行加鎖解鎖操做。性能
避免死鎖:分佈式鎖必定能獲得釋放,即便客戶端在釋放以前崩潰或者網絡不可達。
03 方案
針對分佈式鎖的實現,目前比較經常使用的方案:
一、 基於數據庫實現分佈式鎖
二、 基於緩存(redis)實現分佈式鎖
三、 基於Zookeeper實現分佈式鎖
04 DB鎖
4.1 實現
一、惟一約束
二、基於數據庫來作分佈式鎖的話,一般有兩種作法:
基於數據庫的樂觀鎖(lock in share mode)
基於數據庫的悲觀鎖(select ... for update)
4.2 特色
優勢:
實現簡單
缺點:
一、可用性差(鎖的可用性依賴於數據庫,若是數據庫故障,則系統不可用);
二、數據庫性能存在瓶頸,不適合高併發場景;
三、鎖的失效時間難以控制,刪除鎖失敗容易致使死鎖。即這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖。
說明:通常在分佈式系統中使用這種機制實現分佈式鎖時,須要業務側增長控制鎖超時和重試的流程。
05 Redis分佈式鎖
5.1 實現
加鎖和解鎖的鎖必須是同一個,常見的解決方案是給每一個鎖一個鑰匙(惟一ID),加鎖時生成,解鎖時判斷。
一、redis原子操做
基於Redis實現的鎖機制,主要是依賴redis自身的原子操做。
加鎖
setnx命令加鎖,並設置鎖的有效時間和持有人標識:
SET user_key user_value NX PX 100
參數:
NX:只在在鍵不存在時,纔對鍵進行設置操做,SET key value NX 效果等同於 SETNX key value
PX millisecond:設置鍵的過時時間爲millisecond毫秒,當超過這個時間後,設置的鍵會自動失效
解鎖
檢查是否持有鎖,而後刪除鎖:
delete values命令刪除鎖
value具備惟一性,這是避免了一種狀況:假設A獲取了鎖,過時時間200ms,此時250ms以後,鎖已經自動釋放了,A去釋放鎖,可是此時可能B獲取了鎖。A客戶端就不能刪除B的鎖了。
二、Redlock
使用redis作分佈式鎖的缺點在於:若是採用單機部署模式,會存在單點問題,只要redis故障了。加鎖就不行了。
基於以上的考慮,其實redis的做者也考慮到這個問題,他提出了一個RedLock的算法,這個算法的意思大概是這樣的:
Redlock的實現以下:
1)獲取當前時間。
2)依次獲取N個節點的鎖
3)判斷是否獲取鎖成功。
若是client在上述步驟中獲取到了(N/2 + 1)個節點鎖,而且每一個鎖的過時時間都是大於0的,則獲取鎖成功,不然失敗。失敗時釋放鎖。
4)釋放鎖。
對全部節點發送釋放鎖的指令,之因此要對全部節點操做?由於分佈式場景下從一個節點獲取鎖失敗不表明在那個節點上加鎖失敗,可能實際上加鎖已經成功了,可是返回時由於網絡抖動超時了。
三、Redisson
Redisson是一個企業級的開源Redis Client,也提供了分佈式鎖的支持。
5.2 特色
優勢:
性能好,實現起來較爲方便。
缺點:
一、單點問題。這裏的單點指的是單master,就算是個集羣,若是加鎖成功後,鎖從master複製到slave的時候掛了,也是會出現同一資源被多個client加鎖的。
二、執行時間超過了鎖的過時時間。它獲取鎖的方式簡單粗暴,獲取不到鎖直接不斷嘗試獲取鎖,比較消耗性能。
三、redis的設計定位決定了它的數據並非強一致性的,在某些極端狀況下,可能會出現問題,不夠健壯。即使使用redlock算法來實現,在某些複雜場景下,也沒法保證其實現100%沒有問題。
06 zookeeper分佈式鎖
6.1 方案
![](http://static.javashuo.com/static/loading.gif)
當某客戶端要進行邏輯的加鎖時,就在zookeeper上的某個指定節點的目錄(locker目錄)下生成一個惟一的臨時有序節點(locker/node_N),而後判斷本身是不是這些有序節點中序號最小的一個,若是是,則算是獲取了鎖。若是不是,則說明沒有獲取到鎖,那麼就須要在序列中找到比本身小的那個節點,並對其調用exist()方法,對其註冊事件監聽,當監聽到這個節點被刪除了,那就再去判斷一次本身當初建立的節點是否變成了序列中最小的。若是是,則獲取鎖,若是不是,則重複上述步驟。
當釋放鎖的時候,只需將這個臨時節點刪除便可。
6.2 特色
優勢:
有效的解決單點問題,不可重入問題,非阻塞問題以及鎖沒法釋放的問題。實現起來較爲簡單。
缺點:
性能上不如使用緩存實現分佈式鎖,若是有較多的客戶端頻繁的申請加鎖、釋放鎖,對於zk集羣的壓力會比較大。
07 選擇
具體選擇哪一種分佈式鎖實現方案,須要結合業務場景,結合對性能、可靠性、複雜性的要求,具體以下:
一、從理解的難易程度角度(從低到高)
數據庫 > 緩存 > Zookeeper
二、從實現的複雜性角度(從低到高)
Zookeeper >= 緩存 > 數據庫
三、從性能角度(從高到低)
緩存 > Zookeeper >= 數據庫
四、從可靠性角度(從高到低)
Zookeeper > 緩存 > 數據庫
綜上所述:
若是系統不想引入過多網元,能夠採用數據庫鎖實現,好處就是比較容易理解,可是這種方案業務層控制邏輯多且複雜,須要對業務側足夠了解,易於理解可是實現複雜度最高。
若是追求高性能,Redis是最佳選擇,可是redis是有可能存在隱患的,可能會致使數據不對的狀況,可靠性不如ZK。
若是系統已經存在ZK集羣,優先選用ZK實現,實現最簡單,且能夠提供高可靠性,性能稍遜Redis緩存方案。