Memcached應用總結

memcached是一款高性能的分佈式緩存系統,憑藉其簡單方便的操做,穩定可靠的性能普遍應用於互聯網應用中,網上關於memcached介紹的資料也不少,最經典的資料就是《memcached全面剖析》這個文檔,原文連接:http://gihyo.jp/dev/feature/01/memcached/0001,中文翻譯網上不少:http://tech.idv2.com/2008/08/17/memcached-pdf/,這個文檔寫的很好,也很容易讀懂。接下來我主要去總結一些常見應用場景問題以及解決辦法。html

1. 緩存的存儲設計

按應用場景的不一樣通常有如下兩種設計方案:前端

  • **方案一:**把數據庫的SQL查詢結果緩存到memcached,讀取數據的時候優先從memcached讀取,擋住數據庫查詢請求。java

    優勢:咱們能夠在開發框架上作一些統一的緩存處理,對業務開發透明,減小對業務邏輯代碼的侵入。這種狀況下緩存的預熱也比較方便,咱們能夠藉助數據庫的存儲日誌(eg:mysql的binlog)來預熱緩存。mysql

    缺點:這種方式有個隱患就是若是前端的一次請求須要涉及到多個SQL查詢結果,這時候memcached須要取屢次數據,在高併發的狀況下網絡io的開銷和memcached的併發壓力有可能成爲瓶頸。git

  • **方案二:**把業務處理的最終結果進行緩存,客戶端來請求時能夠直接返回這個緩存的結果。github

    優勢:能夠快速返回數據,只取一次memcache就能夠了,減小了網絡io消耗和運算處理消耗。redis

    缺點:須要在業務邏輯裏顯式處理緩存,同時存儲的數據結構較複雜,當咱們有數據更新時,從新生成緩存會比較麻煩。這種狀況比較適用於計算密集型的高併發應用場景。sql

2. 緩存更新策略

兩種常見的方案,也各有優缺點和應用場景:數據庫

  • **方案一:**懶惰式加載,客戶端先查詢memcached,若是命中,返回結果,若是沒命中(沒有數據或已通過期),則從數據庫中加載最新數據,並寫回到memcached中,最後返回結果。後端

    優勢:使用方便,簡單;

    缺點:高併發的狀況下若是緩存失效,將對後端數據庫形成瞬時壓力。固然,咱們能夠在應用里加鎖來控制併發,可是這樣也會對應用程序形成影響。

  • **方案二:**主動更新策略,緩存裏的數據永不失效,當有數據更新的時候,由單獨程序來更新這個緩存。

    優勢:緩存數據老是可靠的(沒有LRU的狀況下),前端能夠快速響應,後端的數據庫也沒有併發查詢的壓力。

    缺點:程序結構變複雜了,須要維護單獨的程序來完成更新,兩套程序要共享一套緩存配置。(ps:其實有一些業務場景原本就是這樣的,好比門戶網站的內容發佈系統和網站系統就須要共享一份數據,一個負責寫數據,一個負責展現數據)

3. 批量刪除(或更新)問題

在memcached中,咱們的絕大部分操做都是基於單個key的add/set/del/get操做,用起來很方便,可是呢,有些時候咱們會碰到批量刪除(或更新)的問題。好比某手機App應用由於出現了敏感內容,網絡監管部門要求刪除全部跟這條內容有關的信息,這個時候由於手機機型、版本不一樣,這個內容在緩存裏的key有多種多樣。咱們不能方便地拿到全部的key,或者能夠枚舉出全部的key,可是memcached並不支持批量刪除操做,這就麻煩了,怎麼解決這種問題呢?下面我以某門戶網站刪除敏感新聞來舉例,咱們假設每條新聞都有不少維度的內容,新聞以newsid標識,每一個維度以prop來老相識,再加一個通用前綴,這樣,完整的key應該是這樣的格式:key_{newsid}_{prop}

  • 方案一:

    用一個單獨的集合(Set)把一類key維護起來。當須要批量刪除(或更新)時只須要取出這個集合裏的全部key進行相應的操做便可。這樣作起來比較簡單:

    首先,咱們往memcached裏面添加一個新的k,v時,就往那個set里加一個key,好比一條新聞在memcached裏面有下面這些<k,v>對:

    key_{newsid}_{prop1}:value1
      key_{newsid}_{prop2}:value2
      key_{newsid}_{prop3}:value3
      ……
      key_{newsid}_{propn}:valuen

    在咱們的集合裏面,就要存放全部跟這條新聞有關key的集合:

    keyset_{newsid}:key_{newsid}_{prop1},key_{newsid}_{prop2},……,key_{newsid}_{propn})

    這樣,當咱們要清除這條新聞的緩存時,就能夠取出這個key的集合,而後遍歷這些key,到memcached裏面逐個刪除,這樣就達到了批量刪除的目的。

    在這裏,咱們提到的這個key set具體怎麼存放和維護呢?

    一種方式是,在memcached裏面把全部key用逗號拼接成一個大字符串構成keyset的value或者藉助開發語言提供的集合結構(set)來組織數據,系列化到memcached中。

    另外一種方式是,藉助更方便的存儲結構來保存這個key,好比redis的set結構,固然了,這種方式並不推薦,會給現有系統帶來複雜度。

  • 方案二:

    經過動態更新key的方式來實現,這種方式是給每個key都在原來key的基礎上加一個版本號來組成,當須要批量刪除或更新時只需升級版本號便可,具體怎麼作呢?

    首先,咱們在memcached給這條新聞維護一個版本號,這樣:

    key_version_{newsid}:v1.0 (版本號能夠用時間戳或其它任何有意義的內容代替)  
      // 僞代碼
      $memcacheClient->setVersion(key_version{newsid}, "v1.0");

    而後,當咱們要保存或讀取這條新聞相關的數據時,先取出這個版本號來生成新的key,以下:

    //僞代碼
      $version = getVersion(key_version_{newsid});
      $key = "key_{newsid}_{prop}_" + $version;

    再用這個新的key來保存(或讀取)真正的內容,這樣在memcached裏面保存的跟這條新聞有關的<k,v>對就是下面這樣了:

    key_{newsid}_{prop1}_v1.0:value1  
      key_{newsid}_{prop2}_v1.0:value2
      key_{newsid}_{prop3}_v1.0:value3
      ……
      key_{newsid}_{propn}_v1.0:valuen

    當咱們須要刪除(或更新)這條新聞相關的全部key時,只須要升級版本號便可,以下:

    //僞代碼
      $memcacheClient->updateVersion(key_version_{newsid},"v2.0");

    這樣的話,當咱們下次訪問這條新聞的緩存時,因爲版本號升級,新的key下全部內容都爲空,須要從數據庫加載新的內容,或者是返回空的結果。而舊的key在過時時間到了之後也就能夠回收利用了。這樣就達到了咱們批量刪除或更新的目的。

