.NET 中缓存的实现

简介: .NET 中缓存的实现

在实际开发中我们经常会用到是缓存。它是的核心思想是记录过程数据重用操作结果。当程序需要执行复杂且消耗资源的操作时,我们一般会将运行的结果保存在缓存中,当下次需要该结果时,将它从缓存中读取出来。

缓存适用于不经常更改的数据,甚至永远不改变的数据。不断变化的数据并不适合缓存,例如飞机飞行的GPS数据就不该被缓存,否则你会得到错误的数据。


一、缓存类型

缓存一共有三种类型:


  1. In-Memory Cache:进程内缓存。进程终止时缓存也随之终止。
  2. 持久性进程内缓存:在进程内存之外备份缓存,备份位置可能在文件中,可能在数据库中,也可能在其他位置。如果进程重启,缓存并不会丢失。
  3. 分布式缓存:多台机器共享缓存。如果一台服务器保存了一个缓存项,其他服务器也可以使用它。

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));

获取用户头像时只有首次请求才会真正请求数据库,请求到数据库后将头像数据保存在进程内存中,后续对头像所有请求都将从内存中提取,从而节省了时间和资源。但是由于多种原因这个解决方案并不是最好的。首先它不是线程安全的,多个线程使用时可能会发生异常。另外缓存的数据将永远留在内存中,一旦内存被各种原因清理掉,保存在内存中的数据就会丢失。下面总结出了这种解决方案的缺点:


  1. 缓存占用大量内存,导致内存不足异常和崩溃;

         
  1. 高内存消耗会导致内存压力,垃圾收集器的工作量会超应有的水平害性能;
  2. 如果数据发生变化,需要刷新缓存


为了解决上面的问题,缓存框架就必须具有驱逐策略,根据算法逻辑从缓存中删除项目。常见的驱逐政策如下:


  1. 过期策略:在指定时间后从缓存中删除项目;
  2. 如果在指定时间段内未访问某个项目,滑动过期策略将从缓存中删除该项目。例如我们将过期时间设置为1分钟,只要每30秒使用一次该项目,就会一直保留在缓存中。但是超过一分钟不使用它就会被删除。
  3. 大小限制策略:限制缓存内存大小。


下面根据上面所说的策略来改进我们的代码,我们可以使用微软为我们提供的解决方案。微软有两个个解决方案 ,提供两个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;
    }
}
  1. SizeLimit被添加到MemoryCacheOptions. 这为我们的缓存容器添加了基于缓存大小的策略。混村大小没有单位。我们需要在每个缓存条目上设置大小;
  2. 我们可以使用.SetPriority()设置当达到大小限制时删除什么级别的缓存,级别为Low、Normal、High和NeverRemove;
  3. SetSlidingExpiration(TimeSpan.FromSeconds(2))将滑动过期时间设置为两秒,如果一个项目在两秒内未被访问,就将被删除;
  4. SetAbsoluteExpiration(TimeSpan.FromSeconds(10))将绝对过期时间设置为10秒,项目将在10秒内被删除。

你以为这种实现就没问题了吗?其实他还是存在问题的:


  1. 虽然可以设置缓存大小限制,但缓存实际上并不监控GC压力。
  2. 当多个线程同时请求同一个项目时,请求不会等待第一个完成,那么这个项目将被创建多次。比如正在缓存头像,从数据库中获取头像需要5秒,在第一次请求后的3秒中另一个请求来获取头像,它将检查头像是否已缓存,这时头像并没有缓存,那么它也将开始访问数据库。


下面我们来解决上面提到的两个问题:

首先关于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。

上述实现有一些开销,只有在以下情况下方可使用:


  1. 当项目的创建时间具有某种成本时;
  2. 当一个项目的创建时间很长时;
  3. 当必须确保每个键都创建一个项目时。


TIP:缓存是非常强大的模式但也很危险,且有其自身的复杂性。缓存太多会导致 GC 压力,缓存太少会导致性能问题。

目录
相关文章
|
缓存 数据格式
实现LRU缓存的三种方式(建议收藏)
LRU全称为Least Recently Used,即最近使用的。针对的是在有限的内存空间内,只缓存最近使用的数据(即get和set的数据),超过有限内存空间的数据将会被删除。这个在面试题中也是常会被问到的内容,接下来就看看怎么来实现。
1009 0
实现LRU缓存的三种方式(建议收藏)
|
6月前
|
缓存 监控 NoSQL
一个.Net Core开源缓存中间件,让你更加简单、方便使用缓存
一个.Net Core开源缓存中间件,让你更加简单、方便使用缓存
146 0
|
数据采集 缓存 NoSQL
干货 | 使用布隆过滤器实现高效缓存
本文主要描述,使用布隆过滤实现高效缓存。文中采用数组做为缓存,如果需要高并发命中,则需将文中的数组换成Redis数据库。
干货 | 使用布隆过滤器实现高效缓存
|
机器学习/深度学习 缓存 Oracle
【数据库设计与实现】第7章:缓存与检查点
缓存与检查点设计原则数据缓冲区与检查点是相辅相成的,所以放在同一个章节介绍。由于CPU与持久化设备之间存在巨大的速度差距,所以在内存中引入缓冲区缩小这个差距。从读的角度来看,将热点数据或预判用户可能读取的数据提前加载到内存中,从而将持久化设备的读时延和带宽提升至内存的时延和带宽。从写的角度来看,直接修改缓冲区中的数据而不是磁盘中的数据,可以带来两方面的优势。其一,将持久化设备的写时延和带宽提升至内
【数据库设计与实现】第7章:缓存与检查点
|
缓存 JSON Java
java 实现读取txt文件,反射创建对象,android 手机缓存文件目录
java 实现读取txt文件,反射创建对象,android 手机缓存文件目录
342 1
java 实现读取txt文件,反射创建对象,android 手机缓存文件目录
|
存储 缓存 算法
基于LinkedHashMap实现LRU缓存
基于LinkedHashMap实现LRU缓存
170 0
基于LinkedHashMap实现LRU缓存
|
缓存 算法 安全
如何使用 LinkedHashMap 实现 LRU 缓存?
在上一篇文章里,我们聊到了 HashMap 的实现原理和源码分析,在源码分析的过程中,我们发现一些 LinkedHashMap 相关的源码,当时没有展开,现在它来了。 那么,LinkedHashMap 与 HashMap 有什么区别呢?其实,LinkedHashMap 的使用场景非常明确 —— LRU 缓存。今天,我们就来讨论 LinkedHashMap 是如何实现 LRU 缓存的。
136 0
|
存储 缓存 算法
手把手使用 PHP 实现 LRU 缓存淘汰算法
手把手使用 PHP 实现 LRU 缓存淘汰算法
175 0
手把手使用 PHP 实现 LRU 缓存淘汰算法
|
缓存 NoSQL Java
基于注解实现缓存的框架 -- SpringCache
Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能,大大简化我们在业务中操作缓存的代码。
基于注解实现缓存的框架 -- SpringCache
|
缓存 JavaScript Serverless
js: 实现一个cached缓存函数计算结果
js: 实现一个cached缓存函数计算结果
182 0

热门文章

最新文章