【愚公系列】2022年12月 Redis数据库-缓存雪崩和缓存穿透问题的解决

2022-12-09 13:47:07 浏览数 (1)

文章目录

  • 前言
  • 一、缓存雪崩和缓存穿透问题的解决
    • 1.IMemoryCache的改造
      • 1.1 解决方案
      • 1.2 依赖
      • 1.3 解决思路
      • 1.4 具体代码
    • 2.IDistributedCache的改造
      • 2.1 解决方案
      • 2.2 依赖
      • 2.3 解决思路
      • 2.4 具体代码

前言

接上文: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这样的具体类型信息去掉,变成IEnumerable<>再比较。

3、增加随机过期时间

  • 默认值为60s,即在60s-120s之间取一个值。
  • 如果使用的时候想设置缓存是永久有效的,此时这个值将导致无法设置缓存永久有效,需要将该值改为0(或负数)
  • 设置为0 或者 负数,不生效

4、全局注册:builder.Services.AddScoped<IMemoryCachePro, MemoryCachePro>();

1.4 具体代码

1、Program.cs

代码语言:javascript复制
//优化后的Cache缓存策略
builder.Services.AddScoped<IMemoryCachePro, MemoryCachePro>();

2、IMemoryCachePro.cs

代码语言:javascript复制
/// <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

代码语言:javascript复制
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 控制器

代码语言:javascript复制
[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委托传递过来的方法,获取需要存入缓存的内容。 最后,将内容序列化一下,存入缓存。
  • 有值
    • 首先,刷新一下缓存,可以达到重置滑动过期时间的目的。 然后,反序列化成对象进行返回。

3、全局注册:builder.Services.AddScoped<IDistributedCachePro, DistributedCachePro>();

2.4 具体代码

1、Program.cs

代码语言:javascript复制
//优化后的Redis缓存策略注入
builder.Services.AddScoped<IDistributedCachePro, DistributedCachePro>();

2、IDistributedCachePro.cs

代码语言:javascript复制
/// <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

代码语言:javascript复制
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控制器

代码语言:javascript复制
[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 人点赞