• ASP.NET Core 性能优化-缓存


    缓存

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

    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
    {
        /// 
        /// 从缓存中获取数据,如果缓存中没有数据,则调用valueFactory获取数据。
        /// 
        /// 
        /// 这里加入了缓存数据的类型不能是IEnumerable、IQueryable等类型的限制
        /// 
        /// 缓存的值的类型
        /// 缓存的key
        /// 提供数据的委托
        /// 缓存过期秒数的最大值,实际缓存时间是在[expireSeconds,expireSeconds*2)之间,这样可以一定程度上避免大批key集中过期导致的“缓存雪崩”的问题
        /// 
        TResult? GetOrCreate<TResult>(string cacheKey, Func<ICacheEntry, TResult?> valueFactory, int expireSeconds = 60);
    
        /// 
        /// 从缓存中获取数据,如果缓存中没有数据,则调用valueFactory获取数据。
        /// 
        /// 
        /// 这里加入了缓存数据的类型不能是IEnumerable、IQueryable等类型的限制
        /// 
        /// 缓存的值的类型
        /// 缓存的key
        /// 提供数据的委托
        /// 缓存过期秒数的最大值,实际缓存时间是在[expireSeconds,expireSeconds*2)之间,这样可以一定程度上避免大批key集中过期导致的“缓存雪崩”的问题
        /// 
        Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> valueFactory, int expireSeconds = 60);
    
        /// 
        /// 删除缓存的值
        /// 
        /// 
        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
    /// 
    /// IMemoryCacheHelper 内存缓存帮助实现类
    /// 
    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);
    
        /// 
        /// 过期时间
        /// 
        /// 
        /// Random.Shared 是.NET6新增的
        /// 
        /// ICacheEntry
        /// 过期时间
        private static void InitCacheEntry(ICacheEntry entry, int baseExpireSeconds) =>
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.NextDouble(baseExpireSeconds, baseExpireSeconds * 2));
    
        /// 
        /// 验证值类型
        /// 
        /// 
        /// 
        private static void ValidateValueType<TResult>()
        {
            // 因为IEnumerable、IQueryable等有延迟执行的问题,造成麻烦,因此禁止用这些类型
            Type typeResult = typeof(TResult);
    
            // 如果是IEnumerable这样的泛型类型,则把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 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
    {
        /// 
        /// 创建缓存
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        TResult? GetOrCreate<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, TResult?> valueFactory, int expireSeconds = 60);
    
        /// 
        /// 创建缓存
        /// 
        /// 
        /// 
        /// 
        /// 
        /// 
        Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, Task<TResult?>> valueFactory, int expireSeconds = 60);
    
        /// 
        /// 删除缓存
        /// 
        /// 
        void Remove(string cacheKey);
    
        /// 
        /// 删除缓存
        /// 
        /// 
        /// 
        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
    /// 
    /// 分布式缓存帮助实现类
    /// 
    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
    {
        /// 
        ///  返回指定范围内的随机双精度数值
        /// 
        /// 
        /// 返回的随机数的包含下限
        /// 返回的随机数的独占上界。maxValue必须大于或等于minValue
        /// 
        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
  • 相关阅读:
    C语言 | Leetcode C语言题解之第125题验证回文串
    【C语言】——三道基础程序练习
    QT软件开发-基于FFMPEG设计视频播放器-支持软解与硬解-完整例子(六)
    VulnHub Nullbyte
    RabbitMQ消息可靠性问题
    ChatGPT AIGC 高效办公自动化案例
    selenium (自动化概念 && 测试环境配置)
    Python实验二
    最长连续序列(哈希解)
    影视广告创意与制作(四)
  • 原文地址:https://blog.csdn.net/qq_43562262/article/details/126206297