【愚公系列】2022年12月 Redis数据库-缓存雪崩和缓存穿透问题的解决
前言
接上文:https://blog.csdn.net/aa2528877987/article/details/128231481?spm=1001.2014.3001.5501
本文主要是讲如何改造AddMemoryCache和AddDistributedMemoryCache方法解决以下两个问题:
-
缓存雪崩:在使用缓存时,通常会对缓存设置过期时间,一方面目的是保持缓存与数据库数据的一致性,另一方面是减少冷缓存占用过多的内存空间。但当缓存中大量热点缓存采用了相同的实效时间,就会导致缓存在某一个时刻同时实效,请求全部转发到数据库,从而导致数据库压力骤增,甚至宕机。从而形成一系列的连锁反应,造成系统崩溃等情况,这就是缓存雪崩(Cache Avalanche)。
-
缓存穿透:用户访问的数据既不在缓存当中,也不在数据库中。出于容错的考虑,如果从底层数据库查询不到数据,则不写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时,数据库的压力骤增,甚至崩溃,这就是缓存穿透问题。
一、缓存雪崩和缓存穿透问题的解决
1.IMemoryCache的改造
1.1 解决方案
- 主要是对分布式缓存添加一个随机过期时间,防止
缓存出现雪崩现象
。 - 至于
缓存穿透
,通常采用cache null策略,表现在调用的时候,对目标值不判空,直接存入缓存即可
1.2 依赖
nuget安装:Microsoft.Extensions.Caching.Memory
1.3 解决思路
1、先反编译分析一下默认GetOrCreate的实现,在这个基础上继续添加业务
这里直接不直接使用内置的GetOrCreate,而是直接用它的实现代码来改造,可以省了一次的委托的调用。经分析源码可知:CreateEntry方法,设置一个缓存key;然后通过 cacheEntry.Value,给该key赋值,这相当于一个基础方法了,不能在再拆解。
2、增加限制:校验缓存内容的类型
IQueryable、IEnumerable等类型可能存在着延迟加载的问题,如果把这两种类型的变量指向的对象保存到缓存中,
在我们把它们取出来再去执行的时候,如果它们延迟加载时候需要的对象已经被释放的话,就会执行失败。因此缓存禁止这两种类型。
注:如果是是IEnumerable<String>这样的泛型类型,则把String这样的具体类型信息去掉,变成IEnumerable<>再比较。
3、增加随机过期时间
- 默认值为60s,即在60s-120s之间取一个值。
- 如果使用的时候想设置缓存是永久有效的,此时这个值将导致无法设置缓存永久有效,需要将该值改为0(或负数)
- 设置为0 或者 负数,不生效
4、全局注册:builder.Services.AddScoped<IMemoryCachePro, MemoryCachePro>();
1.4 具体代码
1、Program.cs
//优化后的Cache缓存策略
builder.Services.AddScoped<IMemoryCachePro, MemoryCachePro>();
2、IMemoryCachePro.cs
/// <summary>
/// 扩展的内存缓存接口
/// </summary>
public interface IMemoryCachePro
{
/// <summary>
/// 01-读取或设置缓存(同步)
/// </summary>
TResult GetOrCreate<TResult>(object key, Func<ICacheEntry, TResult> Func, int defaultExpireSecondes = 60);
/// <summary>
/// 02-读取or设置缓存(异步)
/// </summary>
Task<TResult> GetOrCreateAsync<TResult>(object key, Func<ICacheEntry, Task<TResult>> Func, int defaultExpireSecondes = 60);
}
3、MemoryCachePro.cs
public class MemoryCachePro : IMemoryCachePro
{
private readonly IMemoryCache memoryCache;
public MemoryCachePro(IMemoryCache memoryCache)
{
this.memoryCache = memoryCache;
}
#region 01-读取或设置缓存(同步)
/// <summary>
/// 01-读取或设置缓存(同步)
/// </summary>
/// <typeparam name="TResult">函数的</typeparam>
/// <param name="key">缓存key</param>
/// <param name="Func">委托,需要传入一个函数
/// 函数的参数为:ICacheEntry
/// 函数的返回值为:TResult
/// </param>
/// <param name="defaultExpireSecondes">默认添加的随机过期时间,随机值为 [defaultExprieSeconds,defaultExprieSeconds * 2]之间
/// (1).默认值为60s,即在60s-120s之间取一个值。
/// (2).如果使用的时候想设置缓存是永久有效的,此时这个值将导致无法设置缓存永久有效,需要将该值改为0(或负数)
/// (3).设置为0 或者 负数,不生效
/// </param>
/// <returns></returns>
public TResult GetOrCreate<TResult>(object key, Func<ICacheEntry, TResult> Func, int defaultExpireSecondes = 60)
{
//一. 校验缓存类型
ValidateCacheValueType<TResult>();
//二. 利用TryGetValue和CreateEntry方法进行封装
if (!memoryCache.TryGetValue(key, out var result))
{
//表示缓存不存在
//2.1 创建或覆盖一个缓存key
using ICacheEntry cacheEntry = memoryCache.CreateEntry(key);
//三. 添加一个随机过期时间
if (defaultExpireSecondes > 0)
{
//只有该值 > 0 才生效
SetCacheRandomTime(cacheEntry, defaultExpireSecondes);
}
//2.2 返回值赋值 (这个值来源于委托的调用,获取的返回值)
result = Func(cacheEntry);
//2.3 给该缓存赋值
cacheEntry.Value = result;
// 上述2.2 2.3可以简化为
//result = (cacheEntry.Value = factory(cacheEntry));
}
return (TResult)result!;
}
#endregion
#region 02-读取or设置缓存(异步)
/// <summary>
/// 02-读取or设置缓存(异步)
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="key"></param>
/// <param name="Func"></param>
/// <param name="defaultExpireSecondes"></param>
/// <returns></returns>
public async Task<TResult> GetOrCreateAsync<TResult>(object key, Func<ICacheEntry, Task<TResult>> Func, int defaultExpireSecondes = 60)
{
//一. 校验缓存类型
ValidateCacheValueType<TResult>();
//二. 利用TryGetValue和CreateEntry方法进行封装
if (!memoryCache.TryGetValue(key, out TResult result))
{
//表示缓存不存在
//2.1 创建或覆盖一个缓存key
using ICacheEntry cacheEntry = memoryCache.CreateEntry(key);
//三. 添加一个随机过期时间
if (defaultExpireSecondes > 0)
{
//只有该值 > 0 才生效
SetCacheRandomTime(cacheEntry, defaultExpireSecondes);
}
//2.2 返回值赋值 (这个值来源于委托的调用,获取的返回值)
result = await Func(cacheEntry);
//2.3 给该缓存赋值
cacheEntry.Value = result;
}
return result!;
}
#endregion
#region 检验缓存value的类型
/// <summary>
/// 检验缓存value的类型
/// 注:这里直接以<T>的形式传递,不写在参数里了
/// </summary>
/// <typeparam name="T">缓存value的类型</typeparam>
private static void ValidateCacheValueType<T>()
{
Type typeResult = typeof(T);
if (typeResult.IsGenericType)
{
//如果是是IEnumerable<String>这样的泛型类型,则把String这样的具体类型信息去掉,再比较
typeResult = typeResult.GetGenericTypeDefinition();
}
//类型比较,使用==进行比较,不要使用IsAssignableTo
var typeList = new List<Type>()
{ typeof(IEnumerable), typeof(IEnumerable<>), typeof(IAsyncEnumerable<T>),typeof(IQueryable),typeof(IQueryable<T>) };
if (typeList.Contains(typeResult))
{
throw new InvalidOperationException($"T of {typeResult} is not allowed, please use List<T> or T[] instead.");
}
}
#endregion
#region 设置缓存随机过期时间
/// <summary>
/// 设置缓存随机过期时间
/// </summary>
/// <param name="entry">缓存实体</param>
/// <param name="expireSecondes">过期时间</param>
private static void SetCacheRandomTime(ICacheEntry entry, int expireSecondes)
{
double result = Random.Shared.NextInt64(expireSecondes, expireSecondes * 2);
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(result);
}
#endregion
}
4、CacheOrRedisController 控制器
[Route("api/[controller]/[action]")]
[ApiController]
public class CacheOrRedisController : ControllerBase
{
/// <summary>
/// 内存缓存
/// </summary>
/// <returns></returns>
[HttpGet]
public string TestMemoryCache([FromServices] IMemoryCachePro memoryCachePro)
{
var nowTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); //模拟从数据库中获取
//1. 加强版的缓存
//1.1 手动不设置过期时间
string result1 = memoryCachePro.GetOrCreate<string>("Cache1", cacheEntry => nowTime);
//1.2 手动设置绝对过期时间(某个时间点)
string result2 = memoryCachePro.GetOrCreate<string>("Cache2", cacheEntry =>
{
cacheEntry.AbsoluteExpiration = new DateTimeOffset(DateTime.Parse("2022-12-08 18:10:00"));
return nowTime;
}, 10);
//1.3 手动设置滑动过期时间
string result3 = memoryCachePro.GetOrCreate<string>("Cache3", cacheEntry =>
{
cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(10);
return nowTime;
}, 30);
//1.4 null策略
DateTime? content = null;
var result4 = memoryCachePro.GetOrCreate<DateTime?>("Cache4", cacheEntry => content);
return JsonConvert.SerializeObject(new
{
result1,
result2,
result3,
result4
});
}
}
5、运行效果
2.IDistributedCache的改造
2.1 解决方案
- 主要是对分布式缓存添加一个随机过期时间,防止
缓存出现雪崩现象
。 - 至于
缓存穿透
,通常采用cache null策略,表现在调用的时候,对目标值不判空,直接存入缓存即可
2.2 依赖
nuget安装:Microsoft.Extensions.Caching.Memory
2.3 解决思路
1、利用SetString和GetString方法两个基础方法来进行封装
2、判断缓存key中是否有值
- 无值
- 先通过defaultExpireSecondes值内容,来决定是否调用封装方法SetCacheRandomTime来设置缓存随机过期时间。
然后,调用Func委托传递过来的方法,获取需要存入缓存的内容。 最后,将内容序列化一下,存入缓存。
- 先通过defaultExpireSecondes值内容,来决定是否调用封装方法SetCacheRandomTime来设置缓存随机过期时间。
- 有值
- 首先,刷新一下缓存,可以达到重置滑动过期时间的目的。 然后,反序列化成对象进行返回。
3、全局注册:builder.Services.AddScoped<IDistributedCachePro, DistributedCachePro>();
2.4 具体代码
1、Program.cs
//优化后的Redis缓存策略注入
builder.Services.AddScoped<IDistributedCachePro, DistributedCachePro>();
2、IDistributedCachePro.cs
/// <summary>
/// 扩展的分布式缓存接口
/// </summary>
public interface IDistributedCachePro
{
/// <summary>
/// 01-读取或设置缓存(同步)
/// </summary>
TResult GetOrCreate<TResult>(string key, Func<DistributedCacheEntryOptions, TResult> Func, int defaultExpireSecondes = 60);
/// <summary>
/// 02-读取或设置缓存(异步)
/// </summary>
Task<TResult> GetOrCreateAsync<TResult>(string key, Func<DistributedCacheEntryOptions, Task<TResult>> Func, int defaultExpireSecondes = 60);
}
3、DistributedCachePro.cs
public class DistributedCachePro : IDistributedCachePro
{
private readonly IDistributedCache distributedCache;
public DistributedCachePro(IDistributedCache distributedCache)
{
this.distributedCache = distributedCache;
}
#region 01-读取或设置缓存(同步)
/// <summary>
/// 01-读取或设置缓存(同步)
/// </summary>
/// <typeparam name="TResult">委托返回类型</typeparam>
/// <param name="key">缓存key</param>
/// <param name="Func">委托,需要传入一个函数
/// 函数的参数为:DistributedCacheEntryOptions
/// 函数的返回值为:TResult
/// </param>
/// <param name="defaultExpireSecondes">默认添加的随机过期时间,随机值为 [defaultExprieSeconds,defaultExprieSeconds * 2]之间
/// (1).默认值为60s,即在60s-120s之间取一个值。
/// (2).如果使用的时候想设置缓存是永久有效的,此时这个值将导致无法设置缓存永久有效,需要将该值改为0(或负数)
/// (3).设置为0 或者 负数,不生效
/// </param>
/// <returns></returns>
public TResult GetOrCreate<TResult>(string key, Func<DistributedCacheEntryOptions, TResult> Func, int defaultExpireSecondes = 60)
{
//判断缓存中是否有值
string result = distributedCache.GetString(key)!;
if (string.IsNullOrEmpty(result))
{
//配置随机过期时间
DistributedCacheEntryOptions options = new();
if (defaultExpireSecondes > 0)
{
SetCacheRandomTime(options, defaultExpireSecondes);
}
//调用方法
TResult value = Func(options);
// 写入缓存
string valueString = JsonConvert.SerializeObject(value); //null会被json序列化为字符串"null",所以可以防范“缓存穿透”
distributedCache.SetString(key, valueString, options);
return value;
}
else
{
//读取缓存
distributedCache.Refresh(key); //重置一下过期时间,便于滑动过期时间延期
return JsonConvert.DeserializeObject<TResult>(result)!; //"null"会被反序列化为null; TResult如果是引用类型,就有为null的可能性;
}
}
#endregion
#region 02-读取或设置缓存(异步)
/// <summary>
/// 02-读取或设置缓存(异步)
/// </summary>
/// <typeparam name="TResult">委托返回类型</typeparam>
/// <param name="key">缓存key</param>
/// <param name="Func">委托,需要传入一个函数
/// 函数的参数为:DistributedCacheEntryOptions
/// 函数的返回值为:TResult
/// </param>
/// <param name="defaultExpireSecondes">默认添加的随机过期时间,随机值为 [defaultExprieSeconds,defaultExprieSeconds * 2]之间
/// (1).默认值为60s,即在60s-120s之间取一个值。
/// (2).如果使用的时候想设置缓存是永久有效的,此时这个值将导致无法设置缓存永久有效,需要将该值改为0(或负数)
/// (3).设置为0 或者 负数,不生效
/// </param>
/// <returns></returns>
public async Task<TResult> GetOrCreateAsync<TResult>(string key, Func<DistributedCacheEntryOptions, Task<TResult>> Func, int defaultExpireSecondes = 60)
{
//判断缓存中是否有值
string? result = await distributedCache.GetStringAsync(key);
if (string.IsNullOrEmpty(result))
{
//配置随机过期时间
DistributedCacheEntryOptions options = new();
if (defaultExpireSecondes > 0)
{
SetCacheRandomTime(options, defaultExpireSecondes);
}
//调用方法
TResult value = await Func(options);
// 写入缓存
string valueString = JsonConvert.SerializeObject(value); //null会被json序列化为字符串"null",所以可以防范“缓存穿透”
await distributedCache.SetStringAsync(key, valueString, options);
return value;
}
else
{
//读取缓存
await distributedCache.RefreshAsync(key); //重置一下过期时间,便于滑动过期时间延期
return JsonConvert.DeserializeObject<TResult>(result)!; //"null"会被反序列化为null; TResult如果是引用类型,就有为null的可能性;
}
}
#endregion
#region 设置缓存随机过期时间
/// <summary>
/// 设置缓存随机过期时间
/// </summary>
/// <param name="expireSecondes">过期时间</param>
private static DistributedCacheEntryOptions SetCacheRandomTime(DistributedCacheEntryOptions options, int expireSecondes)
{
double result = Random.Shared.NextInt64(expireSecondes, expireSecondes * 2);
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(result);
return options;
}
#endregion
}
4、CacheOrRedisController控制器
[Route("api/[controller]/[action]")]
[ApiController]
public class CacheOrRedisController : ControllerBase
{
/// <summary>
/// 内存缓存和Redis缓存无缝切换
/// </summary>
/// <returns></returns>
[HttpGet]
public string TestDistributedCache([FromServices] IDistributedCachePro distributedCachePro)
{
//1. 加强版的缓存
//1.1 手动不设置过期时间
var result1 = distributedCachePro.GetOrCreate<PagingClass>("page1", cacheEntry =>
{
PagingClass page = new PagingClass { pageNum = 1, pageSize = 10 }; //模拟从数据库中获取
return page;
});
//1.2 手动设置绝对过期时间(某个时间点)
var result2 = distributedCachePro.GetOrCreate<PagingClass>("page2", cacheEntry =>
{
cacheEntry.AbsoluteExpiration = new DateTimeOffset(DateTime.Parse("2022-12-08 18:10:00"));
PagingClass page = new PagingClass { pageNum = 1, pageSize = 10 }; //模拟从数据库中获取
return page;
});
//1.3 手动设置滑动过期时间
var result3 = distributedCachePro.GetOrCreate<PagingClass>("page3", cacheEntry =>
{
cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(10);
PagingClass page = new PagingClass { pageNum = 1, pageSize = 10 }; //模拟从数据库中获取
return page;
}, 30);
//1.4 Cache Null
var result4 = distributedCachePro.GetOrCreate<PagingClass>("page4", cacheEntry =>
{
PagingClass page = null; //模拟从数据库中获取
return page;
});
return JsonConvert.SerializeObject(new
{
result1,
result2,
result3,
result4
});
}
}
public class PagingClass
{
public int pageNum { get; set;}
public int pageSize { get; set;}
}
5、运行效果
- 点赞
- 收藏
- 关注作者
评论(0)