測試一陣子的專案小明接到回報,
系統明明有寫設定快取機制, 並且快取內容保存10 秒,
按照道理說, 系統一分鐘讀取資料庫最多應該是6 次,
但是資料庫追蹤紀錄顯示系統一分鐘內讀取了400次以上.
小明所寫的快取程式片段如下
public class FoodList
{
public List<Data> GetFoodList(DateTime date)
{
var data = (List<Data>)cache["foodList"];
if( data == null )
{
data = ReadFoodListFromDatabase();
var policy = new CacheItemPolicy();
policy.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10);
cache.Set("foodList", data, policy);
}
return data;
}
}
觀察到這段快取程式, 基本上是沒有問題, 快取機制並不是無效的.
但是作為應用在生產正式環境當中, 沒有什麼是如此簡單.
首先, 當在正式環境中通常會有很多個使用者同時在線上,
故有可能在同一個時間內, 系統同時得到多個請求(Request).
假設有10 個使用者同時要求讀取GetFoodList 資料.
然後這10 個請求(Request)就會同時呼叫GetFoodList() 方法.
當10 個請求(Request)在GetFoodList() 取得過期快取資料的時候,
這時候10 個請求(Request)就會同時執行ReadFoodListFromDatabase().
這10 次取得資料動作只有一次是必要的, 其餘9 次將取得相同結果覆寫同一Cache,
平白消耗資源.
所以雖然專案程式有設定按照小明的期望系統一分鐘讀取資料庫最多應該是6 次,
但實際上有可能卻是6 次以上.
所以我們該怎麼做, 才能讓專案程式一分鐘最多讀取資料庫6 次呢?
於是小明利用C# lock 陳述式, 修改程式碼為
public List<Data> GetFoodList(DateTime date)
{
List<Data> data = null;
lock(_sync)
{
data = (List<Data>)cache["foodList"];
if( data == null )
{
data = ReadFoodListFromDatabase();
var policy = new CacheItemPolicy();
policy.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10);
cache.Set("foodList", data, policy);
}
}
return data;
}
有了這個, 當你試圖取得資料的時候,
如果同一個資料正在被另一個線程創建, 你將等待另一個完成.
然後你將獲得由另一個線程創建的已快取的資料.
這個做法有缺點,
假設從數據庫中獲取資料需要10秒鐘,
當你試圖取得資料的時候,
如果同一個資料正在被另一個線程創建, 你將等待另一個完成.
也就是說你就要等待10 秒之後才能拿到由另一個線程創建的已快取的資料.
所以對高乘載壓力的系統來說這效能並不好.
現在我們知道了缺點, 讓我們繼續尋找更好的解決方案.
首先我們可以另外建立讀取資料庫的任務(Task), 並利用AutoResetEvent 通知這個任務(Task) 去檢查快取資料
var task = new Task(()=> {
while( _cacheCheckSign.WaitOne() )
{
if( 快取資料過期 ) {
_data = ReadFoodListFromDatabase();
}
}
}).Start();
然後在GetFoodList() 方法內,
只發出一個訊號通知給任務(Task) 去檢查快取工作,
不管快取資料有沒有過期, 直接回傳快取資料.
AutoResetEvent _cacheCheckSign = new AutoResetEvent(false);
public List<Data> GetFoodList(DateTime date)
{
_cacheCheckSign.Set();
return _data;
}
改用這方法, 即使同時三條Thread 呼叫GetFoodList() 只會觸發一次讀取資料庫的動作,
可減少高承載系統產生重複讀取資料的壓力.