MemoryCache GetOrCreateValue 注意事項

大家可以想一下什麼情況下會需要使用 memory cache?

我自己的話,一些比較不常變動的設定值,可以放在記憶體內做快取,定期地去讓它失效,再重新拿 database 的值放回快取更新。

這篇介紹的注意事項有一些特定狀況,並不是所有人都會碰到。這樣寫的情境乍看很像對,但測試起來又不太對,我想應該是對於內建方法GetOrCreate有些誤解。

下面這段程式:

  1. 傳入 string key 希望取得 value
  2. 若 key 值為 key1 則回傳 111,若為其他則會發生錯誤,並且回傳預設值 999。這邊是模擬用其他 key 值查詢 cache 時,遇到不可預期的錯誤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static class MemoryCacheHelper
{
private static readonly IMemoryCache _memoryCache = new MemoryCache(new MemoryCacheOptions());

public static async Task<int> GetCacheValue(string key)
{
return await _memoryCache.GetOrCreateAsync(key, async data =>
{
try
{
// Get kv data from database
if (key == "key1") return await Task.FromResult(111);

throw new Exception("Unexpected error");
}
catch (Exception)
{
// log error
}
return 999;
}
);
}
}

第一個測試中,傳入 key1,預期會收到 111的結果。
第二個測試,傳入 keyX,模擬取值遇到例外狀況,然後回傳預設值。這次錯誤可以先略過,並且回傳預設值,那麼下一次呼叫快取呢?如果發現快取是空的,那麼理所當然又要去資料庫撈一次。

這時候問題就來了,第二次測試的 key 值 keyX 其實已經存在在 memory cache 內了,所以第三個測試,再次傳入 keyX 只會取得 999,但這個 999 並非來自例外狀況的預設回傳值且不再會 reload database 的資料,直到 keyX/999 這組 kv 自然失效(expire)。

實際執行結果..

1
2
3
4
5
6
7
var key1Value = await MemoryCacheHelper.GetCacheValue("key1");
var keyXValue = await MemoryCacheHelper.GetCacheValue("keyX");
var keyXValue_2ndCall = await MemoryCacheHelper.GetCacheValue("keyX");

Console.WriteLine($"key1_value:{key1Value}");
Console.WriteLine($"keyX_value:{keyXValue}");
Console.WriteLine($"keyX_Value_2ndCall:{keyXValue_2ndCall}");

Result

1
2
3
4
/usr/local/share/dotnet/dotnet /Users/pattri.chou/Documents/private/ConsoleApp1/ConsoleApp1/bin/Debug/netcoreapp3.1/ConsoleApp1.dll
key1_value:111
keyX_value:999
keyX_Value_2ndCall:999

這個寫法看似簡潔,一旦遇到例外狀況,預設值就會被當成該 key 的 value,直到 expire 為止,才會再重新進入 database 或者其他實體的 instance 取出正確的對應關係,因此一定要仔細評估商業需求或者使用情境是否能接受這樣的暫時性錯誤,當然,要不信邪也可以賭賭看完全不會遇到錯誤啦!要賭的話我寧願不要用 try catch 把 exception 包起來,直接噴出 exception,讓錯誤的 k/v 不要寫入快取更好。

  • 作者: MingYi Chou
  • 版權聲明: 轉載不用問,但請註明出處!本網誌均採用 BY-NC-SA 許可協議。