内存缓存和分布式缓存

参考官方文档:https://learn.microsoft.com/zh-cn/aspnet/core/performance/caching/overview?view=aspnetcore-6.0

内存中缓存可以存储任何对象。 分布式缓存接口仅限于 byte[],应用程序需要自行解决针对缓存对象的序列化和反序列化问题。 内存中和分布式缓存都将缓存项存储为键值对。

1、内存缓存

内存缓存就是把缓存数据放到应用程序内存中,利用内存读写比磁盘、网络请求快的特点来提高应用性能。不同程序内存缓存相互独立,一旦程序重启,内存缓存中的数据也会丢失。

1.1、过期策略

  • 绝对时间过期策略:不论缓存对象最近使用的频率如何,对应的 ICacheEntry 对象总是在指定的时间点之后过期。注意:当 AbsoluteExpiration 和 AbsoluteExpirationRelativeToNow 都设置值的时候,绝对过期时间取距离当前时间近的那个设置。
  • 滑动过期:如果在指定的时间内没有读取过该缓存条目,缓存将会过期。反之,针对缓存的每一次使用都会将过期时间向后延长SlidingExpiration时长。
  • 两种混用:如设置了绝对过期时间为1小时后,且滑动过期时间为5分钟,则过期采用的算法为:Min(AbsoluteExpiration - Now , SlidingExpiration)。注意:绝对过期时间一定要大于滑动过期时间。
  • 利用 IChangeToken 对象发送通知:在对象被修改之前永不过期,修改后更改缓存值。

 

1.2、代码中使用

添加 Nuget 引用:Microsoft.Extensions.Caching.Memory

在 startup.cs 服务配置里面加上:services.AddMemoryCache();

上面将服务添加到依赖注入容器后,在构造函数中请求 IMemoryCache 实例:

public class IndexModel : PageModel
{
    private readonly IMemoryCache _memoryCache;

    public IndexModel(IMemoryCache memoryCache) =>
        _memoryCache = memoryCache;

    // ...
}    

方法一:(推荐)使用 GetOrCreate 和 GetOrCreateAsync 扩展方法来缓存数据

public async Task OnGetCacheGetOrCreateAsync()
{
    var cachedValue = await _memoryCache.GetOrCreateAsync(
        CacheKeys.Entry,//自定义key,object
        cacheEntry =>
        {
            //设置绝对过期时间
            cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
            //设置滑动过期时间
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    // ...
}
//GetOrCreate类似

方法二:使用 TryGetValue 来检查缓存中是否包含时间。 如果未缓存时间,则创建一个新条目,并使用 Set 将该条目添加到缓存中

public void OnGet()
{
    CurrentDateTime = DateTime.Now;

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = CurrentDateTime;

        //设置绝对过期时间
        _memoryCache.Set(CacheKeys.Entry, DateTime.Now, TimeSpan.FromDays(1));
        //设置滑动过期时间
        _memoryCache.Set(CacheKeys.Entry, DateTime.Now,new MemoryCacheEntryOptions() { SlidingExpiration=new TimeSpan(0,0,10)});
    }

    CacheCurrentDateTime = cacheValue;
}

 

2、分布式缓存

分布式缓存是由多个应用服务器共享的缓存,通常作为访问它的应用服务器的外部服务进行维护。.Net Core使用统一的 IDistributedCache 接口与缓存进行交互。

2.1、基于 Redis 的分布式缓存

首先请确保已经正常安装并启动了 Redis,并在项目中添加Nuget包:Microsoft.Extensions.Caching.Redis

关于这个组件工具的文档说明,参考:https://stackexchange.github.io/StackExchange.Redis/

//startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedRedisCache(option =>
    {
        option.Configuration = "localhost";
        option.InstanceName = "Demo_";//每个rediskey前面加上的字符串,用于区分不同应用系统。
    });

    services.AddControllersWithViews();
}
//HomeController.cs
private readonly IDistributedCache distributedCache;//Redis
public HomeController(IDistributedCache distributedCache)
{
    this.distributedCache = distributedCache;
}
public async Task<string> TestRedisCache()
{
    var time = await distributedCache.GetStringAsync("CurrentTime");
    if (null==time)
    {
        time = DateTime.Now.ToString();
        await distributedCache.SetAsync("CurrentTime", Encoding.UTF8.GetBytes(time));
        //设置带有过期时间的
        //redisCache.SetString("str", DateTime.Now.Ticks.ToString(),new DistributedCacheEntryOptions() { AbsoluteExpiration=DateTime.Now.AddSeconds(10)});
    }
    return $"缓存时间:{time}(,当前时间:{DateTime.Now})";
}
 

可以额外了解下:StackExchange.Redis 。它是 .NET 领域知名的 Redis 客户端框架。

 

3、封装

缓存穿透:一直查询不存在的值。
解决方案:把null值也当成一个数据存入缓存。使用 GetOrCreate()/GetOrCreateAsync() 方法即可,因为它会把 null 值也当成合法的缓存值。

