1. 程式人生 > >應對Memcached快取失效,導致高併發查詢DB的幾種思路

應對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。

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();
}
RefreshCacheManager,使用者只需要關心這這兩個介面函式:
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);
}
其中autoRetryGet函式如果get到是null,內部會自動重試4次,每次間隔500ms。 RefreshCacheManager內部自動處理資料快過期,重新重新整理到memcached的邏輯。

總結:

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

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

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