【橙子老哥】C# 实操缓存失效三大场景

hello,大家好呀,欢迎来到橙子老哥的分享时刻,希望大家一起学习,一起进步。

欢迎加入.net意社区,第一时间了解我们的动态,文章第一时间分享至社区

社区官方地址:https://ccnetcore.com (上千.neter聚集地)(注册人数:1731人)

官方微信公众号:公众号搜索 意.Net

官方微信小程序:小程序搜索 意.Net

添加橙子老哥微信加入官方微信群:chengzilaoge520

1、缓存失效场景

前一段时间有人找到我,抱怨

“缓存穿透、击穿、雪崩搞这么多概念干什么,记又难记,也没啥用”

这里其实很多人也有这种想法,原因只是因为没有遇到高并发场景,自然怎么写都不会出问题,上面的经典的三个场景都是前人总结的宝贵经验。

众所皆知,高并发下的代码和没有高并发下的代码,完全是两种写法,没有经过高并发的洗礼,代码怎么写都行

当有高并发的场景下,系统的瓶颈就诞生了,最先可能出现问题地方就是数据库了。

这是因为,程序可以很好做分布式集群处理,1个扛不住,再来10个,但是相对于数据库来说,就不太容易了,而且数据的持久化相对于内存来说,完全不是一个量级

因此,通常在高并发下,先经过缓存的处理,缓存能处理的就不走数据库了,能极大提高系统的吞吐量和响应速度

但是,使用缓存真的就那么简单吗?我们接下来往下看

那就是我们要实操的,面试必备宝典缓存失效三架马车

  • 缓存穿透
  • 缓存击穿
  • 缓存雪崩

关于这3个经典的场景,网上资料一大堆,都搜烂了的那种,不过我接触到一些人,仅仅是停留在能说,能理论,能背出来的地步,没有真正去实操下去,接下来,直接实操

2、实操准备

这里我们准备两个类,代表mysql数据库和redis缓存,每次操作的时候都会打印一句话

class RedisDb
{
    public string? Data = null;

    public string? GetData()
    {
        Console.WriteLine("命中缓存-调用redis查询");
        return Data;
    }

    public void SetData(string data)
    {
        Console.WriteLine("命中缓存-调用redis赋值");
        Data = data;
    }

    public void Expire()
    {
        Console.WriteLine("缓存过期");
        Data = null;
    }
}

class MysqlDb
{
    public string? Data = null;

    public string? GetData()
    {
        Console.WriteLine("命中数据库-调用Mysql查询");
        return Data;
    }

    public void Update(string data)
    {
        Console.WriteLine("命中数据库-调用Mysql更新");
        Data = data;
    }
}

3、缓存穿透

3.1 大白话
请求查询缓存一直是空,再查询数据库也是空,大量请求下来,每次都要去查询缓存和数据库,导致数据库爆炸了

3.2 问题代码

var redisDb=new RedisDb();
var mysqlDb=new MysqlDb();

mysqlDb.Update(null);
int i = 10;

while (i > 0)
{
    Console.WriteLine($"-----开始查询-----");
    string? queryData = null;
	queryData = redisDb.GetData();
	if (queryData is null)
    {
        queryData= mysqlDb.GetData();
        if (queryData is not null)
        {
            redisDb.SetData(queryData);
        }
    }
    Console.WriteLine($"-----查询结果-{queryData}-----");
    i--;
}

上面代码,我们给数据库初始化一个空,执行10遍查询结果:

命中数据库-调用Mysql更新
-----开始查询-----
命中缓存-调用redis查询
命中数据库-调用Mysql查询
-----查询结果------
-----开始查询-----
命中缓存-调用redis查询
命中数据库-调用Mysql查询
-----查询结果------
-----开始查询-----
命中缓存-调用redis查询
命中数据库-调用Mysql查询
-----查询结果------
省略后续

全部打到数据库上了,缓存就失效了

3.3 分析原因
不然看出,就是因为数据存在空的时候,没有做任何处理,导致每次去数据库查空,以此为突破点,我们可以考虑下面的解决方案

  1. 业务处理,不要让空的数据请求进到这里
  2. 空的数据也缓存起来,不过要考虑空数据的缓存对业务的影响
  3. 针对第二点,我们可以借助redis的 布隆过滤器,记录空数据的查询

