如何解决redis缓存击穿问题 您所在的位置:网站首页 批量登录怎么解决问题 如何解决redis缓存击穿问题

如何解决redis缓存击穿问题

2024-04-04 22:06| 来源: 网络整理| 查看: 265

1. 什么是redis的缓存击穿?

如果我有一个业务,需要查询数据库,这个查询很耗时,且业务上来看这个要非常频繁的取查询它,那么通常我可以把查询的结果保存redis,设置一个符合业务的过期时间,然后以后的查询都直接查redis redis的高QPS特性,可以很好的解决查数据库很慢的问题。但是如果我们系统的并发很高,在某个时间节点,突然缓存失效,这时候有大量的请求打过来,那么由于redis没有缓存数据,这时候我们的请求会全部去查一遍数据库,这时候我们的数据库服务会面临非常大的风险,要么连接被占满,要么其他业务不可用,这种情况就是redis的缓存击穿

2. 如何解redis决缓存击穿

解决缓存击穿换句话说,就是要保证最终落在硬盘上的查询操作要可控,风险可控对系统稳定至关重要。

2.1 普通的redis缓存使用方式 先来看看普通的redis缓存设计

2.1.1 使用redis缓存 比较简单的设计,可以在数据需要的时候再加载 redis简单缓存设计 由此图可以看出,查询是只有两种情况的:

1、缓存命中,直接返回缓存结果 2、缓存未命中,查数据库,再返回结果

优点

设计简单,开发效率高 不会侵入业务代码,spring的aop就能很好的实现

缺点

一旦redis的数据失效,那么假如这时候有10w个请求打过来,由于redis没有缓存,那么按照缓存设计的逻辑,就会全部去查数据库

适用情况

如果一个系统是内部系统,天生没有太多的用户量,而某个接口需要非常耗时的查询,而数据变化又不会太频繁,就比较适用这种设计, 没有太多用户,就不会造成缓存失效的时候大量请求压到数据库的问题

2.2 解决redis缓存击穿问题 方案一、定时任务主动刷新缓存设计 方案二、使用redis的分布式锁 方案三、普通加jvm的锁查询缓存 方案四、jvm缓存+redis缓存的多级缓存

以上四种方案的设计都解决了缓存击穿的问题,只是他们的设计各有利弊,我们需要在不同情况下来使用,下面具体分析使用利弊。

1、定时任务主动刷新缓存设计

如果我们的数据是保存在redis缓存中,而redis缓存失效之后一定会查数据库,那么我们是否可以主动出击,在redis缓存失效之前,主动查数据库?定时任务去调度主动查询就可以解决。

查询逻辑

先将所有可能查询到的数据存入redis,对redis中的数据库定时更新,保证redis永远都会有数据存在,来请求只查redis。 定时任务主动刷新redis

优点

1、用户的请求压力永远不会直接打到数据库上 2、查询redis的内存数据,查询效率很高

不足

1、可能对redis内存消耗非常大,因为要提前将数据加载到缓存 增加了系统的复杂度,必须要有个非常可靠的定时任务操作,不然一旦定时任务失效,那么redis中的数据失效,对于用户来说就是服务不可用 数据的实时性非常依赖定时任务的执行频率,定时任务执行的频率高,实时性就强 2、数据不能保证实时刷新,如果刷新缓存的间隔设置很长,那么数据实时性就不够好,如果刷新缓存的间隔很短,那么频繁的全量刷数据库到缓存对系统和数据库都是压力,也会让数据库和应用服务器的负载变得不够平稳 3、由于是只查询缓存,所以会对业务代码进行较大程度的改动,后期业务变化,可能会非常难以维护

适用情况

1、已有一套现成的高可靠分布式定时任务系统 2、查询的数据变化不大 3、用户的请求量非常大的情况下

使用redis的分布式锁

对最基本的使用redis缓存的设计进行一些改动,让我们对数据库的重复查询操作变为1次,既然我们使用了redis,那么可以利用redis实现的分布式锁setnx 来实现互斥的数据库操作 使用redis的分布式锁

查询逻辑

如果缓存命中直接返回数据集 如果缓存没有,则尝试获取分布式锁(有超时设置) 如果没有拿到锁,则阻塞当前线程,n秒,之后再次尝试获取分布式锁(自旋,轮询,浪费CPU) 拿到锁之后检查数据是否已经被其他线程放到redis缓存中,如果redis缓存已有,直接返回redis中的数据,释放分布式锁 如果缓存没有被刷新,则查数据库 将数据库查询的结果保存到redis缓存中 返回查询结果

优点

1、数据的实时性较高,不需要其他外部系统依赖,利用了redis自己的特性,实现分布式锁,保证了同样的数据库查询同时只会查询1次,对数据库的压力较小 2、不会侵入业务代码,spring的aop就能很好的实现

