天天看點

應對Memcached緩存失效,導緻高并發查詢DB的幾種思路

最近看到nginx的合并回源,這個和下面的思路有點像。不過nginx的思路還是在控制緩存失效時的并發請求,而不是當緩存快要失效時,及時地更新緩存。

nginx合并回源,參考:http://blog.csdn.net/brainkick/article/details/8570698

update: 2015-04-23

======================

當memcached緩存失效時,容易出現高并發的查詢db,導緻db壓力驟然上升。

這篇blog主要是探讨如何在緩存将要失效時,及時地更新緩存,而不是如何在緩存失效之後,如何防止高并發的db查詢。

個人認為,當緩存将要失效時,及時地把新的資料刷到memcached裡,這個是解決緩存失效瞬間高并發查db的最好方法。那麼如何及時地知道緩存将要失效?

解決這個問題有幾種思路:

比如一個key是aaa,失效時間是30s。

這種方法有個缺點是,有些業務的key可能是變化的,不确定的。

而且不好界定哪些資料是應該查詢出來放到緩存中的,難以區分冷熱資料。

這種方式不太靠譜,不多讨論。而且如果是多個web伺服器的話,還是有可能有并發的操作。

當get得到資料時,如果目前時間 - 過期時間 > 5s,則背景啟動一個任務去查詢db,更新緩存。

當然,這裡的背景任務必須保證同一個key,隻有一個線程在執行查詢db的任務,不然這個還是高并發查詢db。

缺點是要把過期時間和value合在一起序列化,取出資料後,還要反序列化。很不友善。

網上大部分文章提到的都是前面兩種方式,有少數文章提到第3種方式。下面提出一種基于兩個key的方法:

比如key是aaa,設定失效時間為30s,則另一個key為expire_aaa,失效時間為25s。

在取資料時,用multiget,同時取出aaa和expire_aaa,如果expire_aaa的value == null,則背景啟動一個任務去查詢db,更新緩存。和上面類似。

對于背景啟動一個任務去查詢db,更新緩存,要保證一個key隻有一個線程在執行,這個如何實作?

對于同一個程序,簡單加鎖即可。拿到鎖的就去更新db,沒拿到鎖的直接傳回。

對于叢集式的部署的,如何實作隻允許一個任務執行?

這裡就要用到memcached的add指令了。

add指令是如果不存在key,則設定成功,傳回true,如果已存在key,則不存儲,傳回false。

當get expired_aaa是null時,則add expired_aaa 過期時間由自己靈活處理。比如設定為3秒。

如果成功了,再去查詢db,查到資料後,再set expired_aaa為25秒。set aaa 為30秒。

綜上所述,來梳理下流程:

比如一個key是aaa,失效時間是30s。查詢db在1s内。

put資料時,設定aaa過期時間30s,設定expire_aaa過期時間25s;

get資料時,multiget  aaa 和 expire_aaa,如果expired_aaa對應的value != null,則直接傳回aaa對應的資料給使用者。如果expire_aaa傳回value == null,則背景啟動一個任務,嘗試add expire_aaa,并設定逾時過間為3s。這裡設定為3s是為了防止背景任務失敗或者阻塞,如果這個任務執行失敗,那麼3秒後,如果有另外的使用者通路,那麼可以再次嘗試查詢db。如果add執行成功,則查詢db,再更新aaa的緩存,并設定expire_aaa的逾時時間為25s。

update:2014-06-29

最近重新思考了下這個問題。發現第4種兩個key的辦法比較耗memcached的記憶體,因為key數翻倍了。結合第3種方式,重新設計了下,思路如下:

仍然使用兩個key的方案:

    key

    __load_{key}

其中,__load_{key} 這個key相當于一個鎖,隻允許add成功的線程去更新資料,而這個key的逾時時間是比較短的,不會一直占用memcached的記憶體。

在set 到memcached的value中,加上一個時間,(time, value),time是memcached上的key未來會過期的時間,并不是目前系統時間。

當get到資料時,檢查時間是否快要逾時: time - now < 5 * 1000,假定設定了快要逾時的時間是5秒。

 * 如果是,則背景啟動一個新的線程:

 *     嘗試 add __load_{key},

 *     如果成功,則去加載新的資料,并set到memcached中。

 *  原來的線程直接傳回value給調用者。

按上面的思路,用xmemcached封裝了下:

dataloader,使用者要實作的加載資料的回調接口:

refreshcachemanager,使用者隻需要關心這這兩個接口函數:

其中autoretryget函數如果get到是null,内部會自動重試4次,每次間隔500ms。

refreshcachemanager内部自動處理資料快過期,重新重新整理到memcached的邏輯。

詳細的封裝代碼在這裡:https://gist.github.com/hengyunabc/cc57478bfcb4cd0553c2

我個人是傾向于第5種方式的,因為很簡單,直覺。比第4種方式要節省記憶體,而且不用mget,在使用memcached叢集時不用擔心出麻煩事。

這種兩個key的方式,還有一個好處,就是資料是自然冷熱适應的。如果是冷資料,30秒都沒有人通路,那麼資料會過期。

如果是熱門資料,一直有大流量通路,那麼資料就是一直熱的,而且資料一直不會過期。

繼續閱讀