本文摘抄自如下2篇博文java
http://www.hollischuang.com/archives/1716node
https://blog.csdn.net/u010963948/article/details/79006572mysql
分佈式系統:以數據類分佈式系統爲例,隨着數據量愈來愈大,之前單一或者主從的數據存儲服務器已經不能知足生產的要求,因而考慮將數據根據必定的算法打散存儲到不一樣的服務器上。redis
分佈式鎖:在分佈式數據庫中若是訪問共享的資源,那麼就要求在同一時刻只有一個線程的某個方法訪問數據,以保證數據的一致性,不出現併發的問題。在這種場景下,之前數據庫的表級鎖和行級鎖不能知足要求,需設計一個跨數據庫的鎖,也就是分佈式鎖。分佈式鎖的核心原理就是利用一個物理上全局的對象控制分佈式系統中資源的訪問。好比數據庫表的一行記錄、一個文件、緩存數據庫中的對象、內存中的一個對象等等,具體有數據庫、redis、memorycache、zookeeper等算法
分佈式場景中的數據一致性問題一直是一個比較重要的話題。分佈式的CAP理論告訴咱們「任何一個分佈式系統都沒法同時知足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時知足兩項。」因此,不少系統在設計之初就要對這三者作出取捨。在互聯網領域的絕大多數的場景中,都須要犧牲強一致性來換取系統的高可用性,系統每每只須要保證「最終一致性」,只要這個最終時間是在用戶能夠接受的範圍內便可。sql
在不少場景中,爲了保證數據的最終一致性,須要不少的技術方案來支持,好比分佈式事務、分佈式鎖等。有的時候,咱們須要保證一個方法在同一時間內只能被同一個線程執行。在單機環境中,Java中其實提供了不少併發處理相關的API,可是這些API在分佈式場景中就無能爲力了。也就是說單純的Java Api並不能提供分佈式鎖的能力。因此針對分佈式鎖的實現目前有多種方案。數據庫
針對分佈式鎖的實現,目前比較經常使用的有如下幾種方案:編程
基於數據庫實現分佈式鎖 基於緩存(redis,memcached)實現分佈式鎖 基於Zookeeper實現分佈式鎖
在分析這幾種實現方案以前先來想一下,咱們須要的分佈式鎖應該是怎麼樣的?(這裏以方法鎖爲例,資源鎖同理)後端
能夠保證在分佈式部署的應用集羣中,同一個方法在同一時間只能被一臺機器上的一個線程執行。 這把鎖要是一把可重入鎖(避免死鎖) 這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條) 有高可用的獲取鎖和釋放鎖功能 獲取鎖和釋放鎖的性能要好
要實現分佈式鎖,最簡單的方式可能就是直接建立一張鎖表,而後經過操做該表中的數據來實現了。緩存
當咱們要鎖住某個方法或資源時,咱們就在該表中增長一條記錄,想要釋放鎖的時候就刪除這條記錄。
建立這樣一張數據庫表:
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='鎖定中的方法';
當咱們想要鎖住某個方法時,執行如下SQL:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
由於咱們對method_name
作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。
當方法執行完畢以後,想要釋放鎖的話,須要執行如下Sql:
delete from methodLock where method_name ='method_name'
上面這種簡單的實現有如下幾個問題:
一、這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會致使業務系統不可用。 二、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖。 三、這把鎖只能是非阻塞的,由於數據的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; }
在查詢語句後面增長for update
,數據庫會在查詢過程當中給數據庫表增長排他鎖(這裏再多提一句,InnoDB引擎在加鎖的時候,只有經過索引進行檢索的時候纔會使用行級鎖,不然會使用表級鎖。這裏咱們但願使用行級鎖,就要給method_name添加索引,值得注意的是,這個索引必定要建立成惟一索引,不然會出現多個重載方法之間沒法同時被訪問的問題。重載方法的話建議把參數類型也加上。)。當某條記錄被加上排他鎖以後,其餘線程沒法再在該行記錄上增長排他鎖。
咱們能夠認爲得到排它鎖的線程便可得到分佈式鎖,當獲取到鎖以後,能夠執行方法的業務邏輯,執行完方法以後,再經過如下方法解鎖:
public void unlock(){ connection.commit(); }
經過connection.commit()
操做來釋放鎖。
這種方法能夠有效的解決上面提到的沒法釋放鎖和阻塞鎖的問題。
for update
語句會在執行成功後當即返回,在執行失敗時一直處於阻塞狀態,直到成功。可是仍是沒法直接解決數據庫單點和可重入問題。
這裏還可能存在另一個問題,雖然咱們對method_name
使用了惟一索引,而且顯示使用for update
來使用行級鎖。可是,MySql會對查詢進行優化,即使在條件中使用了索引字段,可是否使用索引來檢索數據是由 MySQL 經過判斷不一樣執行計劃的代價來決定的,若是 MySQL 認爲全表掃效率更高,好比對一些很小的表,它就不會使用索引,這種狀況下 InnoDB 將使用表鎖,而不是行鎖。若是出現這種狀況,使用force index強制mysql走索引。
還有一個問題,就是咱們要使用排他鎖來進行分佈式鎖的lock,那麼一個排他鎖長時間不提交,就會佔用數據庫鏈接。一旦相似的鏈接變得多了,就可能把數據庫鏈接池撐爆。若是出現這種狀況,能夠設置SQL執行的超時時間,好比mybatis的全局設置中 <setting name="defaultStatementTimeout" value="25"/>
總結一下使用數據庫來實現分佈式鎖的方式,這兩種方式都是依賴數據庫的一張表,一種是經過表中的記錄的存在狀況肯定當前是否有鎖存在,另一種是經過數據庫的排他鎖來實現分佈式鎖。
數據庫實現分佈式鎖的優勢
直接藉助數據庫,容易理解。
數據庫實現分佈式鎖的缺點
會有各類各樣的問題,在解決問題的過程當中會使整個方案變得愈來愈複雜。
操做數據庫須要必定的開銷,性能問題須要考慮。
使用數據庫的行級鎖並不必定靠譜,尤爲是當咱們的鎖表並不大的時候。
相比較於基於數據庫實現分佈式鎖的方案來講,基於緩存來實如今性能方面會表現的更好一點。
目前有不少成熟的緩存產品,包括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); }
以上實現方式一樣存在幾個問題:
一、單點問題。
二、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在redis中,其餘線程沒法再得到到鎖。
三、這把鎖只能是非阻塞的,不管成功仍是失敗都直接返回。
四、這把鎖是非重入的,一個線程得到鎖以後,在釋放鎖以前,沒法再次得到該鎖,由於使用到的key在redis中已經存在。沒法再執行setNX操做。
五、這把鎖是非公平的,全部等待的線程同時去發起setNX操做,運氣好的線程能獲取鎖。
固然,一樣有方式能夠解決。
如今主流的緩存服務都支持集羣部署,經過集羣來解決單點問題。
沒有失效時間?redis的setExpire方法支持傳入失效時間,到達時間以後數據會自動刪除。
非阻塞?while重複執行。
非可重入?在一個線程獲取到鎖以後,把當前主機信息和線程信息保存起來,下次再獲取以前先檢查本身是否是當前鎖的擁有者。
非公平?在線程獲取鎖以前先把全部等待的線程放入一個隊列中,而後按先進先出原則獲取鎖。
redis的做者Salvatore Sanfilippo,提出了Redlock算法,該算法實現了比單一節點更安全、可靠的分佈式鎖管理(DLM)。
Redlock算法假設有N個redis節點,這些節點互相獨立,通常設置爲N=5,這N個節點運行在不一樣的機器上以保持物理層面的獨立。
算法的步驟以下:
該專家提到,考慮分佈式鎖的時候須要考慮兩個方面:性能和正確性。
若是使用高性能的分佈式鎖,對正確性要求不高的場景下,那麼使用緩存鎖就足夠了。
若是使用可靠性高的分佈式鎖,那麼就須要考慮嚴格的可靠性問題。而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)。
總結專家關於Redlock不可用的兩點:
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解決鎖超時問題能夠歸納成下面五點:
專家說到的另外一個時鐘問題,redis做者也給出瞭解釋。客戶端實際得到的鎖的時間是默認的超時時間,減去獲取鎖所花費的時間,若是獲取鎖花費時間過長致使超過了鎖的默認超時間,那麼此時客戶端並不能獲取到鎖,不會存在專家提出的例子。
第一個問題我歸納爲,在一個客戶端獲取了分佈式鎖後,在客戶端的處理過程當中,可能出現鎖超時釋放的狀況,這裏說的處理中除了GC等非抗力外,程序流程未處理完也是可能發生的。以前在說到數據庫鎖設置的超時時間2分鐘,若是出現某個任務佔用某個訂單鎖超過2分鐘,那麼另外一個交易中心就能夠得到這把訂單鎖,從而兩個交易中心同時處理同一個訂單。正常狀況,任務固然秒級處理完成,但是有時候,加入某個rpc請求設置的超時時間過長,一個任務中有多個這樣的超時請求,那麼,極可能就出現超過自動解鎖時間了。當初咱們的交易模塊是用C++寫的,不存在GC,若是用java寫,中間還可能出現Full GC,那麼鎖超時解鎖後,本身客戶端沒法感知,是件很是嚴重的事情。我以爲這不是鎖自己的問題,上面說到的任何一個分佈式鎖,只要自帶了超時釋放的特性,都會出現這樣的問題。若是使用鎖的超時功能,那麼客戶端必定得設置獲取鎖超時後,採起相應的處理,而不是繼續處理共享資源。Redlock的算法,在客戶端獲取鎖後,會返回客戶端能佔用的鎖時間,客戶端必須處理該時間,讓任務在超過該時間後中止下來。
第二個問題,天然就是分佈式專家沒有理解Redlock。Redlock有個關鍵的特性是,獲取鎖的時間是鎖默認超時的總時間減去獲取鎖所花費的時間,這樣客戶端處理的時間就是一個相對時間,就跟本地時間無關了。
由此看來,Redlock的正確性是能獲得很好的保證的。仔細分析Redlock,相比於一個節點的redis,Redlock提供的最主要的特性是可靠性更高,這在有些場景下是很重要的特性。可是我以爲Redlock爲了實現可靠性,卻花費了過大的代價。
分析了這麼多緣由,我以爲Redlock的問題,最關鍵的一點在於Redlock須要客戶端去保證寫入的一致性,後端5個節點徹底獨立,全部的客戶端都得操做這5個節點。若是5個節點有一個leader,客戶端只要從leader獲取鎖,其餘節點能同步leader的數據,這樣,分區、超時、衝突等問題都不會存在。因此爲了保證分佈式鎖的正確性,我以爲使用強一致性的分佈式協調服務能更好的解決問題
使用緩存實現分佈式鎖的優勢
性能好,實現起來較爲方便。
使用緩存實現分佈式鎖的缺點
經過超時時間來控制鎖的失效時間並非十分的靠譜。
基於zookeeper臨時有序節點能夠實現的分佈式鎖。
大體思想即爲:每一個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個惟一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只須要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除便可。同時,其能夠避免服務宕機致使的鎖沒法釋放,而產生的死鎖問題。
來看下Zookeeper能不能解決前面提到的問題。
鎖沒法釋放?使用Zookeeper能夠有效的解決鎖沒法釋放的問題,由於在建立鎖的時候,客戶端會在ZK中建立一個臨時節點,一旦客戶端獲取到鎖以後忽然掛掉(Session鏈接斷開),那麼這個臨時節點就會自動刪除掉。其餘客戶端就能夠再次得到鎖。
非阻塞鎖?使用Zookeeper能夠實現阻塞的鎖,客戶端能夠經過在ZK中建立順序節點,而且在節點上綁定監聽器,一旦節點有變化,Zookeeper會通知客戶端,客戶端能夠檢查本身建立的節點是否是當前全部節點中序號最小的,若是是,那麼本身就獲取到鎖,即可以執行業務邏輯了。
不可重入?使用Zookeeper也能夠有效的解決不可重入的問題,客戶端在建立節點的時候,把當前客戶端的主機信息和線程信息直接寫入到節點中,下次想要獲取鎖的時候和當前最小的節點中的數據比對一下就能夠了。若是和本身的信息同樣,那麼本身直接獲取到鎖,若是不同就再建立一個臨時的順序節點,參與排隊。
單點問題?使用Zookeeper能夠有效的解決單點問題,ZK是集羣部署的,只要集羣中有半數以上的機器存活,就能夠對外提供服務。
公平問題?使用Zookeeper能夠解決公平鎖問題,客戶端在ZK中建立的臨時節點是有序的,每次鎖被釋放時,ZK能夠通知最小節點來獲取鎖,保證了公平。
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是否知足因果一致性,須要看客戶端的編程方式。
不知足因果一致性的作法
知足因果一致性的作法
第二種事件監聽機制也是對Zookeeper進行正確編程應該使用的方法,因此,Zookeeper應該是知足因果一致性的
因此咱們在基於Zookeeper實現分佈式鎖的時候,應該使用知足因果一致性的作法,即等待鎖的線程都監聽Zookeeper上鎖的變化,在鎖被釋放的時候,Zookeeper會將鎖變化的通知告訴知足公平鎖條件的等待線程。
能夠直接使用zookeeper第三方庫 Curator 客戶端,這個客戶端中封裝了一個可重入的鎖服務。
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; }
Curator提供的InterProcessMutex是分佈式鎖的實現。acquire方法用戶獲取鎖,release方法用於釋放鎖。
使用ZK實現的分佈式鎖好像徹底符合了本文開頭咱們對一個分佈式鎖的全部指望。可是,其實並非,Zookeeper實現的分佈式鎖其實存在一個缺點,那就是性能上可能並無緩存服務那麼高。由於每次在建立鎖和釋放鎖的過程當中,都要動態建立、銷燬瞬時節點來實現鎖功能。ZK中建立和刪除節點只能經過Leader服務器來執行,而後將數據同不到全部的Follower機器上。
其實,使用Zookeeper也有可能帶來併發問題,只是並不常見而已。考慮這樣的狀況,因爲網絡抖動,客戶端可ZK集羣的session鏈接斷了,那麼zk覺得客戶端掛了,就會刪除臨時節點,這時候其餘客戶端就能夠獲取到分佈式鎖了。就可能產生併發問題。這個問題不常見是由於zk有重試機制,一旦zk集羣檢測不到客戶端的心跳,就會重試,Curator客戶端支持多種重試策略。屢次重試以後還不行的話纔會刪除臨時節點。(因此,選擇一個合適的重試策略也比較重要,要在鎖的粒度和併發之間找一個平衡。)
使用Zookeeper實現分佈式鎖的優勢
有效的解決單點問題,不可重入問題,非阻塞問題以及鎖沒法釋放的問題。實現起來較爲簡單。
使用Zookeeper實現分佈式鎖的缺點
性能上不如使用緩存實現分佈式鎖。 須要對ZK的原理有所瞭解。
上面幾種方式,哪一種方式都沒法作到完美。就像CAP同樣,在複雜性、可靠性、性能等方面沒法同時知足,因此,根據不一樣的應用場景選擇最適合本身的纔是王道。
數據庫 > 緩存 > Zookeeper
Zookeeper >= 緩存 > 數據庫
緩存 > Zookeeper >= 數據庫
Zookeeper > 緩存 > 數據庫