分佈式鎖的幾種使用方式(redis、zookeeper、數據庫)

Q:一個業務服務器,一個數據庫,操做:查詢用戶當前餘額,扣除當前餘額的3%做爲手續費
synchronized
lock
db lock
Q:兩個業務服務器,一個數據庫,操做:查詢用戶當前餘額,扣除當前餘額的3%做爲手續費
分佈式鎖
咱們須要怎麼樣的分佈式鎖?
能夠保證在分佈式部署的應用集羣中,同一個方法在同一時間只能被一臺機器上的一個線程執行。java

這把鎖要是一把可重入鎖(避免死鎖)node

這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)redis

這把鎖最好是一把公平鎖(根據業務需求考慮要不要這條)算法

有高可用的獲取鎖和釋放鎖功能數據庫

獲取鎖和釋放鎖的性能要好編程

1、基於數據庫實現的分佈式鎖
基於表實現的分佈式鎖
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '備註信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數據時間,自動生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
1
2
3
4
5
6
7
當咱們想要鎖住某個方法時,執行如下SQL:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
由於咱們對method_name作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。後端

當方法執行完畢以後,想要釋放鎖的話,須要執行如下Sql:
delete from methodLock where method_name ='method_name'緩存

上面這種簡單的實現有如下幾個問題:安全

這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會致使業務系統不可用。服務器

這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖。

這把鎖只能是非阻塞的,由於數據的insert操做,一旦插入失敗就會直接報錯。沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做。

這把鎖是非重入的,同一個線程在沒有釋放鎖以前沒法再次得到該鎖。由於數據中數據已經存在了。

這把鎖是非公平鎖,全部等待鎖的線程憑運氣去爭奪鎖。

固然,咱們也能夠有其餘方式解決上面的問題。

數據庫是單點?搞兩個數據庫,數據以前雙向同步。一旦掛掉快速切換到備庫上。

沒有失效時間?只要作一個定時任務,每隔必定時間把數據庫中的超時數據清理一遍。

非阻塞的?搞一個while循環,直到insert成功再返回成功。

非重入的?在數據庫表中加個字段,記錄當前得到鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,若是當前機器的主機信息和線程信息在數據庫能夠查到的話,直接把鎖分配給他就能夠了。

非公平的?再建一張中間表,將等待鎖的線程全記錄下來,並根據建立時間排序,只有最早建立的容許獲取鎖

基於排他鎖實現的分佈式鎖
除了能夠經過增刪操做數據表中的記錄之外,其實還能夠藉助數據中自帶的鎖來實現分佈式的鎖。

咱們還用剛剛建立的那張數據庫表。能夠經過數據庫的排他鎖來實現分佈式鎖。 基於MySql的InnoDB引擎,可使用如下方法來實現加鎖操做:

