MemoryCache 使用不當(dāng)導(dǎo)致的一個(gè) BUG
Intro
前幾天發(fā)現(xiàn)代碼里的一個(gè) BUG,原因是 MemoryCache 使用不當(dāng),可以對(duì)于很多人來說可能都知道,但還是想分享記錄一下,避免以后寫出同樣的 BUG
Sample
直接來看下面的示例吧
- await using var services = new ServiceCollection()
- .AddMemoryCache()
- .BuildServiceProvider();
- Console.WriteLine("----- Bad -----");
- GetValidValues(5).Dump();
- GetValidValues(8).Dump();
- List<int> GetValidValues(int threhold)
- {
- var memoryCache = services.GetRequiredService<IMemoryCache>();
- var values = memoryCache.GetOrCreate("test1", entry =>
- {
- return Enumerable.Range(1, 10).ToList();
- });
- values.RemoveAll(x => x > threhold);
- return values;
- }
上面的 Dump 是一個(gè)擴(kuò)展方法就是把 list 內(nèi)的元素輸出出來,實(shí)現(xiàn)如下:
- public static void Dump(this List<int> values)
- {
- var value = string.Join(",", values);
- Console.WriteLine(value);
- }
好了,來想一下上面的輸出結(jié)果會(huì)是什么吧,期望的結(jié)果應(yīng)該是每次都輸出小于等于輸入的值,實(shí)際是什么樣的呢?實(shí)際輸出結(jié)果如下:
Fix
可以看到第二次輸出的結(jié)果和我們的期望不同,之所以會(huì)出現(xiàn)上面的問題是因?yàn)?MemoryCache 的對(duì)象是直接保存在內(nèi)存中的對(duì)象,緩存不發(fā)生變化時(shí)每次都是返回同一個(gè)對(duì)象,如果發(fā)生修改后面再獲取的就是修改后的狀態(tài)了,所以正確的做法應(yīng)該要返回一個(gè)新的對(duì)象而不是修改原來的對(duì)象,一個(gè)修改方法如下:
- List<int> GetValidValues(int threhold)
- {
- var memoryCache = services.GetRequiredService<IMemoryCache>();
- var values = memoryCache.GetOrCreate("test", entry =>
- {
- return Enumerable.Range(1, 10).ToList();
- });
- return values.Where(v => v <= threhold).ToList();
- }
修改后的輸出結(jié)果如下:
More
MemoryCache 背后實(shí)際是一個(gè) ConcurrentDictionary,value 是一個(gè)帶著過期時(shí)間的對(duì)象 CacheEntry,
在不過期,沒有發(fā)生變化的時(shí)候每次返回都是同一個(gè)對(duì)象,作為緩存對(duì)象,應(yīng)該進(jìn)行只讀操作,不應(yīng)該修改緩存的對(duì)象,如果需要修改則應(yīng)創(chuàng)建新的對(duì)象,而非使用原來的對(duì)象。
References
https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs#L26
https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs
https://github.com/WeihanLi/SamplesInPractice/blob/master/MemoryCacheSample/Program.cs