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:java
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
由於咱們對method_name
作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。node
當方法執行完畢以後,想要釋放鎖的話,須要執行如下Sql:redis
delete from methodLock where method_name ='method_name'
上面這種簡單的實現有如下幾個問題:算法
固然,咱們也能夠有其餘方式解決上面的問題。數據庫
除了能夠經過增刪操做數據表中的記錄之外,其實還能夠藉助數據中自帶的鎖來實現分佈式的鎖。編程
咱們還用剛剛建立的那張數據庫表。能夠經過數據庫的排他鎖來實現分佈式鎖。 基於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
,數據庫會在查詢過程當中給數據庫表增長排他鎖。當某條記錄被加上排他鎖以後,其餘線程沒法再在該行記錄上增長排他鎖。緩存
咱們能夠認爲得到排它鎖的線程便可得到分佈式鎖,當獲取到鎖以後,能夠執行方法的業務邏輯,執行完方法以後,再經過如下方法解鎖:安全
public void unlock(){ connection.commit(); }
經過connection.commit();
操做來釋放鎖。服務器
這種方法能夠有效的解決上面提到的沒法釋放鎖和阻塞鎖的問題。
阻塞鎖? for update語句會在執行成功後當即返回,在執行失敗時一直處於阻塞狀態,直到成功。
鎖定以後服務宕機,沒法釋放?使用這種方式,服務宕機以後數據庫會本身把鎖釋放掉。
可是仍是沒法直接解決數據庫單點、可重入和公平鎖的問題。
總結一下使用數據庫來實現分佈式鎖的方式,這兩種方式都是依賴數據庫的一張表,一種是經過表中的記錄的存在狀況肯定當前是否有鎖存在,另一種是經過數據庫的排他鎖來實現分佈式鎖。
數據庫實現分佈式鎖的優勢
數據庫實現分佈式鎖的缺點
會有各類各樣的問題,在解決問題的過程當中會使整個方案變得愈來愈複雜。
操做數據庫須要必定的開銷,性能問題須要考慮。
相比較於基於數據庫實現分佈式鎖的方案來講,基於緩存來實如今性能方面會表現的更好一點。
目前有不少成熟的緩存產品,包括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集羣的同步策略是須要時間的,有可能A線程setNX成功後拿到鎖,可是這個值尚未更新到B線程執行setNX的這臺服務器,那就會產生併發問題。
redis的做者Salvatore Sanfilippo,提出了Redlock算法,該算法實現了比單一節點更安全、可靠的分佈式鎖管理(DLM)。
Redlock算法假設有N個redis節點,這些節點互相獨立,通常設置爲N=5,這N個節點運行在不一樣的機器上以保持物理層面的獨立。
算法的步驟以下:
可是,有一位分佈式的專家寫了一篇文章《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)。
總結專家關於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的數據,這樣,分區、超時、衝突等問題都不會存在。因此爲了保證分佈式鎖的正確性,我以爲使用強一致性的分佈式協調服務能更好的解決問題。
問題又來了,失效時間我設置多長時間爲好?如何設置的失效時間過短,方法沒等執行完,鎖就自動釋放了,那麼就會產生併發問題。若是設置的時間太長,其餘獲取鎖的線程就可能要平白的多等一段時間。
這個問題使用數據庫實現分佈式鎖一樣存在。
對於這個問題目前主流的作法是每得到一個鎖時,只設置一個很短的超時時間,同時起一個線程在每次快要到超時時間時去刷新鎖的超時時間。在釋放鎖的同時結束這個線程。如redis官方的分佈式鎖組件redisson,就是用的這種方案。
使用緩存實現分佈式鎖的優勢
使用緩存實現分佈式鎖的缺點
基於zookeeper臨時有序節點能夠實現的分佈式鎖。
大體思想即爲:每一個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個惟一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只須要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除便可。同時,其能夠避免服務宕機致使的鎖沒法釋放,而產生的死鎖問題。
來看下Zookeeper能不能解決前面提到的問題。
問題又來了,咱們知道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是否知足因果一致性,須要看客戶端的編程方式。
不知足因果一致性的作法
知足因果一致性的作法
第二種事件監聽機制也是對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; }
使用ZK實現的分佈式鎖好像徹底符合了本文開頭咱們對一個分佈式鎖的全部指望。可是,其實並非,Zookeeper實現的分佈式鎖其實存在一個缺點,那就是性能上可能並無緩存服務那麼高。由於每次在建立鎖和釋放鎖的過程當中,都要動態建立、銷燬瞬時節點來實現鎖功能。ZK中建立和刪除節點只能經過Leader服務器來執行,而後將數據同不到全部的Follower機器上。
使用Zookeeper實現分佈式鎖的優勢
使用Zookeeper實現分佈式鎖的缺點