上面提到的兩種方案其實都比較簡單和實用,固然也各有缺點,方案一的key set維護須要額外的消耗,方案二的老版本數據不能及時清理,形成緩存垃圾。咱們在實際應用場景中能夠靈活選擇,二者在效果上其實不會有太大區別。

4. 故障轉移和擴容的問題

memcached它不是一個分佈式的系統,嚴格來講是個單點系統,所謂的分佈式只是藉助客戶端來實現的。因此它沒有那些開源分佈式系統那樣的高可用性,咱們這裏來討論一下memcached怎麼去避免單點故障,以及在線擴容的問題。(ps:memcached作得真省事兒,最大的特色就是簡單,好多輔助功能都要依賴於客戶端本身去實現)。

  • 一致性哈希:好吧,這應該算是最簡單常見的一種機制了,依賴於一致性哈希的特色,節點故障或擴容加節點時對集羣影響較小,基本上能夠知足大部分應用場景了。可是要注意:節點調整的最初一段時間內,會有一部分緩存丟失,穿透到後端的數據庫上,在高併發的應用裏,要作好併發控制,以避免對數據庫形成壓力。
  • 雙寫機制:客戶端維護兩個集羣,每次更新數據的時候同時更新兩份,讀取的時候隨機(或固定)讀取一份,這種狀況下集羣的可用性和穩定性是很高的,能夠無痛變動,節點故障或擴容對緩存和後端數據庫都沒有影響。固然,這樣作也是有代價的:一是兩份數據的一致性問題,不過對緩存來講,這種極少數的不一致狀況是能夠容忍的;另外一個是內存浪費的問題,經過冗餘一份數據來減小故障率,代價仍是挺大的,並不適合大型的互聯網應用。
  • Twemproxy:這是twitter開源的一個代理程序,能夠給redis和memcached做代理,有了這個東西能夠減小好多維護成本(主要是客戶端的)。對於故障轉移和在線擴容也很方便。具體能夠參考:https://github.com/twitter/twemproxy

5. 與優化有關的一些小細節

  • 批量讀取(multiget):有些較複雜的業務請求可能一次請求要進行屢次memcached操做,其中的網絡往返的消耗以及對memcached節點施加的併發壓力仍是比較可觀的,這種狀況下咱們能夠考慮進行批量讀取來減小網絡io往返的次數,一次把數據返回,同時還能減輕客戶端的業務處理邏輯。

    這裏有一個著名的multiget無底洞問題,在facebook的應用中發現了這個問題,請參考:http://highscalability.com/blog/2009/10/26/facebooks-memcached-multiget-hole-more-machines-more-capacit.html,這篇文章中已經提出瞭解決方案。但其實咱們也能夠考慮把multiget的key分佈到一個節點上,來避免這個問題,這樣就須要本身定製memcache的客戶端,按必定的規則(好比:相同的前綴)把一類key分佈到同一個節點上,來避免這個問題,同時這樣也能夠提升性能,不用在多個節點之間等待數據。

  • 改變系列化方式:不使用java的對象序列化方式(哈哈,我這裏只針對java來講),本身實現序列化,把要緩存的對象序列化成字節數組或者string進行保存。這樣在內存節省和網絡傳輸上都有不錯的效果。

  • 數據預熱:一些場景下咱們須要爲應用預熱緩存數據(好比節點擴容須要從新分佈數據),在前面說緩存設計的時候提出過,能夠藉助數據庫的更新日誌來預熱緩存,這主要依賴於緩存的內容是跟數據庫存儲一致。其它狀況下咱們能夠考慮在現有緩存前面擋一層空內容的集羣節點,逐步把舊緩存讀取到新緩存集羣中來達到數據預熱的目的,這樣作就是有一點麻煩,須要應用端配合。

  • 增加因子:合理調整memcached的增加因子,能夠有效控制內存的浪費。

  • 空結果的處理:有些場景下咱們數據庫裏沒有查到數據,緩存裏也是空的,這時候須要在緩存裏存放一個短時效的空結果來擋住前端的頻繁請求,以避免對數據庫形成壓力。

memcached的使用其實很是簡單,性能也很出色,上面這些就是咱們在實際業務開發中會碰到的一些場景,根據實際場景去選擇合適的解決方案,能夠給之後的開發維護帶來很多便利。

推薦閱讀:

HTTP協議理解與應用總結

MySQL使用與優化總結

相關文章
相關標籤/搜索