缓存是系统优化中简单又有效的工具,投入小收获大。数据库中的索引等简单有效的优化功能本质上都是缓存
响应报文头:cache-control:max-age=60 表示服务器提示浏览器端“可以缓存这个相应内容60秒”
用法:只要给需要进行缓存控制的控制器的操作方法添加 ResponseCacheAttribute这个Attribute,ASP.NET Core会自动添加 cache-control 报文头。
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协议规定)。
内存缓存原理:
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
用来设定缓存项的绝对过期时间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);
}
///
/// 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." );
}
}
}
常用的分布式缓存服务器有 Redis、Memcached 等
.NET Core中提供了统一的分布式缓存服务器的操作接口 IDistributedCache,用法和内存缓存类似。
分布式缓存和内存缓存的区别:缓存的值类型为byte[],需要进行类型转换,也提供了一些按照string类型存取值的扩展方法。
用什么做缓存服务器?
SQL Server 做缓存性能不好——不推荐
Memcached是缓存专用,性能非常高,但集群、高可用等方面比较弱,而且有 “缓存键的最大长度为250字节” 的限制。没有官方NuGet包,可以安装EnyimMemcachedCore
第三方 NuGet包来使用
Redis不局限于缓存,Redis做缓存服务器比Memcached性能稍差,但是Redis的集群、高可用等方面非常强大,适合在数据量大、高可用性等场合使用。官方NuGet包:Microsoft.Extensions.Caching.StackExchangedRedis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = "xxx_"; // 规避混乱
});
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);
}
///
/// 分布式缓存帮助实现类
///
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))
};
}
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;
}
}