Memcached應用總結

jopen 11年前發布 | 35K 次閱讀 緩存服務器 memcached

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

1. 緩存的存儲設計

按應用場景的不同一般有以下兩種設計方案:

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

    優點:我們可以在開發框架上做一些統一的緩存處理,對業務開發透明,減少對業務邏輯代碼的侵入。這種情況下緩存的預熱也比較方便,我們可以借助數據庫的存儲日志(eg:mysql的binlog)來預熱緩存。

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

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

    優點:可以快速返回數據,只取一次memcache就可以了,減少了網絡io消耗和運算處理消耗。

    缺點:需要在業務邏輯里顯式處理緩存,同時存儲的數據結構較復雜,當我們有數據更新時,重新生成緩存會比較麻煩。這種情況比較適用于計算密集型的高并發應用場景。

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里面有下面這些 對:

    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里面保存的跟這條新聞有關的 對就是下面這樣了:

    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:這是推ter開源的一個代理程序,可以給redis和memcached作代理,有了這個東西可以減少好多維護成本(主要是客戶端的)。對于故障轉移和在線擴容也很方便。具體可以參考:https://github.com/推ter/twemproxy

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

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

    這里有一個著名的multiget無底洞問題,在非死book的應用中發現了這個問題,請參考:http://highscalability.com/blog/2009/10/26/非死books-memcached-multiget-hole-more-machines-more-capacit.html,這篇文章中已經提出了解決方案。但其實我們也可以考慮把multiget的key分布到一個節點上,來避免這個問題,這樣就需要自己定制memcache 的客戶端,按一定的規則(比如:相同的前綴)把一類key分布到同一個節點上,來避免這個問題,同時這樣也可以提高性能,不用在多個節點之間等待數據。

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

  • 數據預熱:一些場景下我們需要為應用預熱緩存數據(比如節點擴容需要重新分布數據),在前面說緩存設計的時候提出過,可以借助數據庫的更新日志來預熱緩存,這主要依賴于緩存的內容是跟數據庫存儲一致。其它情況下我們可以考慮在現有緩存前面擋一層空內容的集群節點,逐步把舊緩存讀取到新緩存集群中來達到數據預熱的目的,這樣做就是有一點麻煩,需要應用端配合。
  • 增長因子:合理調整memcached的增長因子,可以有效控制內存的浪費。
  • 空結果的處理:有些場景下我們數據庫里沒有查到數據,緩存里也是空的,這時候需要在緩存里存放一個短時效的空結果來擋住前端的頻繁請求,以免對數據庫造成壓力。

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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!