不足

1、由于阻塞等待分布式锁是个自旋阻塞操作,所以其实对应用服务器来说非常浪费cpu的分片时间,如果这时候大量请求打过来, 应用服务器反而会先扛不住,因为这里会有大量的线程在自旋占用CPU,如果用户的查询是由多个系统的结果构成,每个系统的查询依赖上一个系统查询的结果,各个查询是串行的,那么自旋的睡眠时间可能会成为拖慢请求的罪魁祸首,多个系统都这么设计都在自旋睡眠,明显效率很低

适用情况

如果要求保证数据库的压力特别小,同样的请求只能查询一次数据库, 而且服务器较多,足以将多个请求分散到不同服务器,不至于造成太多线程自旋,那么可以使用这样的设计,但不推荐,因为这种自旋操作真的不是个好设计

3、普通加jvm的锁查询缓存

上面分布式锁自旋的方法,真的不优雅 这时候我们需要反问一下自己,每个请求,真的只能查询一次数据库吗?数据库的压力已经大到如此地步了吗? 如果不是 那么下面还有更加合适的设计 不再强求相同的查询只能查一次数据库

普通加jvm的锁查询缓存

查询逻辑

如果缓存命中直接返回数据集 如果缓存没有,则尝试JVM锁,其他线程阻塞 拿到锁之后,检查redis是否有数据,以免其他线程已经刷过缓存 如果redis已经有数据,直接返回,并释放锁,返回数据库结束 如果redis没有数据,则查询数据库,并保存到redis缓存中 返回数据,释放锁 设有s台服务器,用户请求数为n 那么同一时间参数相同的请求最多只会有s次查询打到数据库上,这里s这个常量 相当于原来对于数据库来说一个O(n)的操作时间下降到了O(s) 这里可以看出,查询数据库操作的耗时与n的增长无关,只与s有关

想象一下,我们有4台服务器,本来打到数据库上可能有10w个查询,但是因为我们使用了jvm的锁,每台服务器只会查询一次,总的数据库查询次数下降到了4次,是不是很高效?而且jvm提供的锁一定比redis分布式锁自旋轮询高效太多!

优点

1、数据的实时性较高 相对于使用redis分布式锁,大幅降低服务器资源的消耗,jvm的锁效率要高很多 2、对于数据库的消耗较小,是一个和服务器数量s相关的耗时操作,与请求数量n无关(n可能会很大,十万,百万级别,而s可能最多两位数) 如果mysql数据库版本较低,说不定还能利用上mysql数据库的缓存,如果是个不频繁更新的表,运气好的情况下s-1次的查询可能都会命中mysql的缓存 3、实现的复杂度低 4、不会侵入业务代码,spring的aop就能很好的实现

不足

1、对数据库查询虽然减小到了一个只与服务器数量相关的函数,但依然有冗余(其实也还好了)

适用情况

如果能容忍较少次数的数据库重复查询 这种设计就用这种就已经能很好的解决缓存穿透的问题了,而且设计简单复杂度低 复杂度低意味着系统的稳定

4、jvm缓存+redis缓存的多级缓存

jvm缓存+redis缓存的多级缓存

查询的逻辑如图所示

二级缓存的关键在于:

jvm的缓存时间是个随机值,比如 10秒~30秒 这种设计,服务器只会在jvm缓存失效,且redis缓存也失效的情况下才会查询数据库,而多个服务器的jvm缓存失效时间是随机值,所以很大程度上避免的同时失效去查库的情况,由于所有服务器jvm缓存同时失效redis缓存也失效的可能性极低,所以数据库上重复的查询会很少 (不一定是jvm缓存和jvm的锁啊,python,go同理) 设服务器的台数为s

如何让O(s)的问题其变为O(1)呢?其实也是有办法的,就是多级缓存 就是让每台服务器上加一个jvm的缓存在redis之前 这个jvm的缓存时间需要设置一个随机值,比如 缓存时间为 5s-10s,这样可以很大程度避免在redis失效的时候,每台服务器都需要去做更新redis缓存的操作,因为每个服务器的jvm缓存失效时间是不一样的

优点

1、数据的实时性较高 (设置合适的jvm缓存过期时间和redis缓存过期时间) 2、几乎没有冗余的数据库查询 3、绝大多数查询是使用的jvm缓存,效率极高 4、对cpu的占用很低 5、不会侵入业务代码,spring的aop就能很好的实现

不足

1、如果查询的参数离散度较高,其实会很浪费业务服务器的内存空间(但是可以通过减少jvm缓存的时间来优化一点) 2、设计稍微有点复杂,需要有经验的工程师来实现

适用情况

几乎可以支持所有情况,强力推荐

原文请参看:https://blog.csdn.net/zyjzyj2/article/details/105027746



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有