1. 程式人生 > >分散式鎖的幾種使用方式(redis、zookeeper、資料庫)

分散式鎖的幾種使用方式(redis、zookeeper、資料庫)

Q:一個業務伺服器,一個數據庫,操作:查詢使用者當前餘額,扣除當前餘額的3%作為手續費

  • synchronized
  • lock
  • db lock

Q:兩個業務伺服器,一個數據庫,操作:查詢使用者當前餘額,扣除當前餘額的3%作為手續費

  • 分散式鎖

我們需要怎麼樣的分散式鎖?

  • 可以保證在分散式部署的應用叢集中,同一個方法在同一時間只能被一臺機器上的一個執行緒執行。

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

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

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

  • 有高可用的獲取鎖和釋放鎖功能

  • 獲取鎖和釋放鎖的效能要好

一、基於資料庫實現的分散式鎖

基於表實現的分散式鎖
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操作,一旦插入失敗就會直接報錯。沒有獲得鎖的執行緒並不會進入排隊佇列,要想再次獲得鎖就要再次觸發獲得鎖操作。

  • 這把鎖是非重入的,同一個執行緒在沒有釋放鎖之前無法再次獲得該鎖。因為資料中資料已經存在了。

  • 這把鎖是非公平鎖,所有等待鎖的執行緒憑運氣去爭奪鎖。

當然,我們也可以有其他方式解決上面的問題。

  • 資料庫是單點?搞兩個資料庫,資料之前雙向同步。一旦掛掉快速切換到備庫上。

  • 沒有失效時間?只要做一個定時任務,每隔一定時間把資料庫中的超時資料清理一遍。

  • 非阻塞的?搞一個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;
}

在查詢語句後面增加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); 
}

以上實現方式同樣存在幾個問題:

  • 1、單點問題。

  • 2、這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在redis中,其他執行緒無法再獲得到鎖。

  • 3、這把鎖只能是非阻塞的,無論成功還是失敗都直接返回。

  • 4、這把鎖是非重入的,一個執行緒獲得鎖之後,在釋放鎖之前,無法再次獲得該鎖,因為使用到的key在redis中已經存在。無法再執行setNX操作。

  • 5、這把鎖是非公平的,所有等待的執行緒同時去發起setNX操作,運氣好的執行緒能獲取鎖。

當然,同樣有方式可以解決。

  • 現在主流的快取服務都支援叢集部署,通過叢集來解決單點問題。

  • 沒有失效時間?redis的setExpire方法支援傳入失效時間,到達時間之後資料會自動刪除。

  • 非阻塞?while重複執行。

  • 非可重入?在一個執行緒獲取到鎖之後,把當前主機資訊和執行緒資訊儲存起來,下次再獲取之前先檢查自己是不是當前鎖的擁有者。

  • 非公平?線上程獲取鎖之前先把所有等待的執行緒放入一個佇列中,然後按先進先出原則獲取鎖。

redis叢集的同步策略是需要時間的,有可能A執行緒setNX成功後拿到鎖,但是這個值還沒有更新到B執行緒執行setNX的這臺伺服器,那就會產生併發問題。

redis的作者Salvatore Sanfilippo,提出了Redlock演算法,該演算法實現了比單一節點更安全、可靠的分散式鎖管理(DLM)。

Redlock演算法假設有N個redis節點,這些節點互相獨立,一般設定為N=5,這N個節點執行在不同的機器上以保持物理層面的獨立。

演算法的步驟如下:

  • 1、客戶端獲取當前時間,以毫秒為單位。
  • 2、客戶端嘗試獲取N個節點的鎖,(每個節點獲取鎖的方式和前面說的快取鎖一樣),N個節點以相同的key和value獲取鎖。客戶端需要設定介面訪問超時,介面超時時間需要遠遠小於鎖超時時間,比如鎖自動釋放的時間是10s,那麼介面超時大概設定5-50ms。這樣可以在有redis節點宕機後,訪問該節點時能儘快超時,而減小鎖的正常使用。
  • 3、客戶端計算在獲得鎖的時候花費了多少時間,方法是用當前時間減去在步驟一獲取的時間,只有客戶端獲得了超過3個節點的鎖,而且獲取鎖的時間小於鎖的超時時間,客戶端才獲得了分散式鎖。
  • 4、客戶端獲取的鎖的時間為設定的鎖超時時間減去步驟三計算出的獲取鎖花費時間。
  • 5、如果客戶端獲取鎖失敗了,客戶端會依次刪除所有的鎖。
    使用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個客戶端都在處理共享資源的情況。

lock_unsafe_lock

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

lock_fencing-token

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

  • 1、client 1從A、B、C成功獲取鎖,從D、E獲取鎖網路超時。
  • 2、節點C的時鐘不準確,導致鎖超時。
  • 3、client 2從C、D、E成功獲取鎖,從A、B獲取鎖網路超時。
  • 4、這樣client 1和client 2都獲得了鎖。

總結專家關於Redlock不可用的兩點:

  • 1、GC等場景可能隨時發生,並導致在客戶端獲取了鎖,在處理中超時,導致另外的客戶端獲取了鎖。專家還給出了使用自增token的解決方法。
  • 2、演算法依賴本地時間,會出現時鐘不準,導致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是否滿足因果一致性,需要看客戶端的程式設計方式。

不滿足因果一致性的做法

    1. A程序向Zookeeper的/z寫入一個數據,成功返回
    1. A程序通知B程序,A已經修改了/z的資料
    1. B讀取Zookeeper的/z的資料
    1. 由於B連線的Zookeeper的伺服器有可能還沒有得到A寫入資料的更新,那麼B將讀不到A寫入的資料

滿足因果一致性的做法

    1. B程序監聽Zookeeper上/z的資料變化
    1. A程序向Zookeeper的/z寫入一個數據,成功返回前,Zookeeper需要呼叫註冊在/z上的監聽器,Leader將資料變化的通知告訴B
    1. B程序的事件響應方法得到響應後,去取變化的資料,那麼B一定能夠得到變化的值
    1. 這裡的因果一致性提現在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; 
}

使用ZK實現的分散式鎖好像完全符合了本文開頭我們對一個分散式鎖的所有期望。但是,其實並不是,Zookeeper實現的分散式鎖其實存在一個缺點,那就是效能上可能並沒有快取服務那麼高。因為每次在建立鎖和釋放鎖的過程中,都要動態建立、銷燬瞬時節點來實現鎖功能。ZK中建立和刪除節點只能通過Leader伺服器來執行,然後將資料同不到所有的Follower機器上。

使用Zookeeper實現分散式鎖的優點

有效的解決單點問題,不可重入問題,非阻塞問題以及鎖無法釋放的問題。實現起來較為簡單。

使用Zookeeper實現分散式鎖的缺點

效能上不如使用快取實現分散式鎖。 需要對ZK的原理有所瞭解。

三種方案的比較

從理解的難易程度角度(從低到高)

資料庫 > 快取 > Zookeeper

從實現的複雜性角度(從低到高)

Zookeeper > 快取 > 資料庫

從效能角度(從高到低)

快取 > Zookeeper >= 資料庫

從可靠性角度(從高到低)

Zookeeper > 快取 > 資料庫\