当前位置:   article > 正文

ASP.NET Core 性能优化-缓存_memorycache 帮助类

memorycache 帮助类

缓存

缓存是系统优化中简单又有效的工具,投入小收获大。数据库中的索引等简单有效的优化功能本质上都是缓存

ASP.NET Core 客户端缓存

响应报文头:cache-control:max-age=60 表示服务器提示浏览器端“可以缓存这个相应内容60秒”

用法:只要给需要进行缓存控制的控制器的操作方法添加 ResponseCacheAttribute这个Attribute,ASP.NET Core会自动添加 cache-control 报文头。

ASP.NET Core 服务端缓存

1、如果ASP.NET Core中安装了“响应缓存中间件”,那么ASP.NET Core不仅会继续根据 [ResponseCache] 设置来生成cache-control响应报文头来设置客户端缓存,而且服务器端也会按照 [ResponseCache] 的设置来对响应进行服务器端缓存。
2、“响应缓存中间件”的好处:对于来自不同客户端的相同请求或者不支持客户端缓存的客户端,能减低服务器的压力。

用法:app.MapControllers() 之前 加上 app.UseResponseCaching()。确保app.UseCors() 写到 app.UseResponseCaching()之前。

服务器端响应缓存很鸡肋

1、无法解决恶意请求给服务器带来的压力
2、服务器端响应缓存还有很多限制,包括但不限于:响应状态码为200的GET或者HEAD请求才可能被缓存;报文头中不能含有Authorization、Set-Cookie等(限制由 RFC7234协议规定)。

  • 解决方案:采用内存缓存、分布式缓存 等。

内存缓存(In-memory cache)

内存缓存原理:
1、把缓存数据放到应用程序的内存。内存缓存中保存的是一系列的键值对,就像Dictionary类型一样。
2、内存缓存的数据保存在当前运行的网站程序的内存中,是和进程相关的。因为在Web服务器中,多个不同网站的缓存是不会互相干扰的,而且网站重启后,内存中的所有数据也就被清空了。

用法:
1、启用:builder.Services.AddMemoryCache()
2、注入 IMemoryCache 接口,常用接口方法:TryGetValue、Remove、Set、GetOrCreate、GetOrCreateAsync

缓存的过期时间策略

1、在数据改变的时候调用Remove或者Set来删除或者修改缓存。优点:及时
2、过期时间(只要过期时间比较短,缓存数据不一致的情况也不会持续很长时间)

过期时间的两种策略

1、绝对过期时间
2、滑动过期时间

绝对过期时间
  • GetOrCreateAsync() 方法的回调方法中有一个 ICacheEntry 类型的参数,通过 ICacheEntry 对当前的缓存项做设置
  • AbsoluteExpirationRelativeToNow 用来设定缓存项的绝对过期时间
滑动过期时间
  • ICacheEntry 的 SlidingExpiration 设定缓存项的滑动过期时间
两种过期时间混用

使用滑动过期时间策略,如果一个缓存项一直被频繁访问,那么这个缓存项就会一直被续期而不过期。可以对一个缓存项同时设定滑动过期时间和绝对过期时间,并且把绝对过期时间设定的比滑动过期时间长,这样缓存项的内容会在绝对过期时间内随着访问被滑动续期,但是一旦超过了绝对过期时间,缓存项就会被删除。

内存缓存过期时间策略使用机制

无论哪种过期时间策略,程序中都会存在缓存数据不一致的情况。部分系统(博客等)无所谓,部分系统不能忍受(金融等)。

可以通过其他机制获取数据源改变的消息,在通过代码调用 IMemoryCache 的 Set() 方法更新缓存

缓存穿透问题

解决方案:
把 “查不到” 也当成一个数据放入缓存
使用 GetOrCreateAsync() 方法即可,因为它会把 null 值也当成合法的缓存值

缓存雪崩问题

  • 缓存项集中过期引起缓存雪崩

解决方案:在基础过期时间之上,再加一个随机的过期时间

内存缓存操作帮助类

简化操作,规避缓存穿透和缓存雪崩问题

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

    /// <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>
    Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> valueFactory, int expireSeconds = 60);

    /// <summary>
    /// 删除缓存的值
    /// </summary>
    /// <param name="cacheKey"></param>
    void Remove(string cacheKey);
}
  • 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
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
/// <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.");
        }
    }
}
  • 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
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83

分布式缓存

  • 一般情况下不需要使用到分布式缓存,内存缓存即足够。

常用的分布式缓存服务器有 Redis、Memcached 等
.NET Core中提供了统一的分布式缓存服务器的操作接口 IDistributedCache,用法和内存缓存类似。
分布式缓存和内存缓存的区别:缓存的值类型为byte[],需要进行类型转换,也提供了一些按照string类型存取值的扩展方法。

用什么做缓存服务器?
SQL Server 做缓存性能不好——不推荐
Memcached是缓存专用,性能非常高,但集群、高可用等方面比较弱,而且有 “缓存键的最大长度为250字节” 的限制。没有官方NuGet包,可以安装 EnyimMemcachedCore 第三方 NuGet包来使用
Redis不局限于缓存,Redis做缓存服务器比Memcached性能稍差,但是Redis的集群、高可用等方面非常强大,适合在数据量大、高可用性等场合使用。官方NuGet包:Microsoft.Extensions.Caching.StackExchangedRedis

Redis 用法

builder.Services.AddStackExchangeRedisCache(options =>
{
	options.Configuration = "localhost";
	options.InstanceName = "xxx_"; // 规避混乱
});
  • 1
  • 2
  • 3
  • 4
  • 5

分布式缓存操作帮助类

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);
}
  • 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
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
/// <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))
    };
}
  • 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
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

随机数生成扩展方法

public static class RandomExtensions
{
    /// <summary>
    ///  返回指定范围内的随机双精度数值
    /// </summary>
    /// <param name="random"></param>
    /// <param name="minValue">返回的随机数的包含下限</param>
    /// <param name="maxValue">返回的随机数的独占上界。maxValue必须大于或等于minValue</param>
    /// <returns></returns>
    public static double NextDouble(this Random random, double minValue, double maxValue)
    {
        if (minValue >= maxValue)
        {
            throw new ArgumentOutOfRangeException(nameof(minValue), "minValue cannot be bigger than maxValue");
        }
        // c# Double.MinValue 和 Double.MaxValue 之间的随机数 https://stackoverflow.com/questions/65900931/c-sharp-random-number-between-double-minvalue-and-double-maxvalue
        double x = random.NextDouble();
        return x * maxValue + (1 - x) * minValue;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/秋刀鱼在做梦/article/detail/940974
推荐阅读
相关标签
  

闽ICP备14008679号