出處: hollisweb
目前幾乎不少大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。分佈式的CAP理論告訴咱們「任何一個redis
分佈式系統都沒法同時知足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時知足兩項。」因此,很算法
多系統在設計之初就要對這三者作出取捨。在互聯網領域的絕大多數的場景中,都須要犧牲強一致性來換取系統的高可用性,系統每每只須要保證「最數據庫
終一致性」,只要這個最終時間是在用戶能夠接受的範圍內便可。緩存
在不少場景中,咱們爲了保證數據的最終一致性,須要不少的技術方案來支持,好比分佈式事務、分佈式鎖等。有的時候,咱們須要保證一個方法在同服務器
一時間內只能被同一個線程執行。在單機環境中,Java中其實提供了不少併發處理相關的API,可是這些API在分佈式場景中就無能爲力了。也就是說單網絡
純的Java Api並不能提供分佈式鎖的能力。因此針對分佈式鎖的實現目前有多種方案:併發
在分析這幾種實現方案以前咱們先來想一下,咱們須要的分佈式鎖應該是怎麼樣的?(這裏以方法鎖爲例,資源鎖同理)框架
一. 基於數據庫實現分佈式鎖分佈式
1.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='鎖定中的方法';
當咱們想要鎖住某個方法時,執行如下SQL:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
由於咱們對method_name作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲
操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。
當方法執行完畢以後,想要釋放鎖的話,須要執行如下Sql:
delete from methodLock where method_name ='method_name'
上面這種簡單的實現有如下幾個問題:
一、這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會致使業務系統不可用。
二、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖。
三、這把鎖只能是非阻塞的,由於數據的insert操做,一旦插入失敗就會直接報錯。沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做。
四、這把鎖是非重入的,同一個線程在沒有釋放鎖以前沒法再次得到該鎖。由於數據中數據已經存在了。
固然,咱們也能夠有其餘方式解決上面的問題。
1.2 基於數據庫排他鎖
除了能夠經過增刪操做數據表中的記錄之外,其實還能夠藉助數據中自帶的鎖來實現分佈式的鎖。
咱們還用剛剛建立的那張數據庫表。能夠經過數據庫的排他鎖來實現分佈式鎖。 基於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()操做來釋放鎖。
這種方法能夠有效的解決上面提到的沒法釋放鎖和阻塞鎖的問題。
可是仍是沒法直接解決數據庫單點和可重入問題。
1.3 總結
總結一下使用數據庫來實現分佈式鎖的方式,這兩種方式都是依賴數據庫的一張表,一種是經過表中的記錄的存在狀況肯定當前是否有鎖存在,另一種是經過數據庫的排他鎖來實現分佈式鎖。
數據庫實現分佈式鎖的優勢: 直接藉助數據庫,容易理解。
數據庫實現分佈式鎖的缺點: 會有各類各樣的問題,在解決問題的過程當中會使整個方案變得愈來愈複雜。
操做數據庫須要必定的開銷,性能問題須要考慮。
二.基於緩存實現分佈式鎖
相比較於基於數據庫實現分佈式鎖的方案來講,基於緩存來實如今性能方面會表現的更好一點。並且不少緩存是能夠集羣部署的,能夠解決單點問題。
目前有不少成熟的緩存產品,包括Redis,memcached以及咱們公司內部的Tair。
這裏以Tair爲例來分析下使用緩存實現分佈式鎖的方案。關於Redis和memcached在網絡上有不少相關的文章,而且也有一些成熟的框架及算法能夠直接使用。
基於Tair的實現分佈式鎖在內網中有不少相關文章,其中主要的實現方式是使用TairManager.put方法來實現。
public boolean trylock(String key) { ResultCode code = ldbTairManager.put(NAMESPACE, key, "This is a Lock.", 2, 0); if (ResultCode.SUCCESS.equals(code)) return true; else return false; } public boolean unlock(String key) { ldbTairManager.invalid(NAMESPACE, key); }
以上實現方式一樣存在幾個問題:
一、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在tair中,其餘線程沒法再得到到鎖。
二、這把鎖只能是非阻塞的,不管成功仍是失敗都直接返回。
三、這把鎖是非重入的,一個線程得到鎖以後,在釋放鎖以前,沒法再次得到該鎖,由於使用到的key在tair中已經存在。沒法再執行put操做。
固然,一樣有方式能夠解決。
可是,失效時間我設置多長時間爲好?如何設置的失效時間過短,方法沒等執行完,鎖就自動釋放了,那麼就會產生併發問題。若是設置的時間太長,
其餘獲取鎖的線程就可能要平白的多等一段時間。這個問題使用數據庫實現分佈式鎖一樣存在
總結
可使用緩存來代替數據庫來實現分佈式鎖,這個能夠提供更好的性能,同時,不少緩存服務都是集羣部署的,能夠避免單點問題。而且不少緩存服務都提供了能夠用
來實現分佈式鎖的方法,好比Tair的put方法,redis的setnx方法等。而且,這些緩存服務也都提供了對數據的過時自動刪除的支持,能夠直接設置超時時間來控制鎖的釋放。
使用緩存實現分佈式鎖的優勢: 性能好,實現起來較爲方便。
使用緩存實現分佈式鎖的缺點: 經過超時時間來控制鎖的失效時間並非十分的靠譜。
三. 基於Zookeeper實現分佈式鎖
基於zookeeper臨時有序節點能夠實現的分佈式鎖。大體思想即爲:每一個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個惟一的
瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只須要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除便可。同時,其能夠避免服務宕機導
致的鎖沒法釋放,而產生的死鎖問題。
來看下Zookeeper能不能解決前面提到的問題。
Session鏈接斷開),那麼這個臨時節點就會自動刪除掉。其餘客戶端就能夠再次得到鎖。
端能夠檢查本身建立的節點是否是當前全部節點中序號最小的,若是是,那麼本身就獲取到鎖,即可以執行業務邏輯了。
時候和當前最小的節點中的數據比對一下就能夠了。若是和本身的信息同樣,那麼本身直接獲取到鎖,若是不同就再建立一個臨時的順序節點,參與排隊。
能夠直接使用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實現分佈式鎖的優勢: 有效的解決單點問題,不可重入問題,非阻塞問題以及鎖沒法釋放的問題。實現起來較爲簡單。
使用Zookeeper實現分佈式鎖的缺點 : 性能上不如使用緩存實現分佈式鎖。 須要對ZK的原理有所瞭解。
四.三種方案的比較
從理解的難易程度角度(從低到高): 數據庫 > 緩存 > Zookeeper
從實現的複雜性角度(從低到高): Zookeeper >= 緩存 > 數據庫
從性能角度(從高到低): 緩存 > Zookeeper >= 數據庫
從可靠性角度(從高到低): Zookeeper > 緩存 > 數據庫