深圳幻海软件技术有限公司 欢迎您!

浅谈 MemoryCache 的原生插值方式

2023-02-28

.NET运行时内置了常用的缓存模块:MemoryCache标准的MemoryCache暴露了如下几个属性和方法:复制public int Count { get; } public void Compact(double

.NET运行时内置了常用的缓存模块:MemoryCache

标准的MemoryCache暴露了如下几个属性和方法:

public int Count { get; } 
public void Compact(double percentage); 
public ICacheEntry CreateEntry(object key); 
public void Dispose(); 
public void Remove(object key); 
public bool TryGetValue(object keyout object result); 
protected virtual void Dispose(bool disposing); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

但是你使用常规模式去插值/获取值,可能会出现意想不到的情况。

就如下这样的常规代码:

var s = new MemoryCache(new MemoryCacheOptions { }); 
var entry = s.CreateEntry("WeChatID"); 
entry.Value = "精益码农"
 
var f =  s.TryGetValue("WeChatID",out  object obj); 
 
Console.WriteLine(f); 
Console.WriteLine(obj); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

会输出如下结果:

是不是很意外。

但是看官们一般不会使用MemoryCache的原生方法,而是使用位于同一命名空间的 扩展方法Set。

var s = new MemoryCache(new MemoryCacheOptions { }); 
s.Set("WeChatID""精益码农"); 
var f = s.TryGetValue("WeChatID"out object obj); 
 
Console.WriteLine(f); 
Console.WriteLine(obj); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

如此便能正确输出。

扩展类源码看一看

public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value) 

     using ICacheEntry entry = cache.CreateEntry(key); 
     entry.Value = value; 
     return value; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

扩展方法与原生方法的差异在于using关键字 (也说明了CacheEntry继承自IDisposable接口)。

继续追溯CacheEntry实现的Dispose方法:

public void Dispose() 
 { 
     if (!_state.IsDisposed) 
     { 
         _state.IsDisposed = true
 
         if (_cache.TrackLinkedCacheEntries) 
         { 
             CacheEntryHelper.ExitScope(this, _previous); 
         } 
 
         // Don't commit or propagate options if the CacheEntry Value was never set
         // We assume an exception occurred causing the caller to not set the Value successfully, 
         // so don't use this entry. 
         if (_state.IsValueSet) 
         { 
             _cache.SetEntry(this); 
 
             if (_previous != null && CanPropagateOptions()) 
             { 
                 PropagateOptions(_previous); 
             } 
         } 
 
         _previous = null; // we don't want to root unnecessary objects 
     } 
 } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

注意其中的_cache.SetEntry(this),表示在MemoryCache底层的ConcurrentDictionary

综上:缓存项CacheEntry需要被Dispose,才能被插入MemoeyCache。

这是怎样的设计模式?IDisposable接口不是用来释放资源吗?

为啥要使用Dispose方法来向MemoryCache插值?

不能使用一个明确的Commit方法吗?

这在Github上也有issue讨论,从2017年开始就有大佬质疑这是一个反人类的设计思路,官方为了不引入Break Change,一直保持到现在。

基于此现状,我们如果使用MemoryCache的原生插值方法, 需要这样:

var s = new MemoryCache(new MemoryCacheOptions { }); 
using (var entry = s.CreateEntry("WeChatID")) 

     entry.Value = "精益码农"

var f = s.TryGetValue("WeChatID"out object obj); 
... 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

尽量不要使用C#8.0推出的不带大括号的using语法

using var entry = s.CreateEntry("WeChatID"); 
entry.Value = "精益码农"
            
var f = s.TryGetValue("WeChatID"out object obj); 
... 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

这种没明确指定using作用范围的语法,会在函数末尾才执行Dispose方法, 导致执行到TryGetValue时,缓存项其实还没插入!!!

Last

 

  • MemoryCache插值的实现过程很奇葩
  • 尽量使用带明确大括号范围的using语法,C#8.0推出的不带大括号的using语法糖的作用时刻在函数末尾,会带来误导。