3.4 空数据缓存解决方案

var redisDb=new RedisDb();
 var mysqlDb=new MysqlDb();

 int i = 10;
mysqlDb.Update(null);
 while (i > 0)
 {
     Console.WriteLine($"-----开始查询-----");
     string? queryData = null;
     queryData = redisDb.GetData();
     if (queryData is null)
     {
         queryData= mysqlDb.GetData();
         if (queryData is not null)
         {
             redisDb.SetData(queryData);
         }
         else
         {
             //空数据也进行缓存,但是要注意不能影响业务
             redisDb.SetData(String.Empty);
         }
     }
     Console.WriteLine($"-----查询结果-{queryData}-----");
     i--;
 }

执行结果

-----开始查询-----
命中缓存-调用redis查询
命中数据库-调用Mysql查询
命中缓存-调用redis赋值
-----查询结果------
-----开始查询-----
命中缓存-调用redis查询
-----查询结果------
-----开始查询-----
命中缓存-调用redis查询
-----查询结果------
-----开始查询-----
命中缓存-调用redis查询
-----查询结果------
-----开始查询-----
命中缓存-调用redis查询
-----查询结果------

没有一直打在数据库上
3.5 总结:
缓存穿透是“应用”上出现的问题,即使没有并发,也会每次查询数据库,需要在数据库不存在的情况也进行保存起来

4、缓存穿透

4.1 大白话
有一个key失效了,这个key又很特殊,会有大量的请求都打在这个key上,因为这个key失效了,一瞬间全打在数据库上了,导致数据库爆炸了

  • 这里的key,也叫做热点key;
  • 这里的失效,基本上是因为key过期;
  • 这里的一瞬间,指的是没有缓存的下次并发请求,走完这波请求,有缓存了,就正常了

4.2 问题代码

var redisDb=new RedisDb();
var mysqlDb=new MysqlDb();

//同一个key进入刚好过期
int i = 10;

mysqlDb.Update("996");
redisDb.Expire();
//这里是并发
Parallel.ForEach(Enumerable.Range(1, i), (item, state, index) =>
{
    Console.WriteLine($"-----开始查询-----");
    string? queryData = null;
    queryData = redisDb.GetData();
    if (queryData is null)
    {
        queryData= mysqlDb.GetData();
        if (queryData is not null)
        {
            redisDb.SetData(queryData);
        }
    }
    Console.WriteLine($"-----查询结果-{queryData}-----");

});

上面这个代码,一看好像没问题,但是实际结果,这10个请求大部分都去查询数据库了
结果:

缓存过期
-----开始查询-----
-----开始查询-----
-----开始查询-----
-----开始查询-----
命中缓存-调用redis查询
-----开始查询-----
命中缓存-调用redis查询
-----开始查询-----
命中缓存-调用redis查询
-----开始查询-----
命中缓存-调用redis查询
命中数据库-调用Mysql查询
命中缓存-调用redis查询
命中数据库-调用Mysql查询
命中缓存-调用redis查询
命中数据库-调用Mysql查询
命中缓存-调用redis赋值
命中数据库-调用Mysql查询
命中缓存-调用redis赋值
省略

4.3 分析原因
问题在于,查询缓存和查询数据库,是两个动作,不是原子性的,导致当缓存为空,多个线程都是空,全都打到数据库上,这在高并发场景下是很恐怖的

例如:说烂了的0点电商秒杀,用户全是卡着这个点,一瞬间大量请求进来,缓存刚好失效状态,全命中数据库,数据库连接池满,导致查询排队,请求响应持续放慢,后续用户继续压倒性的跟上,直接爆炸,缓存就,缓存了个寂寞

我们可以考虑下面的解决方案

  1. 直接设置key不要过期,都不存在失效的场景
  2. 缓存预热,在活动开始前,把缓存全部查出来
  3. 上锁,让查询缓存和查询数据库为一个动作,不过要考虑不能影响其他请求