缓存雪崩:缓存项集中过期引起缓存雪崩。
解决方案:在基础过期时间之上,再加一个随机的过期时间。

基于缓存穿透和缓存雪崩问题,封装缓存操作类。

3.1、扩展Random.NextDouble()

扩展 random 随机数使其支持 Double ,避免int类型不精确或者缓存过多导致随机数重复,造成缓存雪崩。

RondowExtensions
 public static class RondowExtensions
{
    public static double NextDouble(this Random random, double minValue, double maxValue)
    {
        if (minValue >= maxValue) throw new ArgumentOutOfRangeException(nameof(minValue), "minValue annot be bigger than maxValue");
        double x = random.NextDouble();
        return x * maxValue + (1 - x) * minValue;
    }
}

 

3.2、内存缓存操作帮助类

常用 IMemoryCache接口:

  • bool TryGetValue:尝试获取缓存键为key的缓存值,用value参数获取缓存值。如果缓存中没有缓存键为key的缓存值,方法返回true,否则返回false。
  • void Remove:删除缓存键为key的缓存内容。
  • TItem Set<TItem>:设置缓存键为key的缓存值为value。
  • TItem GetOrCreate<TItem>:尝试获取缓存键为key的缓存值,方法的返回值为获取的缓存值。如果缓存中没有缓存键为key的缓存值,则调用factory指向的回调从数据源获取数据,把获取的数据作为缓存值保存到缓存中,并且把获取的数据作为方法的返回值。
  • TItem GetOrCreateAsync<TItem>:异步版本的GetOrCache方法。
IMemoryCacheHelper
public interface IMemoryCacheHelper
{
    /// <summary>
    /// 从缓存中获取数据,如果缓存中没有数据,则调用valueFactory获取数据。
    /// </summary>
    /// <remarks>
    /// 这里加入了缓存数据的类型不能是IEnumerable、IQueryable等类型的限制
    /// </remarks>
    /// <typeparam name="TResult">缓存的值的类型</typeparam>
    /// <param name="cacheKey">缓存的key</param>
    /// <param name="valueFactory">提供数据的委托</param>
    /// <param name="expireSeconds">缓存过期秒数的最大值,实际缓存时间是在[expireSeconds,expireSeconds*2)之间,这样可以一定程度上避免大批key集中过期导致的“缓存雪崩”的问题</param>
    /// <returns></returns>
    TResult? GetOrCreate<TResult>(string cacheKey, Func<ICacheEntry, TResult?> valueFactory, int expireSeconds = 60);

    Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> valueFactory, int expireSeconds = 60);

    /// <summary>
    /// 删除缓存的值
    /// </summary>
    /// <param name="cacheKey"></param>
    void Remove(string cacheKey);
}
MemoryCacheHelper
/// <summary>
/// IMemoryCacheHelper 内存缓存帮助实现类
/// </summary>
internal class MemoryCacheHelper : IMemoryCacheHelper
{
    private readonly IMemoryCache _memoryCache;
    public MemoryCacheHelper(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public TResult? GetOrCreate<TResult>(string cacheKey, Func<ICacheEntry, TResult?> valueFactory, int expireSeconds = 60)
    {
        ValidateValueType<TResult>();

        // 因为IMemoryCache保存的是一个CacheEntry,所以null值也认为是合法的,因此返回null不会有“缓存穿透”的问题
        // 不调用系统内置的CacheExtensions.GetOrCreate,而是直接用GetOrCreate的代码,这样免得包装一次委托
        if (!_memoryCache.TryGetValue(cacheKey, out TResult result))
        {
            using ICacheEntry entry = _memoryCache.CreateEntry(cacheKey);
            InitCacheEntry(entry, expireSeconds);
            result = valueFactory(entry)!;
            entry.Value = result;
        }

        return result;
    }

    public async Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> valueFactory, int expireSeconds = 60)
    {
        ValidateValueType<TResult>();

        if (!_memoryCache.TryGetValue(cacheKey, out TResult result))
        {
            using ICacheEntry entry = _memoryCache.CreateEntry(cacheKey);
            InitCacheEntry(entry, expireSeconds);
            result = (await valueFactory(entry))!;
            entry.Value = result;
        }

        return result;
    }

    public void Remove(string cacheKey) => _memoryCache.Remove(cacheKey);

