最近看到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。
1.定期從DB裏查詢數據,再刷到memcached裏
這種方法有個缺點是,有些業務的key可能是變化的,不確定的。
而且不好界定哪些數據是應該查詢出來放到緩存中的,難以區分冷熱數據。
2.當緩存取到爲null時,加鎖去查詢DB,只允許一個線程去查詢DB
這種方式不太靠譜,不多討論。而且如果是多個web服務器的話,還是有可能有併發的操作。
3.在向memcached寫入value時,同時寫入當前機器在時間作爲過期時間
當get得到數據時,如果當前時間 - 過期時間 > 5s,則後臺啓動一個任務去查詢DB,更新緩存。
當然,這裏的後臺任務必須保證同一個key,只有一個線程在執行查詢DB的任務,不然這個還是高併發查詢DB。
缺點是要把過期時間和value合在一起序列化,取出數據後,還要反序列化。很不方便。
網上大部分文章提到的都是前面兩種方式,有少數文章提到第3種方式。下面提出一種基於兩個key的方法:
4.兩個key,一個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。
5. 時間存到Value裏,再結合add命令來保證只有一個線程去刷新數據
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,用戶要實現的加載數據的回調接口:
- public interface DataLoader {
- public <T> T load();
- }
- public class RefreshCacheManager {
- static public <T> T tryGet(MemcachedClient memcachedClient, final String key, final int expire, final DataLoader dataLoader);
- static public <T> T autoRetryGet(MemcachedClient memcachedClient, final String key, final int expire, final DataLoader dataLoader);
- }
RefreshCacheManager內部自動處理數據快過期,重新刷新到memcached的邏輯。
詳細的封裝代碼在這裏:https://gist.github.com/hengyunabc/cc57478bfcb4cd0553c2
總結:
我個人是傾向於第5種方式的,因爲很簡單,直觀。比第4種方式要節省內存,而且不用mget,在使用memcached集羣時不用擔心出麻煩事。
這種兩個key的方式,還有一個好處,就是數據是自然冷熱適應的。如果是冷數據,30秒都沒有人訪問,那麼數據會過期。
如果是熱門數據,一直有大流量訪問,那麼數據就是一直熱的,而且數據一直不會過期。
參考:http://blog.csdn.net/hengyunabc/article/details/20735701#comments