4.4 上锁解决方案:

 var redisDb = new RedisDb();
 var mysqlDb = new MysqlDb();

 //同一个key进入刚好过期
 int i = 10;

 mysqlDb.Update("996");
 redisDb.Expire();
 Parallel.ForEach(Enumerable.Range(1, i), (item, state, index) =>
 {
     Console.WriteLine($"-----开始查询-----");
     string? queryData = null;

     //其他请求发现已经上锁了,延迟查询
     //查询到的redisDbkey,刚好同一时间点过期了,这个key以及过期并被移除
     queryData = redisDb.GetData();
     if (queryData is null)
     {
         lock (redisDb)
         {
             queryData = redisDb.GetData();
             if (queryData is null)
             {
                 queryData = mysqlDb.GetData();
                 if (queryData is not null)
                 {
                     redisDb.SetData(queryData);
                 }
             }
         }
     }

     Console.WriteLine($"-----查询结果-{queryData}-----");
 });

这里要注意,分布式下,当然要使用分布锁,同时要注意双锁问题

因为,只在缓存过期后才会执行锁的代码,当有缓存的时候,性能是不会受到影响的

执行结果:

省略
-----开始查询-----
命中缓存-调用redis查询
-----开始查询-----
命中缓存-调用redis查询
命中数据库-调用Mysql查询
-----开始查询-----
命中缓存-调用redis查询
命中缓存-调用redis赋值
命中缓存-调用redis查询
-----查询结果-996-----
-----查询结果-996-----
命中缓存-调用redis查询
-----查询结果-996-----
命中缓存-调用redis查询
-----查询结果-996-----
命中缓存-调用redis查询
省略

嗯,不错,确实只命中了一次数据库,是我们要的结果
4.5 总结:
缓存击穿是“并发”上出现的问题,一个热点key过期,导致大量请求同一时刻进来,就那么一下,全查数据库了

5、缓存雪崩

5.1 大白话
一大片key同时过期了,多个请求请求各自的key,然后都查不到,都去查数据库了,数据库就又爆炸了

5.2 原因分析
这个就很好理解了,短时间内请求没有查到缓存,全查到数据库上了,问题在于为啥会同一时间都没有缓存了?

redis爆炸了、大量设置了同一时刻过期缓存,都可能导致这个问题

例如:还是那个秒杀的场景,1w个人的key过期了,他们查各自的购物车的时候,都查不到,1w个人就短时间内都去查了数据库,数据又爆炸了

我们针对这几点解决

  1. 缓存高可用,不要让redis爆炸了
  2. 缓存提前预热,保证包并发下不过期
  3. 均匀分布过期时间,每个用户的过期时间加的随机点长度

上面的解决方法,都没啥说的,没啥实操的,看了基本都懂了

5.3 总结:
缓存过期了,下次进来都从数据库中查询一次

缓存雪崩是,刚好一大片的缓存同时过期,都去查一遍,数据库就炸了,说明缓存的过期时间分布不均匀

0、热爱与奉献:橙子老哥的人生哲学

在这里插入图片描述
告诉大家一个好消息:

2025年我很高兴获得微软MVP的荣誉,感谢微软的认可,今年也算是圆梦了

六年时光,是微软成就了我,亦是我成就了更好的自己

郑重地感谢以下前辈、老师、同伴,是你们引领着我前进,没有你们就没有如今的我:
姚双贵、郭森辉、温海靖、徐院长、Eleven、张善友、杨中科、瓜哥、上海老杰哥、token、车神、薛家明、Gerry、狼兄、字母老哥

另外,同样真挚的感谢为社区贡献的成员:
lzw、tyjctl、李大饼、Po、凯明、Bi8bo、daxiongok、fuxing168、小希、窗外的麻雀、MengXQ6666、fengxiangguo、gyduhl、faith、凤凰、Xiaoli、1538943610、Your Name、GitHubList、呆呆0518、canye、黎明、songjianjack、zsmygitee、高级工程师、zhouhr

路漫漫其修远兮,吾将上下而求索

未来的故事,来不及讲会有多跌宕,我们拭目以待

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值