    /// <summary>
    /// 过期时间
    /// </summary>
    /// <remarks>
    /// Random.Shared 是.NET6新增的
    /// </remarks>
    /// <param name="entry">ICacheEntry</param>
    /// <param name="baseExpireSeconds">过期时间</param>
    private static void InitCacheEntry(ICacheEntry entry, int baseExpireSeconds) =>
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.NextDouble(baseExpireSeconds, baseExpireSeconds * 2));

    /// <summary>
    /// 验证值类型
    /// </summary>
    /// <typeparam name="TResult"></typeparam>
    /// <exception cref="InvalidOperationException"></exception>
    private static void ValidateValueType<TResult>()
    {
        // 因为IEnumerable、IQueryable等有延迟执行的问题,造成麻烦,因此禁止用这些类型
        Type typeResult = typeof(TResult);

        // 如果是IEnumerable<String>这样的泛型类型,则把String这样的具体类型信息去掉,再比较
        if (typeResult.IsGenericType)
        {
            typeResult = typeResult.GetGenericTypeDefinition();
        }

        // 注意用相等比较,不要用IsAssignableTo
        if (typeResult == typeof(IEnumerable<>)
            || typeResult == typeof(IEnumerable)
            || typeResult == typeof(IAsyncEnumerable<TResult>)
            || typeResult == typeof(IQueryable<TResult>)
            || typeResult == typeof(IQueryable))
        {
            throw new InvalidOperationException($"TResult of {typeResult} is not allowed, please use List<T> or T[] instead.");
        }
    }
}

 

3.3、分布式缓存操作帮助类

常用 IDistributedCache 接口:

  • GetGetAsync:如果在缓存中找到,则接受字符串键并以 byte[] 数组的形式检索缓存项。
  • SetSetAsync:使用字符串键将项(作为 byte[] 数组)添加到缓存。
  • RefreshRefreshAsync:根据键刷新缓存中的项,重置其可调到期超时(如果有)。
  • RemoveRemoveAsync:根据字符串键删除缓存项。
IDistributedCacheHelper
 public interface IDistributedCacheHelper
{
    /// <summary>
    /// 创建缓存
    /// </summary>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="cacheKey"></param>
    /// <param name="valueFactory"></param>
    /// <param name="expireSeconds"></param>
    /// <returns></returns>
    TResult? GetOrCreate<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, TResult?> valueFactory, int expireSeconds = 60);

    /// <summary>
    /// 创建缓存
    /// </summary>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="cacheKey"></param>
    /// <param name="valueFactory"></param>
    /// <param name="expireSeconds"></param>
    /// <returns></returns>
    Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, Task<TResult?>> valueFactory, int expireSeconds = 60);

    /// <summary>
    /// 删除缓存
    /// </summary>
    /// <param name="cacheKey"></param>
    void Remove(string cacheKey);

    /// <summary>
    /// 删除缓存
    /// </summary>
    /// <param name="cacheKey"></param>
    /// <returns></returns>
    Task RemoveAsync(string cacheKey);
}
DistributedCacheHelper
 /// <summary>
/// 分布式缓存帮助实现类
/// </summary>
public class DistributedCacheHelper : IDistributedCacheHelper
{
    private readonly IDistributedCache _distCache;

    public DistributedCacheHelper(IDistributedCache distCache)
    {
        _distCache = distCache;
    }

    public TResult? GetOrCreate<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, TResult?> valueFactory, int expireSeconds = 60)
    {
        string jsonStr = _distCache.GetString(cacheKey);

        // 缓存中不存在
        if (string.IsNullOrEmpty(jsonStr))
        {
            var options = CreateOptions(expireSeconds);

            // 如果数据源中也没有查到,可能会返回null
            TResult? result = valueFactory(options);

            // null 会被 json 序列化为字符串 "null",所以可以防范“缓存穿透”
            string jsonOfResult = JsonSerializer.Serialize(result, typeof(TResult));
            _distCache.SetString(cacheKey, jsonOfResult, options);

            return result;
        }
        else
        {
            // "null"会被反序列化为null
            // TResult如果是引用类型,就有为null的可能性;如果TResult是值类型
            // 在写入的时候肯定写入的是0、1之类的值,反序列化出来不会是null
            // 所以如果obj这里为null,那么存进去的时候一定是引用类型
            _distCache.Refresh(cacheKey);//刷新,以便于滑动过期时间延期

            return JsonSerializer.Deserialize<TResult>(jsonStr)!;
        }
    }

    public async Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, Task<TResult?>> valueFactory, int expireSeconds = 60)
    {
        string jsonStr = await _distCache.GetStringAsync(cacheKey);

        if (string.IsNullOrEmpty(jsonStr))
        {
            var options = CreateOptions(expireSeconds);

            TResult? result = await valueFactory(options);
            string jsonOfResult = JsonSerializer.Serialize(result, typeof(TResult));

            await _distCache.SetStringAsync(cacheKey, jsonOfResult, options);
            return result;
        }
        else
        {
            await _distCache.RefreshAsync(cacheKey);
            return JsonSerializer.Deserialize<TResult>(jsonStr)!;
        }
    }

    public void Remove(string cacheKey) => _distCache.Remove(cacheKey);

    public Task RemoveAsync(string cacheKey) => _distCache.RemoveAsync(cacheKey);

    private static DistributedCacheEntryOptions CreateOptions(int expireSeconds) => new()
    {
        // 过期时间.Random.Shared 是.NET6新增的
        AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.NextDouble(expireSeconds, expireSeconds * 2))
    };
}

 

posted @ 2024-03-14 17:39  茜茜87  阅读(10)  评论(0编辑  收藏  举报