在实际开发中我们经常会用到是缓存。它是的核心思想是记录过程数据重用操作结果。当程序需要执行复杂且消耗资源的操作时,我们一般会将运行的结果保存在缓存中,当下次需要该结果时,将它从缓存中读取出来。 缓存适用于不经常更改的数据,甚至永远不改变的数据。不断变化的数据并不适合缓存,例如飞机飞行的GPS数据就不该被缓存,否则你会得到错误的数据。
缓存一共有三种类型:
Tip:在本篇文章中我们只讲解进程内缓存。
下面我们通过缓存头像,一步一步来实现进程内缓存。 在.NET早期的版本中我们实现缓存的方式很简单,如下代码:
public class NaiveCache<TItem>
{
Dictionary<object, TItem> _cache = new Dictionary<object, TItem>();
public TItem GetOrCreate(object key, Func<TItem> createItem)
{
if (!_cache.ContainsKey(key))
{
_cache[key] = createItem();
}
return _cache[key];
}
}
使用它的方法是这样的:
var _avatarCache = new NaiveCache<byte[]>();
var myAvatar = _avatarCache.GetOrCreate(userId, () => _database.GetAvatar(userId));
获取用户头像时只有首次请求才会真正请求数据库,请求到数据库后将头像数据保存在进程内存中,后续对头像所有请求都将从内存中提取,从而节省了时间和资源。但是由于多种原因这个解决方案并不是最好的。首先它不是线程安全的,多个线程使用时可能会发生异常。另外缓存的数据将永远留在内存中,一旦内存被各种原因清理掉,保存在内存中的数据就会丢失。下面总结出了这种解决方案的缺点:
为了解决上面的问题,缓存框架就必须具有驱逐策略,根据算法逻辑从缓存中删除项目。常见的驱逐政策如下:
下面根据上面所说的策略来改进我们的代码,我们可以使用微软为我们提供的解决方案。微软有两个个解决方案 ,提供两个NuGet包用于缓存。微软推荐使用Microsoft.Extensions.Caching.Memory,因为它可以和Asp.NET Core集成,可以很容易地注入到Asp.NET Core中。使用Microsoft.Extensions.Caching.Memory的样例代码如下:
public class SimpleMemoryCache<TItem>
{
private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
public TItem GetOrCreate(object key, Func<TItem> createItem)
{
TItem cacheEntry;
if (!_cache.TryGetValue(key, out cacheEntry))
{
cacheEntry = createItem();
_cache.Set(key, cacheEntry);
}
return cacheEntry;
}
}
使用它的方法是这样的:
var _avatarCache = new SimpleMemoryCache<byte[]>();
var myAvatar = _avatarCache.GetOrCreate(userId, () => _database.GetAvatar(userId));
首先这是一个线程安全的实现,可以一次从多个线程安全地调用它。其次MemoryCache允许加入所有驱逐政策。下面的例子就是具有驱逐策略的IMemoryCache:
public class MemoryCacheWithPolicy<TItem>
{
private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions()
{
SizeLimit = 1024
});
public TItem GetOrCreate(object key, Func<TItem> createItem)
{
TItem cacheEntry;
if (!_cache.TryGetValue(key, out cacheEntry))
{
cacheEntry = createItem();
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSize(1)
.SetPriority(CacheItemPriority.High)
.SetSlidingExpiration(TimeSpan.FromSeconds(2))
.SetAbsoluteExpiration(TimeSpan.FromSeconds(10));
_cache.Set(key, cacheEntry, cacheEntryOptions);
}
return cacheEntry;
}
}
你以为这种实现就没问题了吗?其实他还是存在问题的:
下面我们来解决上面提到的两个问题: 首先关于GC压力,我们可以使用多种技术和启发式方法来监控GC压力。第二个问题是比较容易解决的,使用一个MemoryCache就可以实现:
public class WaitToFinishMemoryCache<TItem>
{
private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
private ConcurrentDictionary<object, SemaphoreSlim> _locks = new ConcurrentDictionary<object, SemaphoreSlim>();
public async Task<TItem> GetOrCreate(object key, Func<Task<TItem>> createItem)
{
TItem cacheEntry;
if (!_cache.TryGetValue(key, out cacheEntry))
{
SemaphoreSlim mylock = _locks.GetOrAdd(key, k => new SemaphoreSlim(1, 1));
await mylock.WaitAsync();
try
{
if (!_cache.TryGetValue(key, out cacheEntry))
{
cacheEntry = await createItem();
_cache.Set(key, cacheEntry);
}
}
finally
{
mylock.Release();
}
}
return cacheEntry;
}
}
用法:
var _avatarCache = new WaitToFinishMemoryCache<byte[]>();
var myAvatar = await _avatarCache.GetOrCreate(userId, async () => await _database.GetAvatar(userId));
这个实现锁定了项目的创建,锁是特定于钥匙的。如果我们正在等待获取张三的头像,我们仍然可以在另一个线程上获取 李四头像的缓存。_locks存储了所有的锁,因为常规锁不适用于async、await,所以我们需要使用SemaphoreSlim。 上述实现有一些开销,只有在以下情况下方可使用:
TIP:缓存是非常强大的模式但也很危险,且有其自身的复杂性。缓存太多会导致 GC 压力,缓存太少会导致性能问题。