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 分析原因:
不然看出,就是因为数据存在空的时候,没有做任何处理,导致每次去数据库查空,以此为突破点,我们可以考虑下面的解决方案
- 业务处理,不要让空的数据请求进到这里
- 空的数据也缓存起来,不过要考虑空数据的缓存对业务的影响
- 针对第二点,我们可以借助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点电商秒杀,用户全是卡着这个点,一瞬间大量请求进来,缓存刚好失效状态,全命中数据库,数据库连接池满,导致查询排队,请求响应持续放慢,后续用户继续压倒性的跟上,直接爆炸,缓存就,缓存了个寂寞
我们可以考虑下面的解决方案
- 直接设置key不要过期,都不存在失效的场景
- 缓存预热,在活动开始前,把缓存全部查出来
- 上锁,让查询缓存和查询数据库为一个动作,不过要考虑不能影响其他请求
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个人就短时间内都去查了数据库,数据又爆炸了
我们针对这几点解决
- 缓存高可用,不要让redis爆炸了
- 缓存提前预热,保证包并发下不过期
- 均匀分布过期时间,每个用户的过期时间加的随机点长度
上面的解决方法,都没啥说的,没啥实操的,看了基本都懂了
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
路漫漫其修远兮,吾将上下而求索
未来的故事,来不及讲会有多跌宕,我们拭目以待