public boolean lock(){
connection.setAutoCommit(false);
while(true){
try{
result = select * from methodLock where method_name=xxx for update;
if(result==null){
return true;
}
}catch(Exception e){

}
sleep(1000);
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在查詢語句後面增長for update,數據庫會在查詢過程當中給數據庫表增長排他鎖。當某條記錄被加上排他鎖以後,其餘線程沒法再在該行記錄上增長排他鎖。

咱們能夠認爲得到排它鎖的線程便可得到分佈式鎖,當獲取到鎖以後,能夠執行方法的業務邏輯,執行完方法以後,再經過如下方法解鎖:

public void unlock(){ connection.commit(); }

經過connection.commit();操做來釋放鎖。

這種方法能夠有效的解決上面提到的沒法釋放鎖和阻塞鎖的問題。

阻塞鎖? for update語句會在執行成功後當即返回,在執行失敗時一直處於阻塞狀態,直到成功。

鎖定以後服務宕機,沒法釋放?使用這種方式,服務宕機以後數據庫會本身把鎖釋放掉。

可是仍是沒法直接解決數據庫單點、可重入和公平鎖的問題。

總結一下使用數據庫來實現分佈式鎖的方式,這兩種方式都是依賴數據庫的一張表,一種是經過表中的記錄的存在狀況肯定當前是否有鎖存在,另一種是經過數據庫的排他鎖來實現分佈式鎖。

數據庫實現分佈式鎖的優勢
直接藉助數據庫,容易理解。

數據庫實現分佈式鎖的缺點
會有各類各樣的問題,在解決問題的過程當中會使整個方案變得愈來愈複雜。

操做數據庫須要必定的開銷,性能問題須要考慮。

2、基於緩存的分佈式鎖
相比較於基於數據庫實現分佈式鎖的方案來講,基於緩存來實如今性能方面會表現的更好一點。

目前有不少成熟的緩存產品,包括Redis,memcached等。這裏以Redis爲例來分析下使用緩存實現分佈式鎖的方案。

基於Redis實現分佈式鎖在網上有不少相關文章,其中主要的實現方式是使用Jedis.setNX方法來實現。

public boolean trylock(String key) {
ResultCode code = jedis.setNX(key, "This is a Lock.");
if (ResultCode.SUCCESS.equals(code))
return true;
else
return false;
}
public boolean unlock(String key){
ldbTairManager.invalid(NAMESPACE, key);
}
1
2
3
4
5
6
7
8
9
10
以上實現方式一樣存在幾個問題:

一、單點問題。

二、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在redis中,其餘線程沒法再得到到鎖。

三、這把鎖只能是非阻塞的,不管成功仍是失敗都直接返回。

四、這把鎖是非重入的,一個線程得到鎖以後,在釋放鎖以前,沒法再次得到該鎖,由於使用到的key在redis中已經存在。沒法再執行setNX操做。

五、這把鎖是非公平的,全部等待的線程同時去發起setNX操做,運氣好的線程能獲取鎖。

固然,一樣有方式能夠解決。

如今主流的緩存服務都支持集羣部署,經過集羣來解決單點問題。

沒有失效時間?redis的setExpire方法支持傳入失效時間,到達時間以後數據會自動刪除。

非阻塞?while重複執行。

非可重入?在一個線程獲取到鎖以後,把當前主機信息和線程信息保存起來,下次再獲取以前先檢查本身是否是當前鎖的擁有者。

非公平?在線程獲取鎖以前先把全部等待的線程放入一個隊列中,而後按先進先出原則獲取鎖。

redis集羣的同步策略是須要時間的,有可能A線程setNX成功後拿到鎖,可是這個值尚未更新到B線程執行setNX的這臺服務器,那就會產生併發問題。
redis的做者Salvatore Sanfilippo,提出了Redlock算法,該算法實現了比單一節點更安全、可靠的分佈式鎖管理(DLM)。

Redlock算法假設有N個redis節點,這些節點互相獨立,通常設置爲N=5,這N個節點運行在不一樣的機器上以保持物理層面的獨立。

算法的步驟以下:

一、客戶端獲取當前時間,以毫秒爲單位。
二、客戶端嘗試獲取N個節點的鎖,(每一個節點獲取鎖的方式和前面說的緩存鎖同樣),N個節點以相同的key和value獲取鎖。客戶端須要設置接口訪問超時,接口超時時間須要遠遠小於鎖超時時間,好比鎖自動釋放的時間是10s,那麼接口超時大概設置5-50ms。這樣能夠在有redis節點宕機後,訪問該節點時能儘快超時,而減少鎖的正常使用。
三、客戶端計算在得到鎖的時候花費了多少時間,方法是用當前時間減去在步驟一獲取的時間,只有客戶端得到了超過3個節點的鎖,並且獲取鎖的時間小於鎖的超時時間,客戶端纔得到了分佈式鎖。
四、客戶端獲取的鎖的時間爲設置的鎖超時時間減去步驟三計算出的獲取鎖花費時間。
五、若是客戶端獲取鎖失敗了,客戶端會依次刪除全部的鎖。
使用Redlock算法,能夠保證在掛掉最多2個節點的時候,分佈式鎖服務仍然能工做,這相比以前的數據庫鎖和緩存鎖大大提升了可用性,因爲redis的高效性能,分佈式緩存鎖性能並不比數據庫鎖差。
可是,有一位分佈式的專家寫了一篇文章《How to do distributed locking》,質疑Redlock的正確性。
該專家提到,考慮分佈式鎖的時候須要考慮兩個方面:性能和正確性。

若是使用高性能的分佈式鎖,對正確性要求不高的場景下,那麼使用緩存鎖就足夠了。

若是使用可靠性高的分佈式鎖,那麼就須要考慮嚴格的可靠性問題。而Redlock則不符合正確性。爲何不符合呢?專家列舉了幾個方面。

如今不少編程語言使用的虛擬機都有GC功能,在Full GC的時候,程序會停下來處理GC,有些時候Full GC耗時很長,甚至程序有幾分鐘的卡頓,文章列舉了HBase的例子,HBase有時候GC幾分鐘,會致使租約超時。並且Full GC何時到來,程序沒法掌控,程序的任什麼時候候均可能停下來處理GC,好比下圖,客戶端1得到了鎖,正準備處理共享資源的時候,發生了Full GC直到鎖過時。這樣,客戶端2又得到了鎖,開始處理共享資源。在客戶端2處理的時候,客戶端1 Full GC完成,也開始處理共享資源,這樣就出現了2個客戶端都在處理共享資源的狀況。

 

專家給出瞭解決辦法,以下圖,看起來就是MVCC,給鎖帶上token,token就是version的概念,每次操做鎖完成,token都會加1,在處理共享資源的時候帶上token,只有指定版本的token可以處理共享資源。

 

而後專家還說到了算法依賴本地時間,並且redis在處理key過時的時候,依賴gettimeofday方法得到時間,而不是monotonic clock,這也會帶來時間的不許確。好比一下場景,兩個客戶端client 1和client 2,5個redis節點nodes (A, B, C, D and E)。

一、client 1從A、B、C成功獲取鎖,從D、E獲取鎖網絡超時。
二、節點C的時鐘不許確,致使鎖超時。
三、client 2從C、D、E成功獲取鎖,從A、B獲取鎖網絡超時。
四、這樣client 1和client 2都得到了鎖。
總結專家關於Redlock不可用的兩點:

一、GC等場景可能隨時發生,並致使在客戶端獲取了鎖,在處理中超時,致使另外的客戶端獲取了鎖。專家還給出了使用自增token的解決方法。
二、算法依賴本地時間,會出現時鐘不許,致使2個客戶端同時得到鎖的狀況。
因此專家給出的結論是,只有在有界的網絡延遲、有界的程序中斷、有界的時鐘錯誤範圍,Redlock才能正常工做,可是這三種場景的邊界又是沒法確認的,因此專家不建議使用Redlock。對於正確性要求高的場景,專家推薦了Zookeeper,關於使用Zookeeper做爲分佈式鎖後面再討論。
Redis做者的迴應
redis做者看到這個專家的文章後,寫了一篇博客予以迴應。做者很客氣的感謝了專家,而後表達出了對專家觀點的不認同。

I asked for an analysis in the original Redlock specification here: http://redis.io/topics/distlock. So thank you Martin. However I don’t agree with the analysis.

redis做者關於使用token解決鎖超時問題能夠歸納成下面五點:

觀點1,使用分佈式鎖通常是在,你沒有其餘方式去控制共享資源了,專家使用token來保證對共享資源的處理,那麼就不須要分佈式鎖了。
觀點2,對於token的生成,爲保證不一樣客戶端得到的token的可靠性,生成token的服務仍是須要分佈式鎖保證服務的可靠性。
觀點3,對於專家說的自增的token的方式,redis做者認爲徹底不必,每一個客戶端能夠生成惟一的uuid做爲token,給共享資源設置爲只有該uuid的客戶端才能處理的狀態,這樣其餘客戶端就沒法處理該共享資源,直到得到鎖的客戶端釋放鎖。
觀點4,redis做者認爲,對於token是有序的,並不能解決專家提出的GC問題,如上圖所示,若是token 34的客戶端寫入過程當中發送GC致使鎖超時,另外的客戶端可能得到token 35的鎖,並再次開始寫入,致使鎖衝突。因此token的有序並不能跟共享資源結合起來。
觀點5,redis做者認爲,大部分場景下,分佈式鎖用來處理非事務場景下的更新問題。做者意思應該是有些場景很難結合token處理共享資源,因此得依賴鎖去鎖定資源並進行處理。
專家說到的另外一個時鐘問題,redis做者也給出瞭解釋。客戶端實際得到的鎖的時間是默認的超時時間,減去獲取鎖所花費的時間,若是獲取鎖花費時間過長致使超過了鎖的默認超時間,那麼此時客戶端並不能獲取到鎖,不會存在專家提出的例子。

我的感受
第一個問題我歸納爲,在一個客戶端獲取了分佈式鎖後,在客戶端的處理過程當中,可能出現鎖超時釋放的狀況,這裏說的處理中除了GC等非抗力外,程序流程未處理完也是可能發生的。以前在說到數據庫鎖設置的超時時間2分鐘,若是出現某個任務佔用某個訂單鎖超過2分鐘,那麼另外一個交易中心就能夠得到這把訂單鎖,從而兩個交易中心同時處理同一個訂單。正常狀況,任務固然秒級處理完成,但是有時候,加入某個rpc請求設置的超時時間過長,一個任務中有多個這樣的超時請求,那麼,極可能就出現超過自動解鎖時間了。當初咱們的交易模塊是用C++寫的,不存在GC,若是用java寫,中間還可能出現Full GC,那麼鎖超時解鎖後,本身客戶端沒法感知,是件很是嚴重的事情。我以爲這不是鎖自己的問題,上面說到的任何一個分佈式鎖,只要自帶了超時釋放的特性,都會出現這樣的問題。若是使用鎖的超時功能,那麼客戶端必定得設置獲取鎖超時後,採起相應的處理,而不是繼續處理共享資源。Redlock的算法,在客戶端獲取鎖後,會返回客戶端能佔用的鎖時間,客戶端必須處理該時間,讓任務在超過該時間後中止下來。

第二個問題,天然就是分佈式專家沒有理解Redlock。Redlock有個關鍵的特性是,獲取鎖的時間是鎖默認超時的總時間減去獲取鎖所花費的時間,這樣客戶端處理的時間就是一個相對時間,就跟本地時間無關了。

由此看來,Redlock的正確性是能獲得很好的保證的。仔細分析Redlock,相比於一個節點的redis,Redlock提供的最主要的特性是可靠性更高,這在有些場景下是很重要的特性。可是我以爲Redlock爲了實現可靠性,卻花費了過大的代價。

首先必須部署5個節點才能讓Redlock的可靠性更強。
而後須要請求5個節點才能獲取到鎖,經過Future的方式,先併發向5個節點請求,再一塊兒得到響應結果,能縮短響應時間,不過仍是比單節點redis鎖要耗費更多時間。
而後因爲必須獲取到5個節點中的3個以上,因此可能出現獲取鎖衝突,即你們都得到了1-2把鎖,結果誰也不能獲取到鎖,這個問題,redis做者借鑑了raft算法的精髓,經過沖突後在隨機時間開始,能夠大大下降衝突時間,可是這問題並不能很好的避免,特別是在第一次獲取鎖的時候,因此獲取鎖的時間成本增長了。
若是5個節點有2個宕機,此時鎖的可用性會極大下降,首先必須等待這兩個宕機節點的結果超時才能返回,另外只有3個節點,客戶端必須獲取到這所有3個節點的鎖才能擁有鎖,難度也加大了。
若是出現網絡分區,那麼可能出現客戶端永遠也沒法獲取鎖的狀況。
分析了這麼多緣由,我以爲Redlock的問題,最關鍵的一點在於Redlock須要客戶端去保證寫入的一致性,後端5個節點徹底獨立,全部的客戶端都得操做這5個節點。若是5個節點有一個leader,客戶端只要從leader獲取鎖,其餘節點能同步leader的數據,這樣,分區、超時、衝突等問題都不會存在。因此爲了保證分佈式鎖的正確性,我以爲使用強一致性的分佈式協調服務能更好的解決問題。

問題又來了,失效時間我設置多長時間爲好?如何設置的失效時間過短,方法沒等執行完,鎖就自動釋放了,那麼就會產生併發問題。若是設置的時間太長,其餘獲取鎖的線程就可能要平白的多等一段時間。
這個問題使用數據庫實現分佈式鎖一樣存在。

對於這個問題目前主流的作法是每得到一個鎖時,只設置一個很短的超時時間,同時起一個線程在每次快要到超時時間時去刷新鎖的超時時間。在釋放鎖的同時結束這個線程。如redis官方的分佈式鎖組件redisson,就是用的這種方案。

使用緩存實現分佈式鎖的優勢
性能好。

使用緩存實現分佈式鎖的缺點
實現過於負責,須要考慮的因素太多。

基於Zookeeper實現的分佈式鎖
基於zookeeper臨時有序節點能夠實現的分佈式鎖。

大體思想即爲:每一個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個惟一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只須要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除便可。同時,其能夠避免服務宕機致使的鎖沒法釋放,而產生的死鎖問題。

來看下Zookeeper能不能解決前面提到的問題。

鎖沒法釋放?使用Zookeeper能夠有效的解決鎖沒法釋放的問題,由於在建立鎖的時候,客戶端會在ZK中建立一個臨時節點,一旦客戶端獲取到鎖以後忽然掛掉(Session鏈接斷開),那麼這個臨時節點就會自動刪除掉。其餘客戶端就能夠再次得到鎖。

非阻塞鎖?使用Zookeeper能夠實現阻塞的鎖,客戶端能夠經過在ZK中建立順序節點,而且在節點上綁定監聽器,一旦節點有變化,Zookeeper會通知客戶端,客戶端能夠檢查本身建立的節點是否是當前全部節點中序號最小的,若是是,那麼本身就獲取到鎖,即可以執行業務邏輯了。

不可重入?使用Zookeeper也能夠有效的解決不可重入的問題,客戶端在建立節點的時候,把當前客戶端的主機信息和線程信息直接寫入到節點中,下次想要獲取鎖的時候和當前最小的節點中的數據比對一下就能夠了。若是和本身的信息同樣,那麼本身直接獲取到鎖,若是不同就再建立一個臨時的順序節點,參與排隊。

單點問題?使用Zookeeper能夠有效的解決單點問題,ZK是集羣部署的,只要集羣中有半數以上的機器存活,就能夠對外提供服務。

公平問題?使用Zookeeper能夠解決公平鎖問題,客戶端在ZK中建立的臨時節點是有序的,每次鎖被釋放時,ZK能夠通知最小節點來獲取鎖,保證了公平。

問題又來了,咱們知道Zookeeper須要集羣部署,會不會出現Redis集羣那樣的數據同步問題呢?
Zookeeper是一個保證了弱一致性即最終一致性的分佈式組件。

Zookeeper採用稱爲Quorum Based Protocol的數據同步協議。假如Zookeeper集羣有N臺Zookeeper服務器(N一般取奇數,3臺可以知足數據可靠性同時有很高讀寫性能,5臺在數據可靠性和讀寫性能方面平衡最好),那麼用戶的一個寫操做,首先同步到N/2 + 1臺服務器上,而後返回給用戶,提示用戶寫成功。基於Quorum Based Protocol的數據同步協議決定了Zookeeper可以支持什麼強度的一致性。

在分佈式環境下,知足強一致性的數據儲存基本不存在,它要求在更新一個節點的數據,須要同步更新全部的節點。這種同步策略出如今主從同步複製的數據庫中。可是這種同步策略,對寫性能的影響太大而不多見於實踐。由於Zookeeper是同步寫N/2+1個節點,還有N/2個節點沒有同步更新,因此Zookeeper不是強一致性的。

用戶的數據更新操做,不保證後續的讀操做可以讀到更新後的值,可是最終會呈現一致性。犧牲一致性,並非徹底無論數據的一致性,不然數據是混亂的,那麼系統可用性再高分佈式再好也沒有了價值。犧牲一致性,只是再也不要求關係型數據庫中的強一致性,而是隻要系統能達到最終一致性便可。

Zookeeper是否知足因果一致性,須要看客戶端的編程方式。

不知足因果一致性的作法

A進程向Zookeeper的/z寫入一個數據,成功返回
A進程通知B進程,A已經修改了/z的數據
B讀取Zookeeper的/z的數據
因爲B鏈接的Zookeeper的服務器有可能尚未獲得A寫入數據的更新,那麼B將讀不到A寫入的數據
知足因果一致性的作法

B進程監聽Zookeeper上/z的數據變化
A進程向Zookeeper的/z寫入一個數據,成功返回前,Zookeeper須要調用註冊在/z上的監聽器,Leader將數據變化的通知告訴B
B進程的事件響應方法獲得響應後,去取變化的數據,那麼B必定可以獲得變化的值
這裏的因果一致性提如今Leader和B之間的因果一致性,也就是是Leader通知了數據有變化
第二種事件監聽機制也是對Zookeeper進行正確編程應該使用的方法,因此,Zookeeper應該是知足因果一致性的

因此咱們在基於Zookeeper實現分佈式鎖的時候,應該使用知足因果一致性的作法,即等待鎖的線程都監聽Zookeeper上鎖的變化,在鎖被釋放的時候,Zookeeper會將鎖變化的通知告訴知足公平鎖條件的等待線程。

能夠直接使用zookeeper第三方庫客戶端,這個客戶端中封裝了一個可重入的鎖服務。

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
try {
return interProcessMutex.acquire(timeout, unit);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}

public boolean unlock() {
try {
interProcessMutex.release();
} catch (Throwable e) {
log.error(e.getMessage(), e);
} finally {
executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
}
return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用ZK實現的分佈式鎖好像徹底符合了本文開頭咱們對一個分佈式鎖的全部指望。可是,其實並非,Zookeeper實現的分佈式鎖其實存在一個缺點,那就是性能上可能並無緩存服務那麼高。由於每次在建立鎖和釋放鎖的過程當中,都要動態建立、銷燬瞬時節點來實現鎖功能。ZK中建立和刪除節點只能經過Leader服務器來執行,而後將數據同不到全部的Follower機器上。

使用Zookeeper實現分佈式鎖的優勢
有效的解決單點問題,不可重入問題,非阻塞問題以及鎖沒法釋放的問題。實現起來較爲簡單。

使用Zookeeper實現分佈式鎖的缺點
性能上不如使用緩存實現分佈式鎖。 須要對ZK的原理有所瞭解。

三種方案的比較
從理解的難易程度角度(從低到高)
數據庫 > 緩存 > Zookeeper

從實現的複雜性角度(從低到高)
Zookeeper > 緩存 > 數據庫

從性能角度(從高到低)
緩存 > Zookeeper >= 數據庫

從可靠性角度(從高到低)Zookeeper > 緩存 > 數據庫\

相關文章
相關標